]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: renamed dashboard_v2 to dashboard 20912/head
authorRicardo Dias <rdias@suse.com>
Thu, 15 Mar 2018 08:50:22 +0000 (08:50 +0000)
committerRicardo Dias <rdias@suse.com>
Fri, 16 Mar 2018 07:23:08 +0000 (07:23 +0000)
Signed-off-by: Ricardo Dias <rdias@suse.com>
576 files changed:
CMakeLists.txt
ceph.spec.in
debian/rules
make-dist
qa/suites/rados/mgr/tasks/dashboard.yaml [new file with mode: 0644]
qa/suites/rados/mgr/tasks/dashboard_v2.yaml [deleted file]
qa/tasks/mgr/dashboard/__init__.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/helper.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_auth.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_cephfs.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_cluster_configuration.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_dashboard.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_host.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_monitor.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_osd.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_perf_counters.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_pool.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_rbd.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_rgw.py [new file with mode: 0644]
qa/tasks/mgr/dashboard/test_summary.py [new file with mode: 0644]
qa/tasks/mgr/dashboard_v2/__init__.py [deleted file]
qa/tasks/mgr/dashboard_v2/helper.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_auth.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_cephfs.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_cluster_configuration.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_dashboard.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_host.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_monitor.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_osd.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_perf_counters.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_pool.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_rbd.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_rgw.py [deleted file]
qa/tasks/mgr/dashboard_v2/test_summary.py [deleted file]
qa/tasks/mgr/test_dashboard.py [new file with mode: 0644]
qa/tasks/mgr/test_dashboard_v2.py [deleted file]
src/pybind/mgr/CMakeLists.txt
src/pybind/mgr/dashboard/.coveragerc [new file with mode: 0644]
src/pybind/mgr/dashboard/.editorconfig [new file with mode: 0644]
src/pybind/mgr/dashboard/.gitignore [new file with mode: 0644]
src/pybind/mgr/dashboard/.pylintrc [new file with mode: 0644]
src/pybind/mgr/dashboard/CMakeLists.txt [new file with mode: 0644]
src/pybind/mgr/dashboard/HACKING.rst [new file with mode: 0644]
src/pybind/mgr/dashboard/README.rst [new file with mode: 0644]
src/pybind/mgr/dashboard/__init__.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/__init__.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/auth.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/cephfs.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/cluster_configuration.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/dashboard.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/host.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/monitor.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/osd.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/perf_counters.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/pool.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/rbd.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/rgw.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/summary.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/.angular-cli.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/.editorconfig [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/.gitignore [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/e2e/app.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/e2e/app.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/e2e/tsconfig.e2e.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/karma.conf.js [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/package.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/protractor.conf.js [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/proxy.conf.json.sample [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/app.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/app.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/app.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/app.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/view-cache-status.enum.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-selection.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/chart-tooltip.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-interceptor.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/host.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/pool.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/services.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/tcmu-iscsi.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/assets/.gitkeep [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/assets/1280px-Mimic_Octopus2.jpg [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/assets/loading.gif [new file with mode: 0755]
src/pybind/mgr/dashboard/frontend/src/assets/logo-mini.png [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/assets/notification-icons.png [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/defaults.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/environments/environment.prod.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/environments/environment.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/favicon.ico [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/index.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/main.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/openattic-theme.scss [new file with mode: 0755]
src/pybind/mgr/dashboard/frontend/src/polyfills.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/styles.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/styles/chart-tooltip.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/test.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/tsconfig.app.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/tsconfig.spec.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/typings.d.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/tsconfig.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/tslint.json [new file with mode: 0644]
src/pybind/mgr/dashboard/module.py [new file with mode: 0644]
src/pybind/mgr/dashboard/requirements.txt [new file with mode: 0644]
src/pybind/mgr/dashboard/run-backend-api-tests.sh [new file with mode: 0755]
src/pybind/mgr/dashboard/run-frontend-unittests.sh [new file with mode: 0755]
src/pybind/mgr/dashboard/run-tox.sh [new file with mode: 0755]
src/pybind/mgr/dashboard/services/__init__.py [new file with mode: 0644]
src/pybind/mgr/dashboard/services/ceph_service.py [new file with mode: 0644]
src/pybind/mgr/dashboard/settings.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tests/__init__.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tests/helper.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tests/test_notification.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tests/test_settings.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tests/test_tools.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tools.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tox.ini [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/.coveragerc [deleted file]
src/pybind/mgr/dashboard_v2/.editorconfig [deleted file]
src/pybind/mgr/dashboard_v2/.gitignore [deleted file]
src/pybind/mgr/dashboard_v2/.pylintrc [deleted file]
src/pybind/mgr/dashboard_v2/CMakeLists.txt [deleted file]
src/pybind/mgr/dashboard_v2/HACKING.rst [deleted file]
src/pybind/mgr/dashboard_v2/README.rst [deleted file]
src/pybind/mgr/dashboard_v2/__init__.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/__init__.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/auth.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/cephfs.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/cluster_configuration.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/dashboard.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/host.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/monitor.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/osd.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/perf_counters.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/pool.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/rbd.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/rbd_mirroring.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/rgw.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/summary.py [deleted file]
src/pybind/mgr/dashboard_v2/controllers/tcmu_iscsi.py [deleted file]
src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json [deleted file]
src/pybind/mgr/dashboard_v2/frontend/.editorconfig [deleted file]
src/pybind/mgr/dashboard_v2/frontend/.gitignore [deleted file]
src/pybind/mgr/dashboard_v2/frontend/e2e/app.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/e2e/app.po.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/e2e/tsconfig.e2e.json [deleted file]
src/pybind/mgr/dashboard_v2/frontend/karma.conf.js [deleted file]
src/pybind/mgr/dashboard_v2/frontend/package.json [deleted file]
src/pybind/mgr/dashboard_v2/frontend/protractor.conf.js [deleted file]
src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/block.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/components.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/datatable.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/view-cache-status.enum.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-column.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-selection.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/chart-tooltip.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/pipes.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/host.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/pool.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/services.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.spec.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/tcmu-iscsi.service.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/assets/.gitkeep [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/assets/1280px-Mimic_Octopus2.jpg [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/assets/loading.gif [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/assets/logo-mini.png [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/assets/notification-icons.png [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/defaults.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.prod.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/favicon.ico [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/index.html [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/main.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/polyfills.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/styles.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/styles/chart-tooltip.scss [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/test.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.app.json [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.spec.json [deleted file]
src/pybind/mgr/dashboard_v2/frontend/src/typings.d.ts [deleted file]
src/pybind/mgr/dashboard_v2/frontend/tsconfig.json [deleted file]
src/pybind/mgr/dashboard_v2/frontend/tslint.json [deleted file]
src/pybind/mgr/dashboard_v2/module.py [deleted file]
src/pybind/mgr/dashboard_v2/requirements.txt [deleted file]
src/pybind/mgr/dashboard_v2/run-backend-api-tests.sh [deleted file]
src/pybind/mgr/dashboard_v2/run-frontend-unittests.sh [deleted file]
src/pybind/mgr/dashboard_v2/run-tox.sh [deleted file]
src/pybind/mgr/dashboard_v2/services/__init__.py [deleted file]
src/pybind/mgr/dashboard_v2/services/ceph_service.py [deleted file]
src/pybind/mgr/dashboard_v2/settings.py [deleted file]
src/pybind/mgr/dashboard_v2/tests/__init__.py [deleted file]
src/pybind/mgr/dashboard_v2/tests/helper.py [deleted file]
src/pybind/mgr/dashboard_v2/tests/test_notification.py [deleted file]
src/pybind/mgr/dashboard_v2/tests/test_rbd_mirroring.py [deleted file]
src/pybind/mgr/dashboard_v2/tests/test_settings.py [deleted file]
src/pybind/mgr/dashboard_v2/tests/test_tcmu_iscsi.py [deleted file]
src/pybind/mgr/dashboard_v2/tests/test_tools.py [deleted file]
src/pybind/mgr/dashboard_v2/tools.py [deleted file]
src/pybind/mgr/dashboard_v2/tox.ini [deleted file]
src/test/CMakeLists.txt
src/test/mgr/CMakeLists.txt
src/test/mgr/mgr-dashboard-smoke.sh [new file with mode: 0755]
src/test/mgr/mgr-dashboard_v2-smoke.sh [deleted file]
src/vstart.sh

index b521b9cec6ee0eb6351f869ad57f5a476a61f32d..a0eb77b7414978b2cd660a20f2e9b3a3648ab837 100644 (file)
@@ -613,7 +613,7 @@ endif()
 set(Boost_USE_MULTITHREADED ON)
 
 # dashboard angular2 frontend
-option(WITH_MGR_DASHBOARD_V2_FRONTEND "Build the mgr/dashboard_v2 frontend using `npm`" ON)
+option(WITH_MGR_DASHBOARD_FRONTEND "Build the mgr/dashboard frontend using `npm`" ON)
 
 include_directories(SYSTEM ${PROJECT_BINARY_DIR}/include)
 
index 5e890c4c6187c76b76c08844d811785a62c933c8..cc4d0ee5da508f39356c2a51cb44a91b364d9885 100644 (file)
@@ -899,7 +899,7 @@ cmake .. \
     -DWITH_EMBEDDED=OFF \
     -DWITH_MANPAGE=ON \
     -DWITH_PYTHON3=ON \
-    -DWITH_MGR_DASHBOARD_V2_FRONTEND=OFF \
+    -DWITH_MGR_DASHBOARD_FRONTEND=OFF \
 %if %{with python2}
     -DWITH_PYTHON2=ON \
 %else
index 17a0d804b306efc881209f87cea766ed9834e47f..f1e150035fa213a53c5e961bc68794395ba63771 100755 (executable)
@@ -5,7 +5,7 @@ export DESTDIR=$(CURDIR)/debian/tmp
 
 export DEB_HOST_ARCH      ?= $(shell dpkg-architecture -qDEB_HOST_ARCH)
 
-extraopts += -DUSE_CRYPTOPP=OFF -DWITH_OCF=ON -DWITH_LTTNG=ON -DWITH_PYTHON3=ON -DWITH_EMBEDDED=OFF -DWITH_MGR_DASHBOARD_V2_FRONTEND=OFF
+extraopts += -DUSE_CRYPTOPP=OFF -DWITH_OCF=ON -DWITH_LTTNG=ON -DWITH_PYTHON3=ON -DWITH_EMBEDDED=OFF -DWITH_MGR_DASHBOARD_FRONTEND=OFF
 extraopts += -DWITH_CEPHFS_JAVA=ON
 extraopts += -DWITH_SYSTEMD=ON -DCEPH_SYSTEMD_ENV_DIR=/etc/default
 # assumes that ceph is exmpt from multiarch support, so we override the libdir.
index e492fae93e0f0be386972912d31a6e44004210b8..607caf3f9c7be3965ae3113e74106fcdc3eefbd6 100755 (executable)
--- a/make-dist
+++ b/make-dist
@@ -63,11 +63,11 @@ download_boost() {
 
 build_dashboard_frontend() {
   CURR_DIR=`pwd`
-  cd src/pybind/mgr/dashboard_v2/frontend
+  cd src/pybind/mgr/dashboard/frontend
   npm install
   npm run build -- --prod
   cd $CURR_DIR
-  tar cf dashboard_frontend.tar $outfile/src/pybind/mgr/dashboard_v2/frontend/dist
+  tar cf dashboard_frontend.tar $outfile/src/pybind/mgr/dashboard/frontend/dist
 }
 
 # clean out old cruft...
diff --git a/qa/suites/rados/mgr/tasks/dashboard.yaml b/qa/suites/rados/mgr/tasks/dashboard.yaml
new file mode 100644 (file)
index 0000000..8570eaf
--- /dev/null
@@ -0,0 +1,34 @@
+
+tasks:
+  - install:
+  - ceph:
+      # tests may leave mgrs broken, so don't try and call into them
+      # to invoke e.g. pg dump during teardown.
+      wait-for-scrub: false
+      log-whitelist:
+        - overall HEALTH_
+        - \(MGR_DOWN\)
+        - \(PG_
+        - replacing it with standby
+        - No standby daemons available
+        - \(FS_DEGRADED\)
+        - \(MDS_FAILED\)
+        - \(MDS_DEGRADED\)
+        - \(FS_WITH_FAILED_MDS\)
+        - \(MDS_DAMAGE\)
+  - rgw: [client.0]
+  - cephfs_test_runner:
+      modules:
+        - tasks.mgr.test_dashboard
+        - tasks.mgr.dashboard.test_auth
+        - tasks.mgr.dashboard.test_cephfs
+        - tasks.mgr.dashboard.test_cluster_configuration
+        - tasks.mgr.dashboard.test_dashboard
+        - tasks.mgr.dashboard.test_host
+        - tasks.mgr.dashboard.test_monitor
+        - tasks.mgr.dashboard.test_osd
+        - tasks.mgr.dashboard.test_perf_counters
+        - tasks.mgr.dashboard.test_summary
+        - tasks.mgr.dashboard.test_rgw
+        - tasks.mgr.dashboard.test_rbd
+        - tasks.mgr.dashboard.test_pool
diff --git a/qa/suites/rados/mgr/tasks/dashboard_v2.yaml b/qa/suites/rados/mgr/tasks/dashboard_v2.yaml
deleted file mode 100644 (file)
index cd5e347..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-
-tasks:
-  - install:
-  - ceph:
-      # tests may leave mgrs broken, so don't try and call into them
-      # to invoke e.g. pg dump during teardown.
-      wait-for-scrub: false
-      log-whitelist:
-        - overall HEALTH_
-        - \(MGR_DOWN\)
-        - \(PG_
-        - replacing it with standby
-        - No standby daemons available
-        - \(FS_DEGRADED\)
-        - \(MDS_FAILED\)
-        - \(MDS_DEGRADED\)
-        - \(FS_WITH_FAILED_MDS\)
-        - \(MDS_DAMAGE\)
-  - rgw: [client.0]
-  - cephfs_test_runner:
-      modules:
-        - tasks.mgr.test_dashboard_v2
-        - tasks.mgr.dashboard_v2.test_auth
-        - tasks.mgr.dashboard_v2.test_cephfs
-        - tasks.mgr.dashboard_v2.test_cluster_configuration
-        - tasks.mgr.dashboard_v2.test_dashboard
-        - tasks.mgr.dashboard_v2.test_host
-        - tasks.mgr.dashboard_v2.test_monitor
-        - tasks.mgr.dashboard_v2.test_osd
-        - tasks.mgr.dashboard_v2.test_perf_counters
-        - tasks.mgr.dashboard_v2.test_summary
-        - tasks.mgr.dashboard_v2.test_rgw
-        - tasks.mgr.dashboard_v2.test_rbd
-        - tasks.mgr.dashboard_v2.test_pool
diff --git a/qa/tasks/mgr/dashboard/__init__.py b/qa/tasks/mgr/dashboard/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/qa/tasks/mgr/dashboard/helper.py b/qa/tasks/mgr/dashboard/helper.py
new file mode 100644 (file)
index 0000000..3e0c836
--- /dev/null
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=W0212
+from __future__ import absolute_import
+
+import json
+import logging
+import os
+import subprocess
+import sys
+
+import requests
+from ..mgr_test_case import MgrTestCase
+
+
+log = logging.getLogger(__name__)
+
+
+def authenticate(func):
+    def decorate(self, *args, **kwargs):
+        self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin', 'admin'])
+        self._post('/api/auth', {'username': 'admin', 'password': 'admin'})
+        self.assertStatus(201)
+        return func(self, *args, **kwargs)
+    return decorate
+
+
+class DashboardTestCase(MgrTestCase):
+    MGRS_REQUIRED = 2
+    MDSS_REQUIRED = 1
+    REQUIRE_FILESYSTEM = True
+    CLIENTS_REQUIRED = 1
+    CEPHFS = False
+
+    @classmethod
+    def setUpClass(cls):
+        super(DashboardTestCase, cls).setUpClass()
+        cls._assign_ports("dashboard", "server_port")
+        cls._load_module("dashboard")
+        cls.base_uri = cls._get_uri("dashboard").rstrip('/')
+
+        if cls.CEPHFS:
+            cls.mds_cluster.clear_firewall()
+
+            # To avoid any issues with e.g. unlink bugs, we destroy and recreate
+            # the filesystem rather than just doing a rm -rf of files
+            cls.mds_cluster.mds_stop()
+            cls.mds_cluster.mds_fail()
+            cls.mds_cluster.delete_all_filesystems()
+            cls.fs = None  # is now invalid!
+
+            cls.fs = cls.mds_cluster.newfs(create=True)
+            cls.fs.mds_restart()
+
+            # In case some test messed with auth caps, reset them
+            # pylint: disable=not-an-iterable
+            client_mount_ids = [m.client_id for m in cls.mounts]
+            for client_id in client_mount_ids:
+                cls.mds_cluster.mon_manager.raw_cluster_cmd_result(
+                    'auth', 'caps', "client.{0}".format(client_id),
+                    'mds', 'allow',
+                    'mon', 'allow r',
+                    'osd', 'allow rw pool={0}'.format(cls.fs.get_data_pool_name()))
+
+            # wait for mds restart to complete...
+            cls.fs.wait_for_daemons()
+
+    @classmethod
+    def tearDownClass(cls):
+        super(DashboardTestCase, cls).tearDownClass()
+
+    def __init__(self, *args, **kwargs):
+        super(DashboardTestCase, self).__init__(*args, **kwargs)
+        self._session = requests.Session()
+        self._resp = None
+
+    def _request(self, url, method, data=None):
+        url = "{}{}".format(self.base_uri, url)
+        log.info("request %s to %s", method, url)
+        if method == 'GET':
+            self._resp = self._session.get(url)
+            return self._resp.json()
+        elif method == 'POST':
+            self._resp = self._session.post(url, json=data)
+        elif method == 'DELETE':
+            self._resp = self._session.delete(url, json=data)
+        elif method == 'PUT':
+            self._resp = self._session.put(url, json=data)
+        return None
+
+    def _get(self, url):
+        return self._request(url, 'GET')
+
+    def _post(self, url, data=None):
+        self._request(url, 'POST', data)
+
+    def _delete(self, url, data=None):
+        self._request(url, 'DELETE', data)
+
+    def _put(self, url, data=None):
+        self._request(url, 'PUT', data)
+
+    def cookies(self):
+        return self._resp.cookies
+
+    def jsonBody(self):
+        return self._resp.json()
+
+    def reset_session(self):
+        self._session = requests.Session()
+
+    def assertJsonBody(self, data):
+        body = self._resp.json()
+        self.assertEqual(body, data)
+
+    def assertBody(self, body):
+        self.assertEqual(self._resp.text, body)
+
+    def assertStatus(self, status):
+        self.assertEqual(self._resp.status_code, status)
+
+    @classmethod
+    def _ceph_cmd(cls, cmd):
+        res = cls.mgr_cluster.mon_manager.raw_cluster_cmd(*cmd)
+        log.info("command result: %s", res)
+        return res
+
+    def set_config_key(self, key, value):
+        self._ceph_cmd(['config-key', 'set', key, value])
+
+    def get_config_key(self, key):
+        return self._ceph_cmd(['config-key', 'get', key])
+
+    @classmethod
+    def _rbd_cmd(cls, cmd):
+        args = [
+            'rbd'
+        ]
+        args.extend(cmd)
+        cls.mgr_cluster.admin_remote.run(args=args)
+
+    @classmethod
+    def mons(cls):
+        out = cls.ceph_cluster.mon_manager.raw_cluster_cmd('mon_status')
+        j = json.loads(out)
+        return [mon['name'] for mon in j['monmap']['mons']]
diff --git a/qa/tasks/mgr/dashboard/test_auth.py b/qa/tasks/mgr/dashboard/test_auth.py
new file mode 100644 (file)
index 0000000..b176dd0
--- /dev/null
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+
+import time
+
+from .helper import DashboardTestCase
+
+
+class AuthTest(DashboardTestCase):
+    def setUp(self):
+        self.reset_session()
+        self._ceph_cmd(['dashboard', 'set-session-expire', '2'])
+        self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin', 'admin'])
+
+    def test_a_set_login_credentials(self):
+        self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin2', 'admin2'])
+        self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'})
+        self.assertStatus(201)
+        self.assertJsonBody({"username": "admin2"})
+
+    def test_login_valid(self):
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
+        self.assertStatus(201)
+        self.assertJsonBody({"username": "admin"})
+
+    def test_login_stay_signed_in(self):
+        self._post("/api/auth", {
+            'username': 'admin',
+            'password': 'admin',
+            'stay_signed_in': True})
+        self.assertStatus(201)
+        self.assertIn('session_id', self.cookies())
+        for cookie in self.cookies():
+            if cookie.name == 'session_id':
+                self.assertIsNotNone(cookie.expires)
+
+    def test_login_not_stay_signed_in(self):
+        self._post("/api/auth", {
+            'username': 'admin',
+            'password': 'admin',
+            'stay_signed_in': False})
+        self.assertStatus(201)
+        self.assertIn('session_id', self.cookies())
+        for cookie in self.cookies():
+            if cookie.name == 'session_id':
+                self.assertIsNone(cookie.expires)
+
+    def test_login_invalid(self):
+        self._post("/api/auth", {'username': 'admin', 'password': 'inval'})
+        self.assertStatus(403)
+        self.assertJsonBody({"detail": "Invalid credentials"})
+
+    def test_logout(self):
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
+        self._delete("/api/auth")
+        self.assertStatus(204)
+        self.assertBody('')
+        self._get("/api/host")
+        self.assertStatus(401)
+
+    def test_session_expire(self):
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
+        self.assertStatus(201)
+        self._get("/api/host")
+        self.assertStatus(200)
+        time.sleep(3)
+        self._get("/api/host")
+        self.assertStatus(401)
+
+    def test_unauthorized(self):
+        self._get("/api/host")
+        self.assertStatus(401)
+        self._get("/api")
+        self.assertStatus(401)
diff --git a/qa/tasks/mgr/dashboard/test_cephfs.py b/qa/tasks/mgr/dashboard/test_cephfs.py
new file mode 100644 (file)
index 0000000..5669e41
--- /dev/null
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class CephfsTest(DashboardTestCase):
+    CEPHFS = True
+
+    @authenticate
+    def test_cephfs_clients(self):
+        fs_id = self.fs.get_namespace_id()
+        data = self._get("/api/cephfs/clients/{}".format(fs_id))
+        self.assertStatus(200)
+
+        self.assertIn('status', data)
+        self.assertIn('data', data)
+
+    @authenticate
+    def test_cephfs_data(self):
+        fs_id = self.fs.get_namespace_id()
+        data = self._get("/api/cephfs/data/{}/".format(fs_id))
+        self.assertStatus(200)
+
+        self.assertIn('cephfs', data)
+        self.assertIn('standbys', data)
+        self.assertIn('versions', data)
+        self.assertIsNotNone(data['cephfs'])
+        self.assertIsNotNone(data['standbys'])
+        self.assertIsNotNone(data['versions'])
+
+    @authenticate
+    def test_cephfs_mds_counters(self):
+        fs_id = self.fs.get_namespace_id()
+        data = self._get("/api/cephfs/mds_counters/{}".format(fs_id))
+        self.assertStatus(200)
+
+        self.assertIsInstance(data, dict)
+        self.assertIsNotNone(data)
diff --git a/qa/tasks/mgr/dashboard/test_cluster_configuration.py b/qa/tasks/mgr/dashboard/test_cluster_configuration.py
new file mode 100644 (file)
index 0000000..6cfe605
--- /dev/null
@@ -0,0 +1,39 @@
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class ClusterConfigurationTest(DashboardTestCase):
+    @authenticate
+    def test_list(self):
+        data = self._get('/api/cluster_conf')
+        self.assertStatus(200)
+        self.assertIsInstance(data, list)
+        self.assertGreater(len(data), 1000)
+        for conf in data:
+            self._validate_single(conf)
+
+    @authenticate
+    def test_get(self):
+        data = self._get('/api/cluster_conf/admin_socket')
+        self.assertStatus(200)
+        self._validate_single(data)
+        self.assertIn('enum_values', data)
+
+        data = self._get('/api/cluster_conf/fantasy_name')
+        self.assertStatus(404)
+
+    def _validate_single(self, data):
+        self.assertIn('name', data)
+        self.assertIn('daemon_default', data)
+        self.assertIn('long_desc', data)
+        self.assertIn('level', data)
+        self.assertIn('default', data)
+        self.assertIn('see_also', data)
+        self.assertIn('tags', data)
+        self.assertIn('min', data)
+        self.assertIn('max', data)
+        self.assertIn('services', data)
+        self.assertIn('type', data)
+        self.assertIn('desc', data)
+
diff --git a/qa/tasks/mgr/dashboard/test_dashboard.py b/qa/tasks/mgr/dashboard/test_dashboard.py
new file mode 100644 (file)
index 0000000..fa70017
--- /dev/null
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class DashboardTest(DashboardTestCase):
+    CEPHFS = True
+
+    @authenticate
+    def test_health(self):
+        data = self._get("/api/dashboard/health")
+        self.assertStatus(200)
+
+        self.assertIn('health', data)
+        self.assertIn('mon_status', data)
+        self.assertIn('fs_map', data)
+        self.assertIn('osd_map', data)
+        self.assertIn('clog', data)
+        self.assertIn('audit_log', data)
+        self.assertIn('pools', data)
+        self.assertIn('mgr_map', data)
+        self.assertIn('df', data)
+        self.assertIsNotNone(data['health'])
+        self.assertIsNotNone(data['mon_status'])
+        self.assertIsNotNone(data['fs_map'])
+        self.assertIsNotNone(data['osd_map'])
+        self.assertIsNotNone(data['clog'])
+        self.assertIsNotNone(data['audit_log'])
+        self.assertIsNotNone(data['pools'])
+
+        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+        self.assertEqual(len(cluster_pools), len(data['pools']))
+        for pool in data['pools']:
+            self.assertIn(pool['pool_name'], cluster_pools)
+
+        self.assertIsNotNone(data['mgr_map'])
+        self.assertIsNotNone(data['df'])
diff --git a/qa/tasks/mgr/dashboard/test_host.py b/qa/tasks/mgr/dashboard/test_host.py
new file mode 100644 (file)
index 0000000..efa28d9
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class HostControllerTest(DashboardTestCase):
+
+    @authenticate
+    def test_host_list(self):
+        data = self._get('/api/host')
+        self.assertStatus(200)
+
+        for server in data:
+            self.assertIn('services', server)
+            self.assertIn('hostname', server)
+            self.assertIn('ceph_version', server)
+            self.assertIsNotNone(server['hostname'])
+            self.assertIsNotNone(server['ceph_version'])
+            self.assertGreaterEqual(len(server['services']), 1)
+            for service in server['services']:
+                self.assertIn('type', service)
+                self.assertIn('id', service)
+                self.assertIsNotNone(service['type'])
+                self.assertIsNotNone(service['id'])
diff --git a/qa/tasks/mgr/dashboard/test_monitor.py b/qa/tasks/mgr/dashboard/test_monitor.py
new file mode 100644 (file)
index 0000000..0b199cd
--- /dev/null
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class MonitorTest(DashboardTestCase):
+    @authenticate
+    def test_monitor_default(self):
+        data = self._get("/api/monitor")
+        self.assertStatus(200)
+
+        self.assertIn('mon_status', data)
+        self.assertIn('in_quorum', data)
+        self.assertIn('out_quorum', data)
+        self.assertIsNotNone(data['mon_status'])
+        self.assertIsNotNone(data['in_quorum'])
+        self.assertIsNotNone(data['out_quorum'])
diff --git a/qa/tasks/mgr/dashboard/test_osd.py b/qa/tasks/mgr/dashboard/test_osd.py
new file mode 100644 (file)
index 0000000..587a448
--- /dev/null
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class OsdTest(DashboardTestCase):
+
+    def assert_in_and_not_none(self, data, properties):
+        for prop in properties:
+            self.assertIn(prop, data)
+            self.assertIsNotNone(data[prop])
+
+    @authenticate
+    def test_list(self):
+        data = self._get('/api/osd')
+        self.assertStatus(200)
+
+        self.assertGreaterEqual(len(data), 1)
+        data = data[0]
+        self.assert_in_and_not_none(data, ['host', 'tree', 'state', 'stats', 'stats_history'])
+        self.assert_in_and_not_none(data['host'], ['name'])
+        self.assert_in_and_not_none(data['tree'], ['id'])
+        self.assert_in_and_not_none(data['stats'], ['numpg', 'stat_bytes_used', 'stat_bytes',
+                                                    'op_r', 'op_w'])
+        self.assert_in_and_not_none(data['stats_history'], ['op_out_bytes', 'op_in_bytes'])
+
+    @authenticate
+    def test_details(self):
+        data = self._get('/api/osd/0')
+        self.assertStatus(200)
+        self.assert_in_and_not_none(data, ['osd_metadata', 'histogram'])
+        self.assert_in_and_not_none(data['histogram'], ['osd'])
+        self.assert_in_and_not_none(data['histogram']['osd'], ['op_w_latency_in_bytes_histogram',
+                                                               'op_r_latency_out_bytes_histogram'])
diff --git a/qa/tasks/mgr/dashboard/test_perf_counters.py b/qa/tasks/mgr/dashboard/test_perf_counters.py
new file mode 100644 (file)
index 0000000..4da9236
--- /dev/null
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class PerfCountersControllerTest(DashboardTestCase):
+
+    @authenticate
+    def test_perf_counters_list(self):
+        data = self._get('/api/perf_counters')
+        self.assertStatus(200)
+
+        self.assertIsInstance(data, dict)
+        for mon in self.mons():
+            self.assertIn('mon.{}'.format(mon), data)
+
+        osds = self.ceph_cluster.mon_manager.get_osd_dump()
+        for osd in osds:
+            self.assertIn('osd.{}'.format(osd['osd']), data)
+
+    @authenticate
+    def test_perf_counters_mon_get(self):
+        mon = self.mons()[0]
+        data = self._get('/api/perf_counters/mon/{}'.format(mon))
+        self.assertStatus(200)
+
+        self.assertIsInstance(data, dict)
+        self.assertEqual('mon', data['service']['type'])
+        self.assertEqual(mon, data['service']['id'])
+        self.assertIsInstance(data['counters'], list)
+        self.assertGreater(len(data['counters']), 0)
+        counter = data['counters'][0]
+        self.assertIsInstance(counter, dict)
+        self.assertIn('description', counter)
+        self.assertIn('name', counter)
+        self.assertIn('unit', counter)
+        self.assertIn('value', counter)
+
+    @authenticate
+    def test_perf_counters_mgr_get(self):
+        mgr = self.mgr_cluster.mgr_ids[0]
+        data = self._get('/api/perf_counters/mgr/{}'.format(mgr))
+        self.assertStatus(200)
+
+        self.assertIsInstance(data, dict)
+        self.assertEqual('mgr', data['service']['type'])
+        self.assertEqual(mgr, data['service']['id'])
+        self.assertIsInstance(data['counters'], list)
+        self.assertGreater(len(data['counters']), 0)
+        counter = data['counters'][0]
+        self.assertIsInstance(counter, dict)
+        self.assertIn('description', counter)
+        self.assertIn('name', counter)
+        self.assertIn('unit', counter)
+        self.assertIn('value', counter)
diff --git a/qa/tasks/mgr/dashboard/test_pool.py b/qa/tasks/mgr/dashboard/test_pool.py
new file mode 100644 (file)
index 0000000..6852ddb
--- /dev/null
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class DashboardTest(DashboardTestCase):
+    @authenticate
+    def test_pool_list(self):
+        data = self._get("/api/pool")
+        self.assertStatus(200)
+
+        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+        self.assertEqual(len(cluster_pools), len(data))
+        for pool in data:
+            self.assertIn('pool_name', pool)
+            self.assertIn('type', pool)
+            self.assertIn('flags', pool)
+            self.assertIn('flags_names', pool)
+            self.assertNotIn('stats', pool)
+            self.assertIn(pool['pool_name'], cluster_pools)
+
+    @authenticate
+    def test_pool_list_attrs(self):
+        data = self._get("/api/pool?attrs=type,flags")
+        self.assertStatus(200)
+
+        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+        self.assertEqual(len(cluster_pools), len(data))
+        for pool in data:
+            self.assertIn('pool_name', pool)
+            self.assertIn('type', pool)
+            self.assertIn('flags', pool)
+            self.assertNotIn('flags_names', pool)
+            self.assertNotIn('stats', pool)
+            self.assertIn(pool['pool_name'], cluster_pools)
+
+    @authenticate
+    def test_pool_list_stats(self):
+        data = self._get("/api/pool?stats=true")
+        self.assertStatus(200)
+
+        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+        self.assertEqual(len(cluster_pools), len(data))
+        for pool in data:
+            self.assertIn('pool_name', pool)
+            self.assertIn('type', pool)
+            self.assertIn('flags', pool)
+            self.assertIn('stats', pool)
+            self.assertIn('flags_names', pool)
+            self.assertIn(pool['pool_name'], cluster_pools)
+
+    @authenticate
+    def test_pool_get(self):
+        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+        pool = self._get("/api/pool/{}?stats=true&attrs=type,flags,stats"
+                         .format(cluster_pools[0]))
+        self.assertEqual(pool['pool_name'], cluster_pools[0])
+        self.assertIn('type', pool)
+        self.assertIn('flags', pool)
+        self.assertIn('stats', pool)
+        self.assertNotIn('flags_names', pool)
diff --git a/qa/tasks/mgr/dashboard/test_rbd.py b/qa/tasks/mgr/dashboard/test_rbd.py
new file mode 100644 (file)
index 0000000..0835fc2
--- /dev/null
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+
+import unittest
+
+from .helper import DashboardTestCase, authenticate
+
+
+class RbdTest(DashboardTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(RbdTest, cls).setUpClass()
+        cls._ceph_cmd(['osd', 'pool', 'create', 'rbd', '100', '100'])
+        cls._ceph_cmd(['osd', 'pool', 'application', 'enable', 'rbd', 'rbd'])
+        cls._rbd_cmd(['create', '--size=1G', 'img1'])
+        cls._rbd_cmd(['create', '--size=2G', 'img2'])
+
+    @classmethod
+    def tearDownClass(cls):
+        super(RbdTest, cls).tearDownClass()
+        cls._ceph_cmd(['osd', 'pool', 'delete', 'rbd', 'rbd', '--yes-i-really-really-mean-it'])
+
+    @authenticate
+    def test_list(self):
+        data = self._get('/api/rbd/rbd')
+        self.assertStatus(200)
+
+        img1 = data['value'][0]
+        self.assertEqual(img1['name'], 'img1')
+        self.assertEqual(img1['size'], 1073741824)
+        self.assertEqual(img1['num_objs'], 256)
+        self.assertEqual(img1['obj_size'], 4194304)
+        self.assertEqual(img1['features_name'],
+                         'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
+
+        img2 = data['value'][1]
+        self.assertEqual(img2['name'], 'img2')
+        self.assertEqual(img2['size'], 2147483648)
+        self.assertEqual(img2['num_objs'], 512)
+        self.assertEqual(img2['obj_size'], 4194304)
+        self.assertEqual(img2['features_name'],
+                         'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
+
+    @authenticate
+    def test_create(self):
+        rbd_name = 'test_rbd'
+        data = {'pool_name': 'rbd',
+                'name': rbd_name,
+                'size': 10240}
+        self._post('/api/rbd', data)
+        self.assertStatus(201)
+        self.assertJsonBody({"success": True})
+
+        # TODO: change to GET the specific RBD instead of the list as soon as it is available?
+        get_res = self._get('/api/rbd/rbd')
+        self.assertStatus(200)
+
+        for rbd in get_res['value']:
+            if rbd['name'] == rbd_name:
+                self.assertEqual(rbd['size'], 10240)
+                self.assertEqual(rbd['num_objs'], 1)
+                self.assertEqual(rbd['obj_size'], 4194304)
+                self.assertEqual(rbd['features_name'],
+                                 'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
+                break
+
+    # TODO: Re-enable this test for bluestore cluster by figuring out how to skip none-bluestore
+    # ones automatically
+    @unittest.skip("requires bluestore cluster")
+    @authenticate
+    def test_create_rbd_in_data_pool(self):
+        self._ceph_cmd(['osd', 'pool', 'create', 'data_pool', '12', '12', 'erasure'])
+        self._ceph_cmd(['osd', 'pool', 'application', 'enable', 'data_pool', 'rbd'])
+        self._ceph_cmd(['osd', 'pool', 'set', 'data_pool', 'allow_ec_overwrites', 'true'])
+
+        rbd_name = 'test_rbd_in_data_pool'
+        data = {'pool_name': 'rbd',
+                'name': rbd_name,
+                'size': 10240,
+                'data_pool': 'data_pool'}
+        self._post('/api/rbd', data)
+        self.assertStatus(201)
+        self.assertJsonBody({"success": True})
+
+        # TODO: possibly change to GET the specific RBD (see above)
+        get_res = self._get('/api/rbd/rbd')
+        self.assertStatus(200)
+
+        for rbd in get_res['value']:
+            if rbd['name'] == rbd_name:
+                self.assertEqual(rbd['size'], 10240)
+                self.assertEqual(rbd['num_objs'], 1)
+                self.assertEqual(rbd['obj_size'], 4194304)
+                self.assertEqual(rbd['features_name'], 'data-pool, deep-flatten, exclusive-lock, '
+                                                       'fast-diff, layering, object-map')
+                break
+
+        self._ceph_cmd(['osd', 'pool', 'delete', 'data_pool', 'data_pool',
+                       '--yes-i-really-really-mean-it'])
+
+    @authenticate
+    def test_create_rbd_twice(self):
+        data = {'pool_name': 'rbd',
+                'name': 'test_rbd_twice',
+                'size': 10240}
+        self._post('/api/rbd', data)
+
+        self._post('/api/rbd', data)
+        self.assertStatus(400)
+        self.assertJsonBody({"success": False, "errno": 17,
+                             "detail": "[errno 17] error creating image"})
diff --git a/qa/tasks/mgr/dashboard/test_rgw.py b/qa/tasks/mgr/dashboard/test_rgw.py
new file mode 100644 (file)
index 0000000..f6cbf84
--- /dev/null
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class RgwControllerTest(DashboardTestCase):
+
+    @authenticate
+    def test_rgw_daemon_list(self):
+        data = self._get('/api/rgw/daemon')
+        self.assertStatus(200)
+
+        self.assertEqual(len(data), 1)
+        data = data[0]
+        self.assertIn('id', data)
+        self.assertIn('version', data)
+        self.assertIn('server_hostname', data)
+
+    @authenticate
+    def test_rgw_daemon_get(self):
+        data = self._get('/api/rgw/daemon')
+        self.assertStatus(200)
+        data = self._get('/api/rgw/daemon/{}'.format(data[0]['id']))
+        self.assertStatus(200)
+
+        self.assertIn('rgw_metadata', data)
+        self.assertIn('rgw_id', data)
+        self.assertIn('rgw_status', data)
+        self.assertTrue(data['rgw_metadata'])
diff --git a/qa/tasks/mgr/dashboard/test_summary.py b/qa/tasks/mgr/dashboard/test_summary.py
new file mode 100644 (file)
index 0000000..df7cbaf
--- /dev/null
@@ -0,0 +1,25 @@
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class SummaryTest(DashboardTestCase):
+    CEPHFS = True
+
+    @authenticate
+    def test_summary(self):
+        data = self._get("/api/summary")
+        self.assertStatus(200)
+
+        self.assertIn('filesystems', data)
+        self.assertIn('health_status', data)
+        self.assertIn('rbd_pools', data)
+        self.assertIn('mgr_id', data)
+        self.assertIn('have_mon_connection', data)
+        self.assertIn('rbd_mirroring', data)
+        self.assertIsNotNone(data['filesystems'])
+        self.assertIsNotNone(data['health_status'])
+        self.assertIsNotNone(data['rbd_pools'])
+        self.assertIsNotNone(data['mgr_id'])
+        self.assertIsNotNone(data['have_mon_connection'])
+        self.assertEqual(data['rbd_mirroring'], {'errors': 0, 'warnings': 0})
diff --git a/qa/tasks/mgr/dashboard_v2/__init__.py b/qa/tasks/mgr/dashboard_v2/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/qa/tasks/mgr/dashboard_v2/helper.py b/qa/tasks/mgr/dashboard_v2/helper.py
deleted file mode 100644 (file)
index f43970f..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-# -*- coding: utf-8 -*-
-# pylint: disable=W0212
-from __future__ import absolute_import
-
-import json
-import logging
-import os
-import subprocess
-import sys
-
-import requests
-from ..mgr_test_case import MgrTestCase
-
-
-log = logging.getLogger(__name__)
-
-
-def authenticate(func):
-    def decorate(self, *args, **kwargs):
-        self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin', 'admin'])
-        self._post('/api/auth', {'username': 'admin', 'password': 'admin'})
-        self.assertStatus(201)
-        return func(self, *args, **kwargs)
-    return decorate
-
-
-class DashboardTestCase(MgrTestCase):
-    MGRS_REQUIRED = 2
-    MDSS_REQUIRED = 1
-    REQUIRE_FILESYSTEM = True
-    CLIENTS_REQUIRED = 1
-    CEPHFS = False
-
-    @classmethod
-    def setUpClass(cls):
-        super(DashboardTestCase, cls).setUpClass()
-        cls._assign_ports("dashboard_v2", "server_port")
-        cls._load_module("dashboard_v2")
-        cls.base_uri = cls._get_uri("dashboard_v2").rstrip('/')
-
-        if cls.CEPHFS:
-            cls.mds_cluster.clear_firewall()
-
-            # To avoid any issues with e.g. unlink bugs, we destroy and recreate
-            # the filesystem rather than just doing a rm -rf of files
-            cls.mds_cluster.mds_stop()
-            cls.mds_cluster.mds_fail()
-            cls.mds_cluster.delete_all_filesystems()
-            cls.fs = None  # is now invalid!
-
-            cls.fs = cls.mds_cluster.newfs(create=True)
-            cls.fs.mds_restart()
-
-            # In case some test messed with auth caps, reset them
-            # pylint: disable=not-an-iterable
-            client_mount_ids = [m.client_id for m in cls.mounts]
-            for client_id in client_mount_ids:
-                cls.mds_cluster.mon_manager.raw_cluster_cmd_result(
-                    'auth', 'caps', "client.{0}".format(client_id),
-                    'mds', 'allow',
-                    'mon', 'allow r',
-                    'osd', 'allow rw pool={0}'.format(cls.fs.get_data_pool_name()))
-
-            # wait for mds restart to complete...
-            cls.fs.wait_for_daemons()
-
-    @classmethod
-    def tearDownClass(cls):
-        super(DashboardTestCase, cls).tearDownClass()
-
-    def __init__(self, *args, **kwargs):
-        super(DashboardTestCase, self).__init__(*args, **kwargs)
-        self._session = requests.Session()
-        self._resp = None
-
-    def _request(self, url, method, data=None):
-        url = "{}{}".format(self.base_uri, url)
-        log.info("request %s to %s", method, url)
-        if method == 'GET':
-            self._resp = self._session.get(url)
-            return self._resp.json()
-        elif method == 'POST':
-            self._resp = self._session.post(url, json=data)
-        elif method == 'DELETE':
-            self._resp = self._session.delete(url, json=data)
-        elif method == 'PUT':
-            self._resp = self._session.put(url, json=data)
-        return None
-
-    def _get(self, url):
-        return self._request(url, 'GET')
-
-    def _post(self, url, data=None):
-        self._request(url, 'POST', data)
-
-    def _delete(self, url, data=None):
-        self._request(url, 'DELETE', data)
-
-    def _put(self, url, data=None):
-        self._request(url, 'PUT', data)
-
-    def cookies(self):
-        return self._resp.cookies
-
-    def jsonBody(self):
-        return self._resp.json()
-
-    def reset_session(self):
-        self._session = requests.Session()
-
-    def assertJsonBody(self, data):
-        body = self._resp.json()
-        self.assertEqual(body, data)
-
-    def assertBody(self, body):
-        self.assertEqual(self._resp.text, body)
-
-    def assertStatus(self, status):
-        self.assertEqual(self._resp.status_code, status)
-
-    @classmethod
-    def _ceph_cmd(cls, cmd):
-        res = cls.mgr_cluster.mon_manager.raw_cluster_cmd(*cmd)
-        log.info("command result: %s", res)
-        return res
-
-    def set_config_key(self, key, value):
-        self._ceph_cmd(['config-key', 'set', key, value])
-
-    def get_config_key(self, key):
-        return self._ceph_cmd(['config-key', 'get', key])
-
-    @classmethod
-    def _rbd_cmd(cls, cmd):
-        args = [
-            'rbd'
-        ]
-        args.extend(cmd)
-        cls.mgr_cluster.admin_remote.run(args=args)
-
-    @classmethod
-    def mons(cls):
-        out = cls.ceph_cluster.mon_manager.raw_cluster_cmd('mon_status')
-        j = json.loads(out)
-        return [mon['name'] for mon in j['monmap']['mons']]
diff --git a/qa/tasks/mgr/dashboard_v2/test_auth.py b/qa/tasks/mgr/dashboard_v2/test_auth.py
deleted file mode 100644 (file)
index b176dd0..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-import time
-
-from .helper import DashboardTestCase
-
-
-class AuthTest(DashboardTestCase):
-    def setUp(self):
-        self.reset_session()
-        self._ceph_cmd(['dashboard', 'set-session-expire', '2'])
-        self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin', 'admin'])
-
-    def test_a_set_login_credentials(self):
-        self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin2', 'admin2'])
-        self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'})
-        self.assertStatus(201)
-        self.assertJsonBody({"username": "admin2"})
-
-    def test_login_valid(self):
-        self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
-        self.assertStatus(201)
-        self.assertJsonBody({"username": "admin"})
-
-    def test_login_stay_signed_in(self):
-        self._post("/api/auth", {
-            'username': 'admin',
-            'password': 'admin',
-            'stay_signed_in': True})
-        self.assertStatus(201)
-        self.assertIn('session_id', self.cookies())
-        for cookie in self.cookies():
-            if cookie.name == 'session_id':
-                self.assertIsNotNone(cookie.expires)
-
-    def test_login_not_stay_signed_in(self):
-        self._post("/api/auth", {
-            'username': 'admin',
-            'password': 'admin',
-            'stay_signed_in': False})
-        self.assertStatus(201)
-        self.assertIn('session_id', self.cookies())
-        for cookie in self.cookies():
-            if cookie.name == 'session_id':
-                self.assertIsNone(cookie.expires)
-
-    def test_login_invalid(self):
-        self._post("/api/auth", {'username': 'admin', 'password': 'inval'})
-        self.assertStatus(403)
-        self.assertJsonBody({"detail": "Invalid credentials"})
-
-    def test_logout(self):
-        self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
-        self._delete("/api/auth")
-        self.assertStatus(204)
-        self.assertBody('')
-        self._get("/api/host")
-        self.assertStatus(401)
-
-    def test_session_expire(self):
-        self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
-        self.assertStatus(201)
-        self._get("/api/host")
-        self.assertStatus(200)
-        time.sleep(3)
-        self._get("/api/host")
-        self.assertStatus(401)
-
-    def test_unauthorized(self):
-        self._get("/api/host")
-        self.assertStatus(401)
-        self._get("/api")
-        self.assertStatus(401)
diff --git a/qa/tasks/mgr/dashboard_v2/test_cephfs.py b/qa/tasks/mgr/dashboard_v2/test_cephfs.py
deleted file mode 100644 (file)
index 5669e41..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class CephfsTest(DashboardTestCase):
-    CEPHFS = True
-
-    @authenticate
-    def test_cephfs_clients(self):
-        fs_id = self.fs.get_namespace_id()
-        data = self._get("/api/cephfs/clients/{}".format(fs_id))
-        self.assertStatus(200)
-
-        self.assertIn('status', data)
-        self.assertIn('data', data)
-
-    @authenticate
-    def test_cephfs_data(self):
-        fs_id = self.fs.get_namespace_id()
-        data = self._get("/api/cephfs/data/{}/".format(fs_id))
-        self.assertStatus(200)
-
-        self.assertIn('cephfs', data)
-        self.assertIn('standbys', data)
-        self.assertIn('versions', data)
-        self.assertIsNotNone(data['cephfs'])
-        self.assertIsNotNone(data['standbys'])
-        self.assertIsNotNone(data['versions'])
-
-    @authenticate
-    def test_cephfs_mds_counters(self):
-        fs_id = self.fs.get_namespace_id()
-        data = self._get("/api/cephfs/mds_counters/{}".format(fs_id))
-        self.assertStatus(200)
-
-        self.assertIsInstance(data, dict)
-        self.assertIsNotNone(data)
diff --git a/qa/tasks/mgr/dashboard_v2/test_cluster_configuration.py b/qa/tasks/mgr/dashboard_v2/test_cluster_configuration.py
deleted file mode 100644 (file)
index 6cfe605..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class ClusterConfigurationTest(DashboardTestCase):
-    @authenticate
-    def test_list(self):
-        data = self._get('/api/cluster_conf')
-        self.assertStatus(200)
-        self.assertIsInstance(data, list)
-        self.assertGreater(len(data), 1000)
-        for conf in data:
-            self._validate_single(conf)
-
-    @authenticate
-    def test_get(self):
-        data = self._get('/api/cluster_conf/admin_socket')
-        self.assertStatus(200)
-        self._validate_single(data)
-        self.assertIn('enum_values', data)
-
-        data = self._get('/api/cluster_conf/fantasy_name')
-        self.assertStatus(404)
-
-    def _validate_single(self, data):
-        self.assertIn('name', data)
-        self.assertIn('daemon_default', data)
-        self.assertIn('long_desc', data)
-        self.assertIn('level', data)
-        self.assertIn('default', data)
-        self.assertIn('see_also', data)
-        self.assertIn('tags', data)
-        self.assertIn('min', data)
-        self.assertIn('max', data)
-        self.assertIn('services', data)
-        self.assertIn('type', data)
-        self.assertIn('desc', data)
-
diff --git a/qa/tasks/mgr/dashboard_v2/test_dashboard.py b/qa/tasks/mgr/dashboard_v2/test_dashboard.py
deleted file mode 100644 (file)
index fa70017..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class DashboardTest(DashboardTestCase):
-    CEPHFS = True
-
-    @authenticate
-    def test_health(self):
-        data = self._get("/api/dashboard/health")
-        self.assertStatus(200)
-
-        self.assertIn('health', data)
-        self.assertIn('mon_status', data)
-        self.assertIn('fs_map', data)
-        self.assertIn('osd_map', data)
-        self.assertIn('clog', data)
-        self.assertIn('audit_log', data)
-        self.assertIn('pools', data)
-        self.assertIn('mgr_map', data)
-        self.assertIn('df', data)
-        self.assertIsNotNone(data['health'])
-        self.assertIsNotNone(data['mon_status'])
-        self.assertIsNotNone(data['fs_map'])
-        self.assertIsNotNone(data['osd_map'])
-        self.assertIsNotNone(data['clog'])
-        self.assertIsNotNone(data['audit_log'])
-        self.assertIsNotNone(data['pools'])
-
-        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
-        self.assertEqual(len(cluster_pools), len(data['pools']))
-        for pool in data['pools']:
-            self.assertIn(pool['pool_name'], cluster_pools)
-
-        self.assertIsNotNone(data['mgr_map'])
-        self.assertIsNotNone(data['df'])
diff --git a/qa/tasks/mgr/dashboard_v2/test_host.py b/qa/tasks/mgr/dashboard_v2/test_host.py
deleted file mode 100644 (file)
index efa28d9..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class HostControllerTest(DashboardTestCase):
-
-    @authenticate
-    def test_host_list(self):
-        data = self._get('/api/host')
-        self.assertStatus(200)
-
-        for server in data:
-            self.assertIn('services', server)
-            self.assertIn('hostname', server)
-            self.assertIn('ceph_version', server)
-            self.assertIsNotNone(server['hostname'])
-            self.assertIsNotNone(server['ceph_version'])
-            self.assertGreaterEqual(len(server['services']), 1)
-            for service in server['services']:
-                self.assertIn('type', service)
-                self.assertIn('id', service)
-                self.assertIsNotNone(service['type'])
-                self.assertIsNotNone(service['id'])
diff --git a/qa/tasks/mgr/dashboard_v2/test_monitor.py b/qa/tasks/mgr/dashboard_v2/test_monitor.py
deleted file mode 100644 (file)
index 0b199cd..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class MonitorTest(DashboardTestCase):
-    @authenticate
-    def test_monitor_default(self):
-        data = self._get("/api/monitor")
-        self.assertStatus(200)
-
-        self.assertIn('mon_status', data)
-        self.assertIn('in_quorum', data)
-        self.assertIn('out_quorum', data)
-        self.assertIsNotNone(data['mon_status'])
-        self.assertIsNotNone(data['in_quorum'])
-        self.assertIsNotNone(data['out_quorum'])
diff --git a/qa/tasks/mgr/dashboard_v2/test_osd.py b/qa/tasks/mgr/dashboard_v2/test_osd.py
deleted file mode 100644 (file)
index 587a448..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class OsdTest(DashboardTestCase):
-
-    def assert_in_and_not_none(self, data, properties):
-        for prop in properties:
-            self.assertIn(prop, data)
-            self.assertIsNotNone(data[prop])
-
-    @authenticate
-    def test_list(self):
-        data = self._get('/api/osd')
-        self.assertStatus(200)
-
-        self.assertGreaterEqual(len(data), 1)
-        data = data[0]
-        self.assert_in_and_not_none(data, ['host', 'tree', 'state', 'stats', 'stats_history'])
-        self.assert_in_and_not_none(data['host'], ['name'])
-        self.assert_in_and_not_none(data['tree'], ['id'])
-        self.assert_in_and_not_none(data['stats'], ['numpg', 'stat_bytes_used', 'stat_bytes',
-                                                    'op_r', 'op_w'])
-        self.assert_in_and_not_none(data['stats_history'], ['op_out_bytes', 'op_in_bytes'])
-
-    @authenticate
-    def test_details(self):
-        data = self._get('/api/osd/0')
-        self.assertStatus(200)
-        self.assert_in_and_not_none(data, ['osd_metadata', 'histogram'])
-        self.assert_in_and_not_none(data['histogram'], ['osd'])
-        self.assert_in_and_not_none(data['histogram']['osd'], ['op_w_latency_in_bytes_histogram',
-                                                               'op_r_latency_out_bytes_histogram'])
diff --git a/qa/tasks/mgr/dashboard_v2/test_perf_counters.py b/qa/tasks/mgr/dashboard_v2/test_perf_counters.py
deleted file mode 100644 (file)
index 4da9236..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class PerfCountersControllerTest(DashboardTestCase):
-
-    @authenticate
-    def test_perf_counters_list(self):
-        data = self._get('/api/perf_counters')
-        self.assertStatus(200)
-
-        self.assertIsInstance(data, dict)
-        for mon in self.mons():
-            self.assertIn('mon.{}'.format(mon), data)
-
-        osds = self.ceph_cluster.mon_manager.get_osd_dump()
-        for osd in osds:
-            self.assertIn('osd.{}'.format(osd['osd']), data)
-
-    @authenticate
-    def test_perf_counters_mon_get(self):
-        mon = self.mons()[0]
-        data = self._get('/api/perf_counters/mon/{}'.format(mon))
-        self.assertStatus(200)
-
-        self.assertIsInstance(data, dict)
-        self.assertEqual('mon', data['service']['type'])
-        self.assertEqual(mon, data['service']['id'])
-        self.assertIsInstance(data['counters'], list)
-        self.assertGreater(len(data['counters']), 0)
-        counter = data['counters'][0]
-        self.assertIsInstance(counter, dict)
-        self.assertIn('description', counter)
-        self.assertIn('name', counter)
-        self.assertIn('unit', counter)
-        self.assertIn('value', counter)
-
-    @authenticate
-    def test_perf_counters_mgr_get(self):
-        mgr = self.mgr_cluster.mgr_ids[0]
-        data = self._get('/api/perf_counters/mgr/{}'.format(mgr))
-        self.assertStatus(200)
-
-        self.assertIsInstance(data, dict)
-        self.assertEqual('mgr', data['service']['type'])
-        self.assertEqual(mgr, data['service']['id'])
-        self.assertIsInstance(data['counters'], list)
-        self.assertGreater(len(data['counters']), 0)
-        counter = data['counters'][0]
-        self.assertIsInstance(counter, dict)
-        self.assertIn('description', counter)
-        self.assertIn('name', counter)
-        self.assertIn('unit', counter)
-        self.assertIn('value', counter)
diff --git a/qa/tasks/mgr/dashboard_v2/test_pool.py b/qa/tasks/mgr/dashboard_v2/test_pool.py
deleted file mode 100644 (file)
index 6852ddb..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class DashboardTest(DashboardTestCase):
-    @authenticate
-    def test_pool_list(self):
-        data = self._get("/api/pool")
-        self.assertStatus(200)
-
-        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
-        self.assertEqual(len(cluster_pools), len(data))
-        for pool in data:
-            self.assertIn('pool_name', pool)
-            self.assertIn('type', pool)
-            self.assertIn('flags', pool)
-            self.assertIn('flags_names', pool)
-            self.assertNotIn('stats', pool)
-            self.assertIn(pool['pool_name'], cluster_pools)
-
-    @authenticate
-    def test_pool_list_attrs(self):
-        data = self._get("/api/pool?attrs=type,flags")
-        self.assertStatus(200)
-
-        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
-        self.assertEqual(len(cluster_pools), len(data))
-        for pool in data:
-            self.assertIn('pool_name', pool)
-            self.assertIn('type', pool)
-            self.assertIn('flags', pool)
-            self.assertNotIn('flags_names', pool)
-            self.assertNotIn('stats', pool)
-            self.assertIn(pool['pool_name'], cluster_pools)
-
-    @authenticate
-    def test_pool_list_stats(self):
-        data = self._get("/api/pool?stats=true")
-        self.assertStatus(200)
-
-        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
-        self.assertEqual(len(cluster_pools), len(data))
-        for pool in data:
-            self.assertIn('pool_name', pool)
-            self.assertIn('type', pool)
-            self.assertIn('flags', pool)
-            self.assertIn('stats', pool)
-            self.assertIn('flags_names', pool)
-            self.assertIn(pool['pool_name'], cluster_pools)
-
-    @authenticate
-    def test_pool_get(self):
-        cluster_pools = self.ceph_cluster.mon_manager.list_pools()
-        pool = self._get("/api/pool/{}?stats=true&attrs=type,flags,stats"
-                         .format(cluster_pools[0]))
-        self.assertEqual(pool['pool_name'], cluster_pools[0])
-        self.assertIn('type', pool)
-        self.assertIn('flags', pool)
-        self.assertIn('stats', pool)
-        self.assertNotIn('flags_names', pool)
diff --git a/qa/tasks/mgr/dashboard_v2/test_rbd.py b/qa/tasks/mgr/dashboard_v2/test_rbd.py
deleted file mode 100644 (file)
index 0835fc2..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-import unittest
-
-from .helper import DashboardTestCase, authenticate
-
-
-class RbdTest(DashboardTestCase):
-
-    @classmethod
-    def setUpClass(cls):
-        super(RbdTest, cls).setUpClass()
-        cls._ceph_cmd(['osd', 'pool', 'create', 'rbd', '100', '100'])
-        cls._ceph_cmd(['osd', 'pool', 'application', 'enable', 'rbd', 'rbd'])
-        cls._rbd_cmd(['create', '--size=1G', 'img1'])
-        cls._rbd_cmd(['create', '--size=2G', 'img2'])
-
-    @classmethod
-    def tearDownClass(cls):
-        super(RbdTest, cls).tearDownClass()
-        cls._ceph_cmd(['osd', 'pool', 'delete', 'rbd', 'rbd', '--yes-i-really-really-mean-it'])
-
-    @authenticate
-    def test_list(self):
-        data = self._get('/api/rbd/rbd')
-        self.assertStatus(200)
-
-        img1 = data['value'][0]
-        self.assertEqual(img1['name'], 'img1')
-        self.assertEqual(img1['size'], 1073741824)
-        self.assertEqual(img1['num_objs'], 256)
-        self.assertEqual(img1['obj_size'], 4194304)
-        self.assertEqual(img1['features_name'],
-                         'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
-
-        img2 = data['value'][1]
-        self.assertEqual(img2['name'], 'img2')
-        self.assertEqual(img2['size'], 2147483648)
-        self.assertEqual(img2['num_objs'], 512)
-        self.assertEqual(img2['obj_size'], 4194304)
-        self.assertEqual(img2['features_name'],
-                         'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
-
-    @authenticate
-    def test_create(self):
-        rbd_name = 'test_rbd'
-        data = {'pool_name': 'rbd',
-                'name': rbd_name,
-                'size': 10240}
-        self._post('/api/rbd', data)
-        self.assertStatus(201)
-        self.assertJsonBody({"success": True})
-
-        # TODO: change to GET the specific RBD instead of the list as soon as it is available?
-        get_res = self._get('/api/rbd/rbd')
-        self.assertStatus(200)
-
-        for rbd in get_res['value']:
-            if rbd['name'] == rbd_name:
-                self.assertEqual(rbd['size'], 10240)
-                self.assertEqual(rbd['num_objs'], 1)
-                self.assertEqual(rbd['obj_size'], 4194304)
-                self.assertEqual(rbd['features_name'],
-                                 'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
-                break
-
-    # TODO: Re-enable this test for bluestore cluster by figuring out how to skip none-bluestore
-    # ones automatically
-    @unittest.skip("requires bluestore cluster")
-    @authenticate
-    def test_create_rbd_in_data_pool(self):
-        self._ceph_cmd(['osd', 'pool', 'create', 'data_pool', '12', '12', 'erasure'])
-        self._ceph_cmd(['osd', 'pool', 'application', 'enable', 'data_pool', 'rbd'])
-        self._ceph_cmd(['osd', 'pool', 'set', 'data_pool', 'allow_ec_overwrites', 'true'])
-
-        rbd_name = 'test_rbd_in_data_pool'
-        data = {'pool_name': 'rbd',
-                'name': rbd_name,
-                'size': 10240,
-                'data_pool': 'data_pool'}
-        self._post('/api/rbd', data)
-        self.assertStatus(201)
-        self.assertJsonBody({"success": True})
-
-        # TODO: possibly change to GET the specific RBD (see above)
-        get_res = self._get('/api/rbd/rbd')
-        self.assertStatus(200)
-
-        for rbd in get_res['value']:
-            if rbd['name'] == rbd_name:
-                self.assertEqual(rbd['size'], 10240)
-                self.assertEqual(rbd['num_objs'], 1)
-                self.assertEqual(rbd['obj_size'], 4194304)
-                self.assertEqual(rbd['features_name'], 'data-pool, deep-flatten, exclusive-lock, '
-                                                       'fast-diff, layering, object-map')
-                break
-
-        self._ceph_cmd(['osd', 'pool', 'delete', 'data_pool', 'data_pool',
-                       '--yes-i-really-really-mean-it'])
-
-    @authenticate
-    def test_create_rbd_twice(self):
-        data = {'pool_name': 'rbd',
-                'name': 'test_rbd_twice',
-                'size': 10240}
-        self._post('/api/rbd', data)
-
-        self._post('/api/rbd', data)
-        self.assertStatus(400)
-        self.assertJsonBody({"success": False, "errno": 17,
-                             "detail": "[errno 17] error creating image"})
diff --git a/qa/tasks/mgr/dashboard_v2/test_rgw.py b/qa/tasks/mgr/dashboard_v2/test_rgw.py
deleted file mode 100644 (file)
index f6cbf84..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class RgwControllerTest(DashboardTestCase):
-
-    @authenticate
-    def test_rgw_daemon_list(self):
-        data = self._get('/api/rgw/daemon')
-        self.assertStatus(200)
-
-        self.assertEqual(len(data), 1)
-        data = data[0]
-        self.assertIn('id', data)
-        self.assertIn('version', data)
-        self.assertIn('server_hostname', data)
-
-    @authenticate
-    def test_rgw_daemon_get(self):
-        data = self._get('/api/rgw/daemon')
-        self.assertStatus(200)
-        data = self._get('/api/rgw/daemon/{}'.format(data[0]['id']))
-        self.assertStatus(200)
-
-        self.assertIn('rgw_metadata', data)
-        self.assertIn('rgw_id', data)
-        self.assertIn('rgw_status', data)
-        self.assertTrue(data['rgw_metadata'])
diff --git a/qa/tasks/mgr/dashboard_v2/test_summary.py b/qa/tasks/mgr/dashboard_v2/test_summary.py
deleted file mode 100644 (file)
index df7cbaf..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class SummaryTest(DashboardTestCase):
-    CEPHFS = True
-
-    @authenticate
-    def test_summary(self):
-        data = self._get("/api/summary")
-        self.assertStatus(200)
-
-        self.assertIn('filesystems', data)
-        self.assertIn('health_status', data)
-        self.assertIn('rbd_pools', data)
-        self.assertIn('mgr_id', data)
-        self.assertIn('have_mon_connection', data)
-        self.assertIn('rbd_mirroring', data)
-        self.assertIsNotNone(data['filesystems'])
-        self.assertIsNotNone(data['health_status'])
-        self.assertIsNotNone(data['rbd_pools'])
-        self.assertIsNotNone(data['mgr_id'])
-        self.assertIsNotNone(data['have_mon_connection'])
-        self.assertEqual(data['rbd_mirroring'], {'errors': 0, 'warnings': 0})
diff --git a/qa/tasks/mgr/test_dashboard.py b/qa/tasks/mgr/test_dashboard.py
new file mode 100644 (file)
index 0000000..79aebee
--- /dev/null
@@ -0,0 +1,65 @@
+
+
+from mgr_test_case import MgrTestCase
+
+import logging
+import requests
+
+
+log = logging.getLogger(__name__)
+
+
+class TestDashboard(MgrTestCase):
+    MGRS_REQUIRED = 3
+
+    def test_standby(self):
+        self._assign_ports("dashboard", "server_port")
+        self._load_module("dashboard")
+
+        original_active = self.mgr_cluster.get_active_id()
+
+        original_uri = self._get_uri("dashboard")
+        log.info("Originally running at {0}".format(original_uri))
+
+        self.mgr_cluster.mgr_fail(original_active)
+
+        failed_over_uri = self._get_uri("dashboard")
+        log.info("After failover running at {0}".format(failed_over_uri))
+
+        self.assertNotEqual(original_uri, failed_over_uri)
+
+        # The original active daemon should have come back up as a standby
+        # and be doing redirects to the new active daemon
+        r = requests.get(original_uri, allow_redirects=False)
+        self.assertEqual(r.status_code, 303)
+        self.assertEqual(r.headers['Location'], failed_over_uri)
+
+    def test_urls(self):
+        self._assign_ports("dashboard", "server_port")
+        self._load_module("dashboard")
+
+        base_uri = self._get_uri("dashboard")
+
+        # This is a very simple smoke test to check that the dashboard can
+        # give us a 200 response to requests.  We're not testing that
+        # the content is correct or even renders!
+
+        urls = [
+            "/",
+        ]
+
+        failures = []
+
+        for url in urls:
+            r = requests.get(base_uri + url, allow_redirects=False)
+            if r.status_code >= 300 and r.status_code < 400:
+                log.error("Unexpected redirect to: {0} (from {1})".format(
+                    r.headers['Location'], base_uri))
+            if r.status_code != 200:
+                failures.append(url)
+
+            log.info("{0}: {1} ({2} bytes)".format(
+                url, r.status_code, len(r.content)
+            ))
+
+        self.assertListEqual(failures, [])
diff --git a/qa/tasks/mgr/test_dashboard_v2.py b/qa/tasks/mgr/test_dashboard_v2.py
deleted file mode 100644 (file)
index 750ef91..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-from mgr_test_case import MgrTestCase
-
-import logging
-import requests
-
-
-log = logging.getLogger(__name__)
-
-
-class TestDashboard(MgrTestCase):
-    MGRS_REQUIRED = 3
-
-    def test_standby(self):
-        self._assign_ports("dashboard_v2", "server_port")
-        self._load_module("dashboard_v2")
-
-        original_active = self.mgr_cluster.get_active_id()
-
-        original_uri = self._get_uri("dashboard_v2")
-        log.info("Originally running at {0}".format(original_uri))
-
-        self.mgr_cluster.mgr_fail(original_active)
-
-        failed_over_uri = self._get_uri("dashboard_v2")
-        log.info("After failover running at {0}".format(failed_over_uri))
-
-        self.assertNotEqual(original_uri, failed_over_uri)
-
-        # The original active daemon should have come back up as a standby
-        # and be doing redirects to the new active daemon
-        r = requests.get(original_uri, allow_redirects=False)
-        self.assertEqual(r.status_code, 303)
-        self.assertEqual(r.headers['Location'], failed_over_uri)
-
-    def test_urls(self):
-        self._assign_ports("dashboard_v2", "server_port")
-        self._load_module("dashboard_v2")
-
-        base_uri = self._get_uri("dashboard_v2")
-
-        # This is a very simple smoke test to check that the dashboard can
-        # give us a 200 response to requests.  We're not testing that
-        # the content is correct or even renders!
-
-        urls = [
-            "/",
-        ]
-
-        failures = []
-
-        for url in urls:
-            r = requests.get(base_uri + url, allow_redirects=False)
-            if r.status_code >= 300 and r.status_code < 400:
-                log.error("Unexpected redirect to: {0} (from {1})".format(
-                    r.headers['Location'], base_uri))
-            if r.status_code != 200:
-                failures.append(url)
-
-            log.info("{0}: {1} ({2} bytes)".format(
-                url, r.status_code, len(r.content)
-            ))
-
-        self.assertListEqual(failures, [])
index acd0da46c18a2382251743fb6149b1e104923e0b..360a923847e434451900e6fda4f8d1bd9c23943d 100644 (file)
@@ -1 +1 @@
-add_subdirectory(dashboard_v2)
+add_subdirectory(dashboard)
diff --git a/src/pybind/mgr/dashboard/.coveragerc b/src/pybind/mgr/dashboard/.coveragerc
new file mode 100644 (file)
index 0000000..29a6319
--- /dev/null
@@ -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 (file)
index 0000000..a831e3d
--- /dev/null
@@ -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 (file)
index 0000000..b636948
--- /dev/null
@@ -0,0 +1,17 @@
+.coverage*
+htmlcov
+.tox
+coverage.xml
+junit*xml
+__pycache__
+.cache
+ceph.conf
+wheelhouse*
+
+# IDE
+.vscode
+.idea
+*.egg
+
+# virtualenv
+venv
diff --git a/src/pybind/mgr/dashboard/.pylintrc b/src/pybind/mgr/dashboard/.pylintrc
new file mode 100644 (file)
index 0000000..ab5d1f8
--- /dev/null
@@ -0,0 +1,548 @@
+[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=rados,rbd
+
+# 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.
+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=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,
+        locally-enabled,
+        file-ignored,
+        suppressed-message,
+        useless-suppression,
+        deprecated-pragma,
+        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,
+        missing-docstring,
+        invalid-name,
+        no-self-use,
+        too-few-public-methods,
+        no-member,
+        fixme
+
+# 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=
+
+# 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=no
+
+# 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=7
+
+# 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 (file)
index 0000000..512034c
--- /dev/null
@@ -0,0 +1,69 @@
+set(MGR_DASHBOARD_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/mgr-dashboard-virtualenv)
+
+add_custom_target(mgr-dashboard-test-venv
+  COMMAND
+  ${CMAKE_SOURCE_DIR}/src/tools/setup-virtualenv.sh ${MGR_DASHBOARD_VIRTUALENV} &&
+  ${MGR_DASHBOARD_VIRTUALENV}/bin/pip install --no-index --use-wheel --find-links=file:${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/wheelhouse -r requirements.txt
+  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard
+  COMMENT "dashboard tests virtualenv is being created")
+add_dependencies(tests mgr-dashboard-test-venv)
+
+if(WITH_MGR_DASHBOARD_FRONTEND AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
+  find_program(NPM_BIN
+    NAMES npm
+    HINTS $ENV{NPM_ROOT}/bin)
+  if(NOT NPM_BIN)
+    message(FATAL_ERROR "WITH_MGR_DASHBOARD_FRONTEND set, but npm not found")
+  endif()
+
+add_custom_command(
+  OUTPUT "${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend/node_modules"
+  COMMAND ${NPM_BIN} install
+  DEPENDS frontend/package.json
+  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend
+  COMMENT "dashboard frontend dependencies are being installed"
+)
+
+add_custom_target(mgr-dashboard-frontend-deps
+  DEPENDS frontend/node_modules
+  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend
+)
+
+
+# Glob some frontend files. With CMake 3.6, this can be simplified
+# to *.ts *.html. Just add:
+# list(FILTER frontend_src INCLUDE REGEX "frontend/src")
+file(
+  GLOB_RECURSE frontend_src
+  frontend/src/*.ts
+  frontend/src/*.html
+  frontend/src/*/*.ts
+  frontend/src/*/*.html
+  frontend/src/*/*/*.ts
+  frontend/src/*/*/*.html
+  frontend/src/*/*/*/*.ts
+  frontend/src/*/*/*/*.html
+  frontend/src/*/*/*/*/*.ts
+  frontend/src/*/*/*/*/*.html
+  frontend/src/*/*/*/*/*/*.ts
+  frontend/src/*/*/*/*/*/*.html)
+
+if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
+  set(npm_command ${NPM_BIN} run build -- --prod)
+else()
+  set(npm_command ${NPM_BIN} run build)
+endif()
+
+add_custom_command(
+  OUTPUT "${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend/dist"
+  COMMAND ${npm_command}
+  DEPENDS ${frontend_src} frontend/node_modules
+  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend
+  COMMENT "dashboard frontend is being created"
+)
+add_custom_target(mgr-dashboard-frontend-build
+  DEPENDS frontend/dist mgr-dashboard-frontend-deps
+  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend
+)
+add_dependencies(ceph-mgr mgr-dashboard-frontend-build)
+endif(WITH_MGR_DASHBOARD_FRONTEND AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
diff --git a/src/pybind/mgr/dashboard/HACKING.rst b/src/pybind/mgr/dashboard/HACKING.rst
new file mode 100644 (file)
index 0000000..f8d5e2b
--- /dev/null
@@ -0,0 +1,510 @@
+Dashboard Developer Documentation
+====================================
+
+Frontend Development
+--------------------
+
+Before you can start the dashboard from within a development environment,  you
+will need to generate the frontend code and either use a compiled and running
+Ceph cluster (e.g. started by ``vstart.sh``) or the standalone development web
+server.
+
+The build process is based on `Node.js <https://nodejs.org/>`_ and requires the
+`Node Package Manager <https://www.npmjs.com/>`_ ``npm`` to be installed.
+
+Prerequisites
+~~~~~~~~~~~~~
+
+Run ``npm install`` in directory ``src/pybind/mgr/dashboard/frontend`` to
+install the required packages locally.
+
+.. note::
+
+  If you do not have the `Angular CLI <https://github.com/angular/angular-cli>`_
+  installed globally, then you need to execute ``ng`` commands with an
+  additional ``npm run`` before it.
+
+Setting up a Development Server
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create the ``proxy.conf.json`` file based on ``proxy.conf.json.sample``.
+
+Run ``npm start -- --proxy-config proxy.conf.json`` for a dev server.
+Navigate to ``http://localhost:4200/``. The app will automatically
+reload if you change any of the source files.
+
+Code Scaffolding
+~~~~~~~~~~~~~~~~
+
+Run ``ng generate component component-name`` to generate a new
+component. You can also use
+``ng generate directive|pipe|service|class|guard|interface|enum|module``.
+
+Build the Project
+~~~~~~~~~~~~~~~~~
+
+Run ``npm run build`` to build the project. The build artifacts will be
+stored in the ``dist/`` directory. Use the ``-prod`` flag for a
+production build. Navigate to ``http://localhost:8080``.
+
+Running Unit Tests
+~~~~~~~~~~~~~~~~~~
+
+Run ``npm run test`` to execute the unit tests via `Karma
+<https://karma-runner.github.io>`_.
+
+Running End-to-End Tests
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Run ``npm run e2e`` to execute the end-to-end tests via
+`Protractor <http://www.protractortest.org/>`__.
+
+Further Help
+~~~~~~~~~~~~
+
+To get more help on the Angular CLI use ``ng help`` or go check out the
+`Angular CLI
+README <https://github.com/angular/angular-cli/blob/master/README.md>`__.
+
+Example of a Generator
+~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+    # Create module 'Core'
+    src/app> ng generate module core -m=app --routing
+
+    # Create module 'Auth' under module 'Core'
+    src/app/core> ng generate module auth -m=core --routing
+    or, alternatively:
+    src/app> ng generate module core/auth -m=core --routing
+
+    # Create component 'Login' under module 'Auth'
+    src/app/core/auth> ng generate component login -m=core/auth
+    or, alternatively:
+    src/app> ng generate component core/auth/login -m=core/auth
+
+Frontend Typescript Code Style Guide Recommendations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Group the imports based on its source and separate them with a blank
+line.
+
+The source groups can be either from Angular, external or internal.
+
+Example:
+
+.. code:: javascript
+
+    import { Component } from '@angular/core';
+    import { Router } from '@angular/router';
+
+    import { ToastsManager } from 'ng2-toastr';
+
+    import { Credentials } from '../../../shared/models/credentials.model';
+    import { HostService } from './services/host.service';
+
+
+Backend Development
+-------------------
+
+The Python backend code of this module requires a number of Python modules to be
+installed. They are listed in file ``requirements.txt``. Using `pip
+<https://pypi.python.org/pypi/pip>`_ you may install all required dependencies
+by issuing ``pip install -r requirements.txt`` in directory
+``src/pybind/mgr/dashboard``.
+
+If you're using the `ceph-dev-docker development environment
+<https://github.com/ricardoasmarques/ceph-dev-docker/>`_, simply run
+``./install_deps.sh`` from the toplevel directory to install them.
+
+Unit Testing and Linting
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+We included a ``tox`` configuration file that will run the unit tests under
+Python 2 or 3, as well as linting tools to guarantee the uniformity of code.
+
+You need to install ``tox`` and ``coverage`` before running it. To install the
+packages in your system, either install it via your operating system's package
+management tools, e.g. by running ``dnf install python-tox python-coverage`` on
+Fedora Linux.
+
+Alternatively, you can use Python's native package installation method::
+
+  $ pip install tox
+  $ pip install coverage
+
+The unit tests must run against a real Ceph cluster (no mocks are used). This
+has the advantage of catching bugs originated from changes in the internal Ceph
+code.
+
+Our ``tox.ini`` script will start a ``vstart`` Ceph cluster before running the
+python unit tests, and then it stops the cluster after the tests are run. Of
+course this implies that you have built/compiled Ceph previously.
+
+To run tox, run the following command in the root directory (where ``tox.ini``
+is located)::
+
+  $ PATH=../../../../build/bin:$PATH tox
+
+We also collect coverage information from the backend code. You can check the
+coverage information provided by the tox output, or by running the following
+command after tox has finished successfully::
+
+  $ coverage html
+
+This command will create a directory ``htmlcov`` with an HTML representation of
+the code coverage of the backend.
+
+You can also run a single step of the tox script (aka tox environment), for
+instance if you only want to run the linting tools, do::
+
+  $ PATH=../../../../build/bin:$PATH tox -e lint
+
+How to run a single unit test without using ``tox``?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When developing the code of a controller and respective test code, it's useful
+to be able to run that single test file without going through the whole ``tox``
+workflow.
+
+Since the tests must run against a real Ceph cluster, the first thing is to have
+a Ceph cluster running. For that we can leverage the tox environment that starts
+a Ceph cluster::
+
+  $ PATH=../../../../build/bin:$PATH tox -e ceph-cluster-start
+
+The command above uses ``vstart.sh`` script to start a Ceph cluster and
+automatically enables the ``dashboard`` module, and configures its cherrypy
+web server to listen in port ``9865``.
+
+After starting the Ceph cluster we can run our test file using ``py.test`` like
+this::
+
+  DASHBOARD_PORT=9865 UNITTEST=true py.test -s tests/test_mycontroller.py
+
+You can run tests multiple times without having to start and stop the Ceph
+cluster.
+
+After you finish your tests, you can stop the Ceph cluster using another tox
+environment::
+
+  $ tox -e ceph-cluster-stop
+
+How to add a new controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you want to add a new endpoint to the backend, you just need to add a
+class derived from ``BaseController`` decorated with ``ApiController`` in a
+Python file located under the ``controllers`` directory. The Dashboard module
+will automatically load your new controller upon start.
+
+For example create a file ``ping2.py`` under ``controllers`` directory with the
+following code::
+
+  import cherrypy
+  from ..tools import ApiController, BaseController
+
+  @ApiController('ping2')
+  class Ping2(BaseController):
+    @cherrypy.expose
+    def default(self, *args):
+      return "Hello"
+
+Every path given in the ``ApiController`` decorator will automatically be
+prefixed with ``api``. After reloading the Dashboard module you can access the
+above mentioned controller by pointing your browser to
+http://mgr_hostname:8080/api/ping2.
+
+It is also possible to have nested controllers. The ``RgwController`` uses
+this technique to make the daemons available through the URL
+http://mgr_hostname:8080/api/rgw/daemon::
+
+  @ApiController('rgw')
+  @AuthRequired()
+  class Rgw(RESTController):
+    pass
+
+
+  @ApiController('rgw/daemon')
+  @AuthRequired()
+  class RgwDaemon(RESTController):
+
+    def list(self):
+      pass
+
+
+Note that paths must be unique and that a path like ``rgw/daemon`` has to have
+a parent ``rgw``. Otherwise it won't work.
+
+How does the RESTController work?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We also provide a simple mechanism to create REST based controllers using the
+``RESTController`` class. Any class which inherits from ``RESTController`` will,
+by default, return JSON.
+
+The ``RESTController`` is basically an additional abstraction layer which eases
+and unifies the work with collections. A collection is just an array of objects
+with a specific type. ``RESTController`` enables some default mappings of
+request types and given parameters to specific method names. This may sound
+complicated at first, but it's fairly easy. Lets have look at the following
+example::
+
+  import cherrypy
+  from ..tools import ApiController, RESTController
+
+  @ApiController('ping2')
+  class Ping2(RESTController):
+    def list(self):
+      return {"msg": "Hello"}
+
+    def get(self, id):
+      return self.objects[id]
+
+In this case, the ``list`` method is automatically used for all requests to
+``api/ping2`` where no additional argument is given and where the request type
+is ``GET``. If the request is given an additional argument, the ID in our
+case, it won't map to ``list`` anymore but to ``get`` and return the element
+with the given ID (assuming that ``self.objects`` has been filled before). The
+same applies to other request types:
+
++--------------+------------+----------------+-------------+
+| Request type | Arguments  | Method         | Status Code |
++==============+============+================+=============+
+| GET          | No         | list           | 200         |
++--------------+------------+----------------+-------------+
+| PUT          | No         | bulk_set       | 200         |
++--------------+------------+----------------+-------------+
+| PATCH        | No         | bulk_set       | 200         |
++--------------+------------+----------------+-------------+
+| POST         | No         | create         | 201         |
++--------------+------------+----------------+-------------+
+| DELETE       | No         | bulk_delete    | 204         |
++--------------+------------+----------------+-------------+
+| GET          | Yes        | get            | 200         |
++--------------+------------+----------------+-------------+
+| PUT          | Yes        | set            | 200         |
++--------------+------------+----------------+-------------+
+| PATCH        | Yes        | set            | 200         |
++--------------+------------+----------------+-------------+
+| DELETE       | Yes        | delete         | 204         |
++--------------+------------+----------------+-------------+
+
+How to restrict access to a controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you require that only authenticated users can access you controller, just
+add the ``AuthRequired`` decorator to your controller class.
+
+Example::
+
+  import cherrypy
+  from ..tools import ApiController, AuthRequired, RESTController
+
+
+  @ApiController('ping2')
+  @AuthRequired()
+  class Ping2(RESTController):
+    def list(self):
+      return {"msg": "Hello"}
+
+Now only authenticated users will be able to "ping" your controller.
+
+
+How to access the manager module instance from a controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We provide the manager module instance as a global variable that can be
+imported in any module. We also provide a logger instance in the same way.
+
+Example::
+
+  import cherrypy
+  from .. import logger, mgr
+  from ..tools import ApiController, RESTController
+
+
+  @ApiController('servers')
+  class Servers(RESTController):
+    def list(self):
+      logger.debug('Listing available servers')
+      return {'servers': mgr.list_servers()}
+
+
+How to write a unit test for a controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We provide a test helper class called ``ControllerTestCase`` to easily create
+unit tests for your controller.
+
+If we want to write a unit test for the above ``Ping2`` controller, create a
+``test_ping2.py`` file under the ``tests`` directory with the following code::
+
+  from .helper import ControllerTestCase
+  from .controllers.ping2 import Ping2
+
+
+  class Ping2Test(ControllerTestCase):
+      @classmethod
+      def setup_test(cls):
+          Ping2._cp_config['tools.authentica.on'] = False
+
+      def test_ping2(self):
+          self._get("/api/ping2")
+          self.assertStatus(200)
+          self.assertJsonBody({'msg': 'Hello'})
+
+The ``ControllerTestCase`` class will call the dashboard module code that loads
+the controllers and initializes the CherryPy webserver. Then it will call the
+``setup_test()`` class method to execute additional instructions that each test
+case needs to add to the test.
+In the example above we use the ``setup_test()`` method to disable the
+authentication handler for the ``Ping2`` controller.
+
+
+How to listen for manager notifications in a controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The manager notifies the modules of several types of cluster events, such
+as cluster logging event, etc...
+
+Each module has a "global" handler function called ``notify`` that the manager
+calls to notify the module. But this handler function must not block or spend
+too much time processing the event notification.
+For this reason we provide a notification queue that controllers can register
+themselves with to receive cluster notifications.
+
+The example below represents a controller that implements a very simple live
+log viewer page::
+
+  from __future__ import absolute_import
+
+  import collections
+
+  import cherrypy
+
+  from ..tools import ApiController, BaseController, NotificationQueue
+
+
+  @ApiController('livelog')
+  class LiveLog(BaseController):
+      log_buffer = collections.deque(maxlen=1000)
+
+      def __init__(self):
+          super(LiveLog, self).__init__()
+          NotificationQueue.register(self.log, 'clog')
+
+      def log(self, log_struct):
+          self.log_buffer.appendleft(log_struct)
+
+      @cherrypy.expose
+      def default(self):
+          ret = '<html><meta http-equiv="refresh" content="2" /><body>'
+          for l in self.log_buffer:
+              ret += "{}<br>".format(l)
+          ret += "</body></html>"
+          return ret
+
+As you can see above, the ``NotificationQueue`` class provides a register
+method that receives the function as its first argument, and receives the
+"notification type" as the second argument.
+You can omit the second argument of the ``register`` method, and in that case
+you are registering to listen all notifications of any type.
+
+Here is an list of notification types (these might change in the future) that
+can be used:
+
+* ``clog``: cluster log notifications
+* ``command``: notification when a command issued by ``MgrModule.send_command``
+  completes
+* ``perf_schema_update``: perf counters schema update
+* ``mon_map``: monitor map update
+* ``fs_map``: cephfs map update
+* ``osd_map``: OSD map update
+* ``service_map``: services (RGW, RBD-Mirror, etc.) map update
+* ``mon_status``: monitor status regular update
+* ``health``: health status regular update
+* ``pg_summary``: regular update of PG status information
+
+
+How to write a unit test when a controller accesses a Ceph module?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Consider the following example that implements a controller that retrieves the
+list of RBD images of the ``rbd`` pool::
+
+  import rbd
+  from .. import mgr
+  from ..tools import ApiController, RESTController
+
+
+  @ApiController('rbdimages')
+  class RbdImages(RESTController):
+      def __init__(self):
+          self.ioctx = mgr.rados.open_ioctx('rbd')
+          self.rbd = rbd.RBD()
+
+      def list(self):
+          return [{'name': n} for n in self.rbd.list(self.ioctx)]
+
+In the example above, we want to mock the return value of the ``rbd.list``
+function, so that we can test the JSON response of the controller.
+
+The unit test code will look like the following::
+
+  import mock
+  from .helper import ControllerTestCase
+
+
+  class RbdImagesTest(ControllerTestCase):
+      @mock.patch('rbd.RBD.list')
+      def test_list(self, rbd_list_mock):
+          rbd_list_mock.return_value = ['img1', 'img2']
+          self._get('/api/rbdimages')
+          self.assertJsonBody([{'name': 'img1'}, {'name': 'img2'}])
+
+
+
+How to add a new configuration setting?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you need to store some configuration setting for a new feature, we already
+provide an easy mechanism for you to specify/use the new config setting.
+
+For instance, if you want to add a new configuration setting to hold the
+email address of the dashboard admin, just add a setting name as a class
+attribute to the ``Options`` class in the ``settings.py`` file::
+
+  # ...
+  class Options(object):
+    # ...
+
+    ADMIN_EMAIL_ADDRESS = ('admin@admin.com', str)
+
+The value of the class attribute is a pair composed by the default value for that
+setting, and the python type of the value.
+
+By declaring the ``ADMIN_EMAIL_ADDRESS`` class attribute, when you restart the
+dashboard plugin, you will atomatically gain two additional CLI commands to
+get and set that setting::
+
+  $ ceph dashboard get-admin-email-address
+  $ ceph dashboard set-admin-email-address <value>
+
+To access, or modify the config setting value from your Python code, either
+inside a controller or anywhere else, you just need to import the ``Settings``
+class and access it like this::
+
+  from settings import Settings
+
+  # ...
+  tmp_var = Settings.ADMIN_EMAIL_ADDRESS
+
+  # ....
+  Settings.ADMIN_EMAIL_ADDRESS = 'myemail@admin.com'
+
+The settings management implementation will make sure that if you change a
+setting value from the Python code you will see that change when accessing
+that setting from the CLI and vice-versa.
+
diff --git a/src/pybind/mgr/dashboard/README.rst b/src/pybind/mgr/dashboard/README.rst
new file mode 100644 (file)
index 0000000..2236818
--- /dev/null
@@ -0,0 +1,77 @@
+Dashboard and Administration Module for Ceph Manager
+=========================================================================
+
+Overview
+--------
+
+The original Ceph Manager Dashboard that was shipped with Ceph "Luminous"
+started out as a simple read-only view into various run-time information and
+performance data of a Ceph cluster.
+
+However, there is a `growing demand <http://pad.ceph.com/p/mimic-dashboard>`_
+for adding more web-based management capabilities, to make it easier for
+administrators that prefer a WebUI over the command line.
+
+This module is an ongoing project to add a native web based monitoring and
+administration application to Ceph Manager. It aims at becoming a successor of
+the existing dashboard, which provides read-only functionality and uses a
+simpler architecture to achieve the original goal.
+
+The code and architecture of this module is derived from and inspired by the
+`openATTIC Ceph management and monitoring tool <https://openattic.org/>`_ (both
+the backend and WebUI). The development is actively driven by the team behind
+openATTIC.
+
+The intention is to reuse as much of the existing openATTIC code as possible,
+while adapting it to the different environment. The current openATTIC backend
+implementation is based on Django and the Django REST framework, the Manager
+module's backend code will use the CherryPy framework and a custom REST API
+implementation instead.
+
+The WebUI implementation will be developed using Angular/TypeScript, merging
+both functionality from the existing dashboard as well as adding new
+functionality originally developed for the standalone version of openATTIC.
+
+The porting and migration of the existing openATTIC and dashboard functionality
+will be done in stages. The tasks are currently tracked in the `openATTIC team's
+JIRA instance <https://tracker.openattic.org/browse/OP-3039>`_.
+
+Enabling and Starting the Dashboard
+-----------------------------------
+
+If you have installed Ceph from distribution packages, the package management
+system should have taken care of installing all the required dependencies.
+
+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 <http://docs.ceph.com/docs/master/dev/>`_ for details on how to
+accomplish this.
+
+Finally, you need to build the dashboard frontend code. See the file
+``HACKING.rst`` in this directory for instructions on setting up the necessary
+development environment.
+
+From within a running Ceph cluster, you can start the Dashboard module by
+running the following command::
+
+  $ ceph mgr module enable dashboard
+
+You can see currently enabled Manager modules with::
+
+  $ ceph mgr module ls
+
+In order to be able to log in, you need to define a username and password, which
+will be stored in the MON's configuration database::
+
+  $ ceph dashboard set-login-credentials <username> <password>
+
+The password will be stored as a hash using ``bcrypt``.
+
+The Dashboard's WebUI should then be reachable on TCP port 8080.
+
+Working on the Dashboard Code
+-----------------------------
+
+If you're interested in helping with the development of the dashboard, please
+see the file ``HACKING.rst`` for details on how to set up a development
+environment and some other development-related topics.
diff --git a/src/pybind/mgr/dashboard/__init__.py b/src/pybind/mgr/dashboard/__init__.py
new file mode 100644 (file)
index 0000000..f09ef24
--- /dev/null
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=wrong-import-position,global-statement,protected-access
+"""
+openATTIC module
+"""
+from __future__ import absolute_import
+
+import os
+
+
+if 'UNITTEST' not in os.environ:
+    class _LoggerProxy(object):
+        def __init__(self):
+            self._logger = None
+
+        def __getattr__(self, item):
+            if self._logger is None:
+                raise AttributeError("logger not initialized")
+            return getattr(self._logger, item)
+
+    class _ModuleProxy(object):
+        def __init__(self):
+            self._mgr = None
+
+        def init(self, module_inst):
+            global logger
+            self._mgr = module_inst
+            logger._logger = self._mgr._logger
+
+        def __getattr__(self, item):
+            if self._mgr is None:
+                raise AttributeError("global manager module instance not initialized")
+            return getattr(self._mgr, item)
+
+    mgr = _ModuleProxy()
+    logger = _LoggerProxy()
+
+    from .module import Module, StandbyModule
+else:
+    import logging
+    logging.basicConfig(level=logging.DEBUG)
+    logger = logging.getLogger(__name__)
+    logging.root.handlers[0].setLevel(logging.DEBUG)
+    os.environ['PATH'] = '{}:{}'.format(os.path.abspath('../../../../build/bin'),
+                                        os.environ['PATH'])
+
+    # Mock ceph module otherwise every module that is involved in a testcase and imports it will
+    # raise an ImportError
+    import sys
+    import mock
+    sys.modules['ceph_module'] = mock.Mock()
+
+    mgr = mock.Mock()
diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/controllers/auth.py b/src/pybind/mgr/dashboard/controllers/auth.py
new file mode 100644 (file)
index 0000000..28a2f28
--- /dev/null
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import time
+import sys
+
+import bcrypt
+import cherrypy
+
+from ..tools import ApiController, RESTController, Session
+from .. import logger, mgr
+
+
+@ApiController('auth')
+class Auth(RESTController):
+    """
+    Provide login and logout actions.
+
+    Supported config-keys:
+
+      | KEY             | DEFAULT | DESCR                                     |
+      ------------------------------------------------------------------------|
+      | username        | None    | Username                                  |
+      | password        | None    | Password encrypted using bcrypt           |
+      | session-expire  | 1200    | Session will expire after <expires>       |
+      |                           | seconds without activity                  |
+    """
+
+    @RESTController.args_from_json
+    def create(self, username, password, stay_signed_in=False):
+        now = time.time()
+        config_username = mgr.get_config('username', None)
+        config_password = mgr.get_config('password', None)
+        hash_password = Auth.password_hash(password,
+                                           config_password)
+        if username == config_username and hash_password == config_password:
+            cherrypy.session.regenerate()
+            cherrypy.session[Session.USERNAME] = username
+            cherrypy.session[Session.TS] = now
+            cherrypy.session[Session.EXPIRE_AT_BROWSER_CLOSE] = not stay_signed_in
+            logger.debug('Login successful')
+            return {'username': username}
+
+        cherrypy.response.status = 403
+        if config_username is None:
+            logger.warning('No Credentials configured. Need to call `ceph dashboard '
+                           'set-login-credentials <username> <password>` first.')
+        else:
+            logger.debug('Login failed')
+        return {'detail': 'Invalid credentials'}
+
+    def bulk_delete(self):
+        logger.debug('Logout successful')
+        cherrypy.session[Session.USERNAME] = None
+        cherrypy.session[Session.TS] = None
+
+    @staticmethod
+    def password_hash(password, salt_password=None):
+        if not salt_password:
+            salt_password = bcrypt.gensalt()
+        if sys.version_info > (3, 0):
+            return bcrypt.hashpw(password, salt_password)
+        return bcrypt.hashpw(password.encode('utf8'), salt_password)
+
+    @staticmethod
+    def check_auth():
+        username = cherrypy.session.get(Session.USERNAME)
+        if not username:
+            logger.debug('Unauthorized access to %s',
+                         cherrypy.url(relative='server'))
+            raise cherrypy.HTTPError(401, 'You are not authorized to access '
+                                          'that resource')
+        now = time.time()
+        expires = float(mgr.get_config(
+            'session-expire', Session.DEFAULT_EXPIRE))
+        if expires > 0:
+            username_ts = cherrypy.session.get(Session.TS, None)
+            if username_ts and float(username_ts) < (now - expires):
+                cherrypy.session[Session.USERNAME] = None
+                cherrypy.session[Session.TS] = None
+                logger.debug('Session expired')
+                raise cherrypy.HTTPError(401,
+                                         'Session expired. You are not '
+                                         'authorized to access that resource')
+        cherrypy.session[Session.TS] = now
+
+    @staticmethod
+    def set_login_credentials(username, password):
+        mgr.set_config('username', username)
+        hashed_passwd = Auth.password_hash(password)
+        mgr.set_config('password', hashed_passwd)
diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py
new file mode 100644 (file)
index 0000000..c4786ce
--- /dev/null
@@ -0,0 +1,318 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from collections import defaultdict
+import json
+
+import cherrypy
+from mgr_module import CommandResult
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, BaseController, ViewCache
+
+
+@ApiController('cephfs')
+@AuthRequired()
+class CephFS(BaseController):
+    def __init__(self):
+        super(CephFS, self).__init__()
+
+        # Stateful instances of CephFSClients, hold cached results.  Key to
+        # dict is FSCID
+        self.cephfs_clients = {}
+
+    @cherrypy.expose
+    @cherrypy.tools.json_out()
+    def clients(self, fs_id):
+        fs_id = self.fs_id_to_int(fs_id)
+
+        return self._clients(fs_id)
+
+    @cherrypy.expose
+    @cherrypy.tools.json_out()
+    def data(self, fs_id):
+        fs_id = self.fs_id_to_int(fs_id)
+
+        return self.fs_status(fs_id)
+
+    @cherrypy.expose
+    @cherrypy.tools.json_out()
+    def mds_counters(self, fs_id):
+        """
+        Result format: map of daemon name to map of counter to list of datapoints
+        rtype: dict[str, dict[str, list]]
+        """
+
+        # Opinionated list of interesting performance counters for the GUI --
+        # if you need something else just add it.  See how simple life is
+        # when you don't have to write general purpose APIs?
+        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"
+        ]
+
+        fs_id = self.fs_id_to_int(fs_id)
+
+        result = {}
+        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 cherrypy.HTTPError(400, "Invalid cephfs id {}".format(fs_id))
+
+    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 get_rate(self, daemon_type, daemon_name, stat):
+        data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
+
+        if data and len(data) > 1:
+            return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
+
+        return 0
+
+    # pylint: disable=too-many-locals,too-many-statements,too-many-branches
+    def fs_status(self, fs_id):
+        mds_versions = 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 = self.get_latest("mds", info['name'], "mds.inodes")
+                inos = self.get_latest("mds", info['name'], "mds_mem.ino")
+
+                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 = self.colorize(state, self.GREEN)
+                # else:
+                #     c_state = self.colorize(state, self.YELLOW)
+
+                # Populate based on context of state, e.g. client
+                # ops for an active daemon, replay progress, reconnect
+                # progress
+                activity = ""
+
+                if state == "active":
+                    activity = self.get_rate("mds",
+                                             info['name'],
+                                             "mds_server.handle_client_request")
+
+                metadata = mgr.get_metadata('mds', info['name'])
+                mds_versions[metadata.get('ceph_version', 'unknown')].append(
+                    info['name'])
+                rank_table.append(
+                    {
+                        "rank": rank,
+                        "state": state,
+                        "mds": info['name'],
+                        "activity": activity,
+                        "dns": dns,
+                        "inos": inos
+                    }
+                )
+
+            else:
+                rank_table.append(
+                    {
+                        "rank": rank,
+                        "state": "failed",
+                        "mds": "",
+                        "activity": "",
+                        "dns": 0,
+                        "inos": 0
+                    }
+                )
+
+        # Find the standby replays
+        # pylint: disable=unused-variable
+        for gid_str, daemon_info in mdsmap['info'].iteritems():
+            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.inodes")
+
+            activity = self.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
+                }
+            )
+
+        df = mgr.get("df")
+        pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
+        osdmap = mgr.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 = []
+        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['bytes_used'],
+                "avail": stats['max_avail']
+            })
+
+        standby_table = []
+        for standby in fsmap['standbys']:
+            metadata = mgr.get_metadata('mds', standby['name'])
+            mds_versions[metadata.get('ceph_version', 'unknown')].append(
+                standby['name'])
+
+            standby_table.append({
+                'name': standby['name']
+            })
+
+        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']:
+                client['type'] = "userspace"
+                client['version'] = client['client_metadata']['ceph_version']
+                client['hostname'] = client['client_metadata']['hostname']
+            elif "kernel_version" in client['client_metadata']:
+                client['type'] = "kernel"
+                client['version'] = client['client_metadata']['kernel_version']
+                client['hostname'] = client['client_metadata']['hostname']
+            else:
+                client['type'] = "unknown"
+                client['version'] = ""
+                client['hostname'] = ""
+
+        return {
+            'status': status,
+            'data': clients
+        }
+
+    def get_latest(self, daemon_type, daemon_name, stat):
+        data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
+        if data:
+            return data[-1][1]
+        return 0
+
+
+class CephFSClients(object):
+    def __init__(self, module_inst, fscid):
+        self._module = module_inst
+        self.fscid = fscid
+
+    # pylint: disable=unused-variable
+    @ViewCache()
+    def get(self):
+        mds_spec = "{0}:0".format(self.fscid)
+        result = CommandResult("")
+        self._module.send_command(result, "mds", mds_spec,
+                                  json.dumps({
+                                      "prefix": "session ls",
+                                  }),
+                                  "")
+        r, outb, outs = result.wait()
+        # TODO handle nonzero returns, e.g. when rank isn't active
+        assert r == 0
+        return json.loads(outb)
diff --git a/src/pybind/mgr/dashboard/controllers/cluster_configuration.py b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py
new file mode 100644 (file)
index 0000000..d02027b
--- /dev/null
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import cherrypy
+
+from .. import mgr
+from ..tools import ApiController, RESTController, AuthRequired
+
+
+@ApiController('cluster_conf')
+@AuthRequired()
+class ClusterConfiguration(RESTController):
+    def list(self):
+        options = mgr.get("config_options")['options']
+        return options
+
+    def get(self, name):
+        for option in mgr.get('config_options')['options']:
+            if option['name'] == name:
+                return option
+
+        raise cherrypy.HTTPError(404)
diff --git a/src/pybind/mgr/dashboard/controllers/dashboard.py b/src/pybind/mgr/dashboard/controllers/dashboard.py
new file mode 100644 (file)
index 0000000..3457c2f
--- /dev/null
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import collections
+import json
+
+import cherrypy
+from mgr_module import CommandResult
+
+from .. import mgr
+from ..services.ceph_service import CephService
+from ..tools import ApiController, AuthRequired, BaseController, NotificationQueue
+
+
+LOG_BUFFER_SIZE = 30
+
+
+@ApiController('dashboard')
+@AuthRequired()
+class Dashboard(BaseController):
+    def __init__(self):
+        super(Dashboard, self).__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):
+        result = CommandResult("")
+        mgr.send_command(result, "mon", "", json.dumps({
+            "prefix": "log last",
+            "format": "json",
+            "channel": channel_name,
+            "num": LOG_BUFFER_SIZE
+        }), "")
+        r, outb, outs = result.wait()
+        if r != 0:
+            # Oh well. We won't let this stop us though.
+            self.log.error("Error fetching log history (r={0}, \"{1}\")".format(
+                r, outs))
+        else:
+            try:
+                lines = json.loads(outb)
+            except ValueError:
+                self.log.error("Error decoding log history")
+            else:
+                for l in lines:
+                    buf.appendleft(l)
+
+    # pylint: disable=R0914
+    @cherrypy.expose
+    @cherrypy.tools.json_out()
+    def health(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')
+
+        # Fuse osdmap with pg_summary to get description of pools
+        # including their PG states
+
+        osd_map = self.osd_map()
+
+        pools = CephService.get_pool_list_with_stats()
+
+        # Not needed, skip the effort of transmitting this
+        # to UI
+        del osd_map['pg_temp']
+
+        df = mgr.get("df")
+        df['stats']['total_objects'] = sum(
+            [p['stats']['objects'] for p in df['pools']])
+
+        return {
+            "health": self.health_data(),
+            "mon_status": self.mon_status(),
+            "fs_map": mgr.get('fs_map'),
+            "osd_map": osd_map,
+            "clog": list(self.log_buffer),
+            "audit_log": list(self.audit_buffer),
+            "pools": pools,
+            "mgr_map": mgr.get("mgr_map"),
+            "df": df
+        }
+
+    def mon_status(self):
+        mon_status_data = mgr.get("mon_status")
+        return json.loads(mon_status_data['json'])
+
+    def osd_map(self):
+        osd_map = mgr.get("osd_map")
+
+        assert osd_map is not None
+
+        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 health_data(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
diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py
new file mode 100644 (file)
index 0000000..8bef071
--- /dev/null
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, RESTController
+
+
+@ApiController('host')
+@AuthRequired()
+class Host(RESTController):
+    def list(self):
+        return mgr.list_servers()
diff --git a/src/pybind/mgr/dashboard/controllers/monitor.py b/src/pybind/mgr/dashboard/controllers/monitor.py
new file mode 100644 (file)
index 0000000..ac3bfe4
--- /dev/null
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+
+import cherrypy
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, BaseController
+
+
+@ApiController('monitor')
+@AuthRequired()
+class Monitor(BaseController):
+    @cherrypy.expose
+    @cherrypy.tools.json_out()
+    def default(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/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py
new file mode 100644 (file)
index 0000000..24fca6d
--- /dev/null
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+
+from mgr_module import CommandResult
+
+from .. import logger, mgr
+from ..tools import ApiController, AuthRequired, RESTController
+
+
+@ApiController('osd')
+@AuthRequired()
+class Osd(RESTController):
+    def get_counter(self, daemon_name, stat):
+        return mgr.get_counter('osd', daemon_name, stat)[stat]
+
+    def get_rate(self, daemon_name, stat):
+        data = self.get_counter(daemon_name, stat)
+        rate = 0
+        if data and len(data) > 1:
+            rate = (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
+        return rate
+
+    def get_latest(self, daemon_name, stat):
+        data = self.get_counter(daemon_name, stat)
+        latest = 0
+        if data and data[-1] and len(data[-1]) == 2:
+            latest = data[-1][1]
+        return latest
+
+    def list(self):
+        osds = self.get_osd_map()
+        # Extending by osd stats information
+        for s in mgr.get('osd_stats')['osd_stats']:
+            osds[str(s['osd'])].update({'osd_stats': s})
+        # Extending by osd node information
+        nodes = mgr.get('osd_map_tree')['nodes']
+        osd_tree = [(str(o['id']), o) for o in nodes if o['id'] >= 0]
+        for o in osd_tree:
+            osds[o[0]].update({'tree': o[1]})
+        # Extending by osd parent node information
+        hosts = [(h['name'], h) for h in nodes if h['id'] < 0]
+        for h in hosts:
+            for o_id in h[1]['children']:
+                if o_id >= 0:
+                    osds[str(o_id)]['host'] = h[1]
+        # Extending by osd histogram data
+        for o_id in osds:
+            o = osds[o_id]
+            o['stats'] = {}
+            o['stats_history'] = {}
+            osd_spec = str(o['osd'])
+            for s in ['osd.op_w', 'osd.op_in_bytes', 'osd.op_r', 'osd.op_out_bytes']:
+                prop = s.split('.')[1]
+                o['stats'][prop] = self.get_rate(osd_spec, s)
+                o['stats_history'][prop] = self.get_counter(osd_spec, s)
+            # Gauge stats
+            for s in ['osd.numpg', 'osd.stat_bytes', 'osd.stat_bytes_used']:
+                o['stats'][s.split('.')[1]] = self.get_latest(osd_spec, s)
+        return osds.values()
+
+    def get_osd_map(self):
+        osds = {}
+        for osd in mgr.get('osd_map')['osds']:
+            osd['id'] = osd['osd']
+            osds[str(osd['id'])] = osd
+        return osds
+
+    def get(self, svc_id):
+        result = CommandResult('')
+        mgr.send_command(result, 'osd', svc_id,
+                         json.dumps({
+                             'prefix': 'perf histogram dump',
+                         }),
+                         '')
+        r, outb, outs = result.wait()
+        if r != 0:
+            histogram = None
+            logger.warning('Failed to load histogram for OSD %s', svc_id)
+            logger.debug(outs)
+            histogram = outs
+        else:
+            histogram = json.loads(outb)
+        return {
+            'osd_map': self.get_osd_map()[svc_id],
+            'osd_metadata': mgr.get_metadata('osd', svc_id),
+            'histogram': histogram,
+        }
diff --git a/src/pybind/mgr/dashboard/controllers/perf_counters.py b/src/pybind/mgr/dashboard/controllers/perf_counters.py
new file mode 100644 (file)
index 0000000..59692d3
--- /dev/null
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, RESTController
+
+
+class PerfCounter(RESTController):
+    def __init__(self, service_type):
+        self._service_type = service_type
+
+    def _get_rate(self, daemon_type, daemon_name, stat):
+        data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
+        if data and len(data) > 1:
+            return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
+        return 0
+
+    def _get_latest(self, daemon_type, daemon_name, stat):
+        data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
+        if data:
+            return data[-1][1]
+        return 0
+
+    def get(self, service_id):
+        schema = mgr.get_perf_schema(
+            self._service_type, str(service_id)).values()[0]
+        counters = []
+
+        for key, value in sorted(schema.items()):
+            counter = dict()
+            counter['name'] = str(key)
+            counter['description'] = value['description']
+            # pylint: disable=W0212
+            if mgr._stattype_to_str(value['type']) == 'counter':
+                counter['value'] = self._get_rate(
+                    self._service_type, service_id, key)
+                counter['unit'] = mgr._unit_to_str(value['units'])
+            else:
+                counter['value'] = self._get_latest(
+                    self._service_type, service_id, key)
+                counter['unit'] = ''
+            counters.append(counter)
+
+        return {
+            'service': {
+                'type': self._service_type,
+                'id': service_id
+            },
+            'counters': counters
+        }
+
+
+@ApiController('perf_counters')
+@AuthRequired()
+class PerfCounters(RESTController):
+    def __init__(self):
+        self.mds = PerfCounter('mds')
+        self.mon = PerfCounter('mon')
+        self.osd = PerfCounter('osd')
+        self.rgw = PerfCounter('rgw')
+        self.rbd_mirror = PerfCounter('rbd-mirror')
+        self.mgr = PerfCounter('mgr')
+
+    def list(self):
+        counters = mgr.get_all_perf_counters()
+        return counters
diff --git a/src/pybind/mgr/dashboard/controllers/pool.py b/src/pybind/mgr/dashboard/controllers/pool.py
new file mode 100644 (file)
index 0000000..2eac9f5
--- /dev/null
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from ..services.ceph_service import CephService
+from ..tools import ApiController, RESTController, AuthRequired
+
+
+@ApiController('pool')
+@AuthRequired()
+class Pool(RESTController):
+
+    @classmethod
+    def _serialize_pool(cls, pool, attrs):
+        if not attrs or not isinstance(attrs, list):
+            return pool
+
+        res = {}
+        for attr in attrs:
+            if attr not in pool:
+                continue
+            if attr == 'type':
+                res[attr] = {1: 'replicated', 3: 'erasure'}[pool[attr]]
+            else:
+                res[attr] = pool[attr]
+
+        # pool_name is mandatory
+        res['pool_name'] = pool['pool_name']
+        return res
+
+    @staticmethod
+    def _str_to_bool(var):
+        if isinstance(var, bool):
+            return var
+        return var.lower() in ("true", "yes", "1", 1)
+
+    def list(self, attrs=None, stats=False):
+        if attrs:
+            attrs = attrs.split(',')
+
+        if self._str_to_bool(stats):
+            pools = CephService.get_pool_list_with_stats()
+        else:
+            pools = CephService.get_pool_list()
+
+        return [self._serialize_pool(pool, attrs) for pool in pools]
+
+    def get(self, pool_name, attrs=None, stats=False):
+        pools = self.list(attrs, stats)
+        return [pool for pool in pools if pool['pool_name'] == pool_name][0]
diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py
new file mode 100644 (file)
index 0000000..b73697b
--- /dev/null
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import math
+import cherrypy
+import rbd
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, RESTController, ViewCache
+
+
+@ApiController('rbd')
+@AuthRequired()
+class Rbd(RESTController):
+
+    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",
+    }
+
+    def __init__(self):
+        self.rbd = None
+
+    @staticmethod
+    def _format_bitmask(features):
+        """
+        Formats the bitmask:
+
+        >>> Rbd._format_bitmask(45)
+        'deep-flatten, exclusive-lock, layering, object-map'
+        """
+        names = [val for key, val in Rbd.RBD_FEATURES_NAME_MAPPING.items()
+                 if key & features == key]
+        return ', '.join(sorted(names))
+
+    @staticmethod
+    def _format_features(features):
+        """
+        Converts the features list to bitmask:
+
+        >>> Rbd._format_features(['deep-flatten', 'exclusive-lock', 'layering', 'object-map'])
+        45
+
+        >>> Rbd._format_features(None) is None
+        True
+
+        >>> Rbd._format_features('not a list') is None
+        True
+        """
+        if not features or not isinstance(features, list):
+            return None
+
+        res = 0
+        for key, value in Rbd.RBD_FEATURES_NAME_MAPPING.items():
+            if value in features:
+                res = key | res
+        return res
+
+    @ViewCache()
+    def _rbd_list(self, pool_name):
+        ioctx = mgr.rados.open_ioctx(pool_name)
+        self.rbd = rbd.RBD()
+        names = self.rbd.list(ioctx)
+        result = []
+        for name in names:
+            i = rbd.Image(ioctx, name)
+            stat = i.stat()
+            stat['name'] = name
+            features = i.features()
+            stat['features'] = features
+            stat['features_name'] = self._format_bitmask(features)
+
+            try:
+                parent_info = i.parent_info()
+                parent = "{}@{}".format(parent_info[0], parent_info[1])
+                if parent_info[0] != pool_name:
+                    parent = "{}/{}".format(parent_info[0], parent)
+                stat['parent'] = parent
+            except rbd.ImageNotFound:
+                pass
+            result.append(stat)
+        return result
+
+    def get(self, pool_name):
+        # pylint: disable=unbalanced-tuple-unpacking
+        status, value = self._rbd_list(pool_name)
+        if status == ViewCache.VALUE_EXCEPTION:
+            raise value
+        return {'status': status, 'value': value}
+
+    def create(self, data):
+        if not self.rbd:
+            self.rbd = rbd.RBD()
+
+        # Get input values
+        name = data.get('name')
+        pool_name = data.get('pool_name')
+        size = data.get('size')
+        obj_size = data.get('obj_size')
+        features = data.get('features')
+        stripe_unit = data.get('stripe_unit')
+        stripe_count = data.get('stripe_count')
+        data_pool = data.get('data_pool')
+
+        # Set order
+        order = None
+        if obj_size and obj_size > 0:
+            order = int(round(math.log(float(obj_size), 2)))
+
+        # Set features
+        feature_bitmask = self._format_features(features)
+
+        ioctx = mgr.rados.open_ioctx(pool_name)
+
+        try:
+            self.rbd.create(ioctx, name, size, order=order, old_format=False,
+                            features=feature_bitmask, stripe_unit=stripe_unit,
+                            stripe_count=stripe_count, data_pool=data_pool)
+        except rbd.OSError as e:
+            cherrypy.response.status = 400
+            return {'success': False, 'detail': str(e), 'errno': e.errno}
+        return {'success': True}
diff --git a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
new file mode 100644 (file)
index 0000000..62164ff
--- /dev/null
@@ -0,0 +1,305 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+import re
+
+from functools import partial
+
+import cherrypy
+import rbd
+
+from .. import logger, mgr
+from ..services.ceph_service import CephService
+from ..tools import ApiController, AuthRequired, BaseController, ViewCache
+
+
+@ViewCache()
+def get_daemons_and_pools():  # pylint: disable=R0915
+    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']
+
+                try:
+                    status = json.loads(status['json'])
+                except (ValueError, KeyError) as _:
+                    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_color': 'info',
+            'health': 'Unknown'
+        }
+        for _, pool_data in daemon['status'].items():  # TODO: simplify
+            if (health['health'] != 'error' and
+                    [k for k, v in pool_data.get('callouts', {}).items()
+                     if v['level'] == 'error']):
+                health = {
+                    'health_color': 'error',
+                    'health': 'Error'
+                }
+            elif (health['health'] != 'error' and
+                  [k for k, v in pool_data.get('callouts', {}).items()
+                   if v['level'] == 'warning']):
+                health = {
+                    'health_color': 'warning',
+                    'health': 'Warning'
+                }
+            elif health['health_color'] == 'info':
+                health = {
+                    'health_color': 'success',
+                    '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')]
+        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)
+            except:  # noqa pylint: disable=W0702
+                logger.exception("Failed to query mirror mode %s", pool_name)
+
+            stats = {}
+            if mirror_mode == rbd.RBD_MIRROR_MODE_DISABLED:
+                continue
+            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"
+                stats['health_color'] = "warning"
+                stats['health'] = "Warning"
+
+            pool_stats[pool_name] = dict(stats, **{
+                'mirror_mode': mirror_mode
+            })
+
+        for daemon in daemons:
+            for _, pool_data in daemon['status'].items():
+                stats = pool_stats.get(pool_data['name'], None)
+                if stats is None:
+                    continue
+
+                if pool_data.get('leader', False):
+                    # leader instance stores image counts
+                    stats['leader_id'] = daemon['metadata']['instance_id']
+                    stats['image_local_count'] = pool_data.get('image_local_count', 0)
+                    stats['image_remote_count'] = pool_data.get('image_remote_count', 0)
+
+                if (stats.get('health_color', '') != 'error' and
+                        pool_data.get('image_error_count', 0) > 0):
+                    stats['health_color'] = 'error'
+                    stats['health'] = 'Error'
+                elif (stats.get('health_color', '') != 'error' and
+                      pool_data.get('image_warning_count', 0) > 0):
+                    stats['health_color'] = 'warning'
+                    stats['health'] = 'Warning'
+                elif stats.get('health', None) is None:
+                    stats['health_color'] = 'success'
+                    stats['health'] = 'OK'
+
+        for _, stats in pool_stats.items():
+            if stats.get('health', None) is None:
+                # daemon doesn't know about pool
+                stats['health_color'] = 'error'
+                stats['health'] = 'Error'
+            elif stats.get('leader_id', None) is None:
+                # no daemons are managing the pool as leader instance
+                stats['health_color'] = 'warning'
+                stats['health'] = 'Warning'
+        return pool_stats
+
+    daemons = get_daemons()
+    return {
+        'daemons': daemons,
+        'pools': get_pools(daemons)
+    }
+
+
+@ApiController('rbdmirror')
+@AuthRequired()
+class RbdMirror(BaseController):
+
+    def __init__(self):
+        self.pool_data = {}
+
+    @cherrypy.expose
+    @cherrypy.tools.json_out()
+    def default(self):
+        status, content_data = self._get_content_data()
+        return {'status': status, 'content_data': content_data}
+
+    @ViewCache()
+    def _get_pool_datum(self, 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'
+            },
+            rbd.MIRROR_IMAGE_STATUS_STATE_STARTING_REPLAY: {
+                'health': 'ok',
+                'state_color': 'success',
+                'state': 'Starting'
+            },
+            rbd.MIRROR_IMAGE_STATUS_STATE_REPLAYING: {
+                'health': 'ok',
+                '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': 'Primary'
+            }
+        }
+
+        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_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)
+
+        return data
+
+    @ViewCache()
+    def _get_content_data(self):  # pylint: disable=R0914
+
+        def get_pool_datum(pool_name):
+            pool_datum = self.pool_data.get(pool_name, None)
+            if pool_datum is None:
+                pool_datum = partial(self._get_pool_datum, pool_name)
+                self.pool_data[pool_name] = pool_datum
+
+            _, value = pool_datum()
+            return value
+
+        pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
+        _, data = get_daemons_and_pools()
+        if isinstance(data, Exception):
+            logger.exception("Failed to get rbd-mirror daemons list")
+            raise type(data)(str(data))
+        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) or {}
+            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']
+                }
+
+                if mirror_image['health'] == 'ok':
+                    image.update({
+                        'state_color': mirror_image['state_color'],
+                        'state': mirror_image['state'],
+                        'description': mirror_image['description']
+                    })
+                    image_ready.append(image)
+                elif mirror_image['health'] == 'syncing':
+                    p = re.compile("bootstrapping, IMAGE_COPY/COPY_OBJECT (.*)%")
+                    image.update({
+                        'progress': (p.findall(mirror_image['description']) or [0])[0]
+                    })
+                    image_syncing.append(image)
+                else:
+                    image.update({
+                        'state_color': mirror_image['state_color'],
+                        'state': mirror_image['state'],
+                        '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
+        }
diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py
new file mode 100644 (file)
index 0000000..4f8e169
--- /dev/null
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+
+from .. import logger
+from ..services.ceph_service import CephService
+from ..tools import ApiController, RESTController, AuthRequired
+
+
+@ApiController('rgw')
+@AuthRequired()
+class Rgw(RESTController):
+    pass
+
+
+@ApiController('rgw/daemon')
+@AuthRequired()
+class RgwDaemon(RESTController):
+
+    def list(self):
+        daemons = []
+        for hostname, server in CephService.get_service_map('rgw').items():
+            for service in server['services']:
+                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'])
+
+                # extract per-daemon service data and health
+                daemon = {
+                    'id': service['id'],
+                    'version': metadata['ceph_version'],
+                    'server_hostname': hostname
+                }
+
+                daemons.append(daemon)
+
+        return sorted(daemons, key=lambda k: k['id'])
+
+    def get(self, svc_id):
+        daemon = {
+            'rgw_metadata': [],
+            'rgw_id': svc_id,
+            'rgw_status': []
+        }
+        service = CephService.get_service('rgw', svc_id)
+        if not service:
+            return daemon
+
+        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
diff --git a/src/pybind/mgr/dashboard/controllers/summary.py b/src/pybind/mgr/dashboard/controllers/summary.py
new file mode 100644 (file)
index 0000000..93631bb
--- /dev/null
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+
+import cherrypy
+
+from .. import logger, mgr
+from ..controllers.rbd_mirroring import get_daemons_and_pools
+from ..tools import AuthRequired, ApiController, BaseController
+from ..services.ceph_service import CephService
+
+
+@ApiController('summary')
+@AuthRequired()
+class Summary(BaseController):
+    def _rbd_pool_data(self):
+        pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
+        return sorted(pool_names)
+
+    def _health_status(self):
+        health_data = mgr.get("health")
+        return json.loads(health_data["json"])['status']
+
+    def _filesystems(self):
+        fsmap = mgr.get("fs_map")
+        return [
+            {
+                "id": f['id'],
+                "name": f['mdsmap']['fs_name']
+            }
+            for f in fsmap['filesystems']
+        ]
+
+    def _rbd_mirroring(self):
+        _, data = get_daemons_and_pools()
+
+        if isinstance(data, Exception):
+            logger.exception("Failed to get rbd-mirror daemons and pools")
+            raise type(data)(str(data))
+        else:
+            daemons = data.get('daemons', [])
+            pools = data.get('pools', {})
+
+        warnings = 0
+        errors = 0
+        for daemon in daemons:
+            if daemon['health_color'] == 'error':
+                errors += 1
+            elif daemon['health_color'] == 'warning':
+                warnings += 1
+        for _, pool in pools.items():
+            if pool['health_color'] == 'error':
+                errors += 1
+            elif pool['health_color'] == 'warning':
+                warnings += 1
+        return {'warnings': warnings, 'errors': errors}
+
+    @cherrypy.expose
+    @cherrypy.tools.json_out()
+    def default(self):
+        return {
+            'rbd_pools': self._rbd_pool_data(),
+            'health_status': self._health_status(),
+            'filesystems': self._filesystems(),
+            'rbd_mirroring': self._rbd_mirroring(),
+            'mgr_id': mgr.get_mgr_id(),
+            'have_mon_connection': mgr.have_mon_connection()
+        }
diff --git a/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py b/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py
new file mode 100644 (file)
index 0000000..f4849b7
--- /dev/null
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .. import mgr
+from ..services.ceph_service import CephService
+from ..tools import ApiController, AuthRequired, RESTController
+
+SERVICE_TYPE = 'tcmu-runner'
+
+
+@ApiController('tcmuiscsi')
+@AuthRequired()
+class TcmuIscsi(RESTController):
+    # pylint: disable=too-many-locals,too-many-nested-blocks
+    def list(self):  # pylint: disable=unused-argument
+        daemons = {}
+        images = {}
+        for service in CephService.get_service_list(SERVICE_TYPE):
+            metadata = service['metadata']
+            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)
+                lock_acquired_time = (mgr.get_counter(
+                    'tcmu-runner', service_id, perf_key)[perf_key] or
+                                      [[0, 0]])[-1][1] / 1000000000
+                if lock_acquired_time > image.get('optimized_since', 0):
+                    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)
+                        image['stats'][s] = mgr.get_rate(
+                            'tcmu-runner', service_id, perf_key)
+                        image['stats_history'][s] = mgr.get_counter(
+                            'tcmu-runner', service_id, perf_key)[perf_key]
+            else:
+                daemon['non_optimized_paths'] += 1
+                image['non_optimized_paths'].append(hostname)
+
+        return {
+            'daemons': sorted(daemons.values(), key=lambda d: d['server_hostname']),
+            'images': sorted(images.values(), key=lambda i: ['id']),
+        }
diff --git a/src/pybind/mgr/dashboard/frontend/.angular-cli.json b/src/pybind/mgr/dashboard/frontend/.angular-cli.json
new file mode 100644 (file)
index 0000000..6ffd7b7
--- /dev/null
@@ -0,0 +1,66 @@
+{
+  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+  "project": {
+    "name": "ceph-dashboard"
+  },
+  "apps": [
+    {
+      "root": "src",
+      "outDir": "dist",
+      "assets": [
+        "assets",
+        "favicon.ico"
+      ],
+      "index": "index.html",
+      "main": "main.ts",
+      "polyfills": "polyfills.ts",
+      "test": "test.ts",
+      "tsconfig": "tsconfig.app.json",
+      "testTsconfig": "tsconfig.spec.json",
+      "prefix": "cd",
+      "styles": [
+        "../node_modules/bootstrap/dist/css/bootstrap.css",
+        "../node_modules/ng2-toastr/bundles/ng2-toastr.min.css",
+        "../node_modules/font-awesome/css/font-awesome.css",
+        "../node_modules/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css",
+        "styles.scss"
+      ],
+      "scripts": [
+        "../node_modules/chart.js/dist/Chart.bundle.js"
+      ],
+      "environmentSource": "environments/environment.ts",
+      "environments": {
+        "dev": "environments/environment.ts",
+        "prod": "environments/environment.prod.ts"
+      }
+    }
+  ],
+  "e2e": {
+    "protractor": {
+      "config": "./protractor.conf.js"
+    }
+  },
+  "lint": [
+    {
+      "project": "src/tsconfig.app.json",
+      "exclude": "**/node_modules/**"
+    },
+    {
+      "project": "src/tsconfig.spec.json",
+      "exclude": "**/node_modules/**"
+    },
+    {
+      "project": "e2e/tsconfig.e2e.json",
+      "exclude": "**/node_modules/**"
+    }
+  ],
+  "test": {
+    "karma": {
+      "config": "./karma.conf.js"
+    }
+  },
+  "defaults": {
+    "styleExt": "scss",
+    "component": {}
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/.editorconfig b/src/pybind/mgr/dashboard/frontend/.editorconfig
new file mode 100644 (file)
index 0000000..6e87a00
--- /dev/null
@@ -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/.gitignore b/src/pybind/mgr/dashboard/frontend/.gitignore
new file mode 100644 (file)
index 0000000..2e55dc6
--- /dev/null
@@ -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
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+testem.log
+/typings
+
+# e2e
+/e2e/*.js
+/e2e/*.map
+
+# System Files
+.DS_Store
+Thumbs.db
+
+# Package lock files
+yarn.lock
+package-lock.json
+
+# Ceph
+!core
+!*.core
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/app.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/app.e2e-spec.ts
new file mode 100644 (file)
index 0000000..3e98370
--- /dev/null
@@ -0,0 +1,14 @@
+import { AppPage } from './app.po';
+
+describe('ceph-dashboard App', () => {
+  let page: AppPage;
+
+  beforeEach(() => {
+    page = new AppPage();
+  });
+
+  it('should display welcome message', () => {
+    page.navigateTo();
+    expect(page.getParagraphText()).toEqual('Welcome to oa!');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/app.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/app.po.ts
new file mode 100644 (file)
index 0000000..d9761bb
--- /dev/null
@@ -0,0 +1,11 @@
+import { browser, by, element } from 'protractor';
+
+export class AppPage {
+  navigateTo() {
+    return browser.get('/');
+  }
+
+  getParagraphText() {
+    return element(by.css('oa-root h1')).getText();
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/tsconfig.e2e.json b/src/pybind/mgr/dashboard/frontend/e2e/tsconfig.e2e.json
new file mode 100644 (file)
index 0000000..1d9e5ed
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/e2e",
+    "baseUrl": "./",
+    "module": "commonjs",
+    "target": "es5",
+    "types": [
+      "jasmine",
+      "jasminewd2",
+      "node"
+    ]
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/karma.conf.js b/src/pybind/mgr/dashboard/frontend/karma.conf.js
new file mode 100644 (file)
index 0000000..f86ab20
--- /dev/null
@@ -0,0 +1,40 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+  config.set({
+    basePath: '',
+    frameworks: ['jasmine', '@angular/cli'],
+    plugins: [
+      require('karma-jasmine'),
+      require('karma-chrome-launcher'),
+      require('karma-jasmine-html-reporter'),
+      require('karma-coverage-istanbul-reporter'),
+      require('@angular/cli/plugins/karma'),
+      require('karma-phantomjs-launcher'),
+      require('karma-junit-reporter')
+    ],
+    client:{
+      clearContext: false // leave Jasmine Spec Runner output visible in browser
+    },
+    coverageIstanbulReporter: {
+      reports: [ 'html', 'lcovonly', 'cobertura' ],
+      fixWebpackSourcePaths: true
+    },
+    angularCli: {
+      environment: 'dev'
+    },
+    reporters: ['progress', 'kjhtml', 'junit'],
+    junitReporter: {
+      'outputFile': 'junit.frontend.xml',
+      'suite': 'dashboard',
+      'useBrowserName': false
+    },
+    port: 9876,
+    colors: true,
+    logLevel: config.LOG_INFO,
+    autoWatch: true,
+    browsers: ['Chrome'],
+    singleRun: false
+  });
+};
diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json
new file mode 100644 (file)
index 0000000..e173870
--- /dev/null
@@ -0,0 +1,65 @@
+{
+  "name": "ceph-dashboard",
+  "version": "0.0.0",
+  "license": "MIT",
+  "scripts": {
+    "ng": "ng",
+    "start": "ng serve",
+    "build": "ng build",
+    "test": "ng test",
+    "lint": "ng lint",
+    "e2e": "ng e2e"
+  },
+  "private": true,
+  "dependencies": {
+    "@angular/animations": "^5.0.0",
+    "@angular/common": "^5.0.0",
+    "@angular/compiler": "^5.0.0",
+    "@angular/core": "^5.0.0",
+    "@angular/forms": "^5.0.0",
+    "@angular/http": "^5.0.0",
+    "@angular/platform-browser": "^5.0.0",
+    "@angular/platform-browser-dynamic": "^5.0.0",
+    "@angular/router": "^5.0.0",
+    "@swimlane/ngx-datatable": "^11.1.7",
+    "@types/lodash": "^4.14.95",
+    "awesome-bootstrap-checkbox": "0.3.7",
+    "bootstrap": "^3.3.7",
+    "chart.js": "^2.7.1",
+    "core-js": "^2.4.1",
+    "font-awesome": "4.7.0",
+    "lodash": "^4.17.4",
+    "moment": "2.20.1",
+    "ng2-charts": "^1.6.0",
+    "ng2-toastr": "4.1.2",
+    "ngx-bootstrap": "^2.0.1",
+    "rxjs": "^5.5.2",
+    "zone.js": "^0.8.14"
+  },
+  "devDependencies": {
+    "@angular/cli": "^1.6.5",
+    "@angular/compiler-cli": "^5.0.0",
+    "@angular/language-service": "^5.0.0",
+    "@types/jasmine": "~2.5.53",
+    "@types/jasminewd2": "~2.0.2",
+    "@types/node": "~6.0.60",
+    "codelyzer": "^4.0.1",
+    "copy-webpack-plugin": "4.3.0",
+    "jasmine-core": "~2.6.2",
+    "jasmine-spec-reporter": "~4.1.0",
+    "karma": "~1.7.0",
+    "karma-chrome-launcher": "~2.1.1",
+    "karma-cli": "~1.0.1",
+    "karma-coverage-istanbul-reporter": "^1.2.1",
+    "karma-jasmine": "~1.1.0",
+    "karma-jasmine-html-reporter": "^0.2.2",
+    "karma-junit-reporter": "^1.2.0",
+    "karma-phantomjs-launcher": "^1.0.4",
+    "node": "^8.9.4",
+    "protractor": "~5.1.2",
+    "ts-node": "~3.2.0",
+    "tslint": "~5.9.1",
+    "tslint-eslint-rules": "^4.1.1",
+    "typescript": "~2.4.2"
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/protractor.conf.js b/src/pybind/mgr/dashboard/frontend/protractor.conf.js
new file mode 100644 (file)
index 0000000..7ee3b5e
--- /dev/null
@@ -0,0 +1,28 @@
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const { SpecReporter } = require('jasmine-spec-reporter');
+
+exports.config = {
+  allScriptsTimeout: 11000,
+  specs: [
+    './e2e/**/*.e2e-spec.ts'
+  ],
+  capabilities: {
+    'browserName': 'chrome'
+  },
+  directConnect: true,
+  baseUrl: 'http://localhost:4200/',
+  framework: 'jasmine',
+  jasmineNodeOpts: {
+    showColors: true,
+    defaultTimeoutInterval: 30000,
+    print: function() {}
+  },
+  onPrepare() {
+    require('ts-node').register({
+      project: 'e2e/tsconfig.e2e.json'
+    });
+    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
+  }
+};
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 (file)
index 0000000..e654419
--- /dev/null
@@ -0,0 +1,7 @@
+{
+  "/api/": {
+    "target": "http://localhost:8080",
+    "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 (file)
index 0000000..8883796
--- /dev/null
@@ -0,0 +1,54 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
+import { MirroringComponent } from './ceph/block/mirroring/mirroring.component';
+import { PoolDetailComponent } from './ceph/block/pool-detail/pool-detail.component';
+import { CephfsComponent } from './ceph/cephfs/cephfs/cephfs.component';
+import { ClientsComponent } from './ceph/cephfs/clients/clients.component';
+import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
+import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
+import { MonitorComponent } from './ceph/cluster/monitor/monitor.component';
+import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component';
+import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
+import {
+  PerformanceCounterComponent
+} from './ceph/performance-counter/performance-counter/performance-counter.component';
+import { RgwDaemonListComponent } from './ceph/rgw/rgw-daemon-list/rgw-daemon-list.component';
+import { LoginComponent } from './core/auth/login/login.component';
+import { NotFoundComponent } from './core/not-found/not-found.component';
+import { AuthGuardService } from './shared/services/auth-guard.service';
+
+const routes: Routes = [
+  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
+  { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] },
+  { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
+  { path: 'login', component: LoginComponent },
+  { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
+  {
+    path: 'rgw',
+    component: RgwDaemonListComponent,
+    canActivate: [AuthGuardService]
+  },
+  { path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] },
+  { path: 'block/pool/:name', component: PoolDetailComponent, canActivate: [AuthGuardService] },
+  {
+    path: 'perf_counters/:type/:id',
+    component: PerformanceCounterComponent,
+    canActivate: [AuthGuardService]
+  },
+  { path: 'monitor', component: MonitorComponent, canActivate: [AuthGuardService] },
+  { path: 'cephfs/:id/clients', component: ClientsComponent, canActivate: [AuthGuardService] },
+  { path: 'cephfs/:id', component: CephfsComponent, canActivate: [AuthGuardService] },
+  { path: 'configuration', component: ConfigurationComponent, canActivate: [AuthGuardService] },
+  { path: 'mirroring', component: MirroringComponent, canActivate: [AuthGuardService] },
+  { path: '404', component: NotFoundComponent },
+  { path: 'osd', component: OsdListComponent, canActivate: [AuthGuardService] },
+  { path: '**', redirectTo: '/404'}
+];
+
+@NgModule({
+  imports: [RouterModule.forRoot(routes, { useHash: true })],
+  exports: [RouterModule]
+})
+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 (file)
index 0000000..638edaa
--- /dev/null
@@ -0,0 +1,5 @@
+<cd-navigation *ngIf="!isLoginActive()"></cd-navigation>
+<div class="container-fluid"
+     [ngClass]="{'full-height':isLoginActive()}">
+  <router-outlet></router-outlet>
+</div>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..3cca10d
--- /dev/null
@@ -0,0 +1,28 @@
+import { async, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+
+import { AppComponent } from './app.component';
+import { BlockModule } from './ceph/block/block.module';
+import { ClusterModule } from './ceph/cluster/cluster.module';
+import { CoreModule } from './core/core.module';
+import { SharedModule } from './shared/shared.module';
+
+describe('AppComponent', () => {
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        imports: [
+          RouterTestingModule,
+          CoreModule,
+          SharedModule,
+          ToastModule.forRoot(),
+          ClusterModule,
+          BlockModule
+        ],
+        declarations: [AppComponent]
+      }).compileComponents();
+    })
+  );
+});
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 (file)
index 0000000..c9e0e7e
--- /dev/null
@@ -0,0 +1,27 @@
+import { Component, ViewContainerRef } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ToastsManager } from 'ng2-toastr';
+
+import { AuthStorageService } from './shared/services/auth-storage.service';
+
+@Component({
+  selector: 'cd-root',
+  templateUrl: './app.component.html',
+  styleUrls: ['./app.component.scss']
+})
+export class AppComponent {
+  title = 'cd';
+
+  constructor(private authStorageService: AuthStorageService,
+              private router: Router,
+              public toastr: ToastsManager,
+              private vcr: ViewContainerRef) {
+    this.toastr.setRootViewContainerRef(vcr);
+  }
+
+  isLoginActive() {
+    return this.router.url === '/login' || !this.authStorageService.isLoggedIn();
+  }
+
+}
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 (file)
index 0000000..525e947
--- /dev/null
@@ -0,0 +1,57 @@
+import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { ToastModule, ToastOptions } from 'ng2-toastr/ng2-toastr';
+
+import { AccordionModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap';
+import { AppRoutingModule } from './app-routing.module';
+import { AppComponent } from './app.component';
+import { CephModule } from './ceph/ceph.module';
+import { CoreModule } from './core/core.module';
+import { AuthInterceptorService } from './shared/services/auth-interceptor.service';
+import { SharedModule } from './shared/shared.module';
+
+export class CustomOption extends ToastOptions {
+  animate = 'flyRight';
+  newestOnTop = true;
+  showCloseButton = true;
+  enableHTML = true;
+}
+
+@NgModule({
+  declarations: [
+    AppComponent
+  ],
+  imports: [
+    HttpClientModule,
+    BrowserModule,
+    BrowserAnimationsModule,
+    ToastModule.forRoot(),
+    AppRoutingModule,
+    HttpClientModule,
+    CoreModule,
+    SharedModule,
+    CephModule,
+    AccordionModule.forRoot(),
+    BsDropdownModule.forRoot(),
+    TabsModule.forRoot(),
+    HttpClientModule,
+    BrowserAnimationsModule
+  ],
+  exports: [SharedModule],
+  providers: [
+    {
+      provide: HTTP_INTERCEPTORS,
+      useClass: AuthInterceptorService,
+      multi: true
+    },
+    {
+      provide: ToastOptions,
+      useClass: CustomOption
+    },
+  ],
+  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 (file)
index 0000000..6e094fa
--- /dev/null
@@ -0,0 +1,35 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { ComponentsModule } from '../../shared/components/components.module';
+import { PipesModule } from '../../shared/pipes/pipes.module';
+import { ServicesModule } from '../../shared/services/services.module';
+import { SharedModule } from '../../shared/shared.module';
+import { IscsiComponent } from './iscsi/iscsi.component';
+import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
+import { MirroringComponent } from './mirroring/mirroring.component';
+import { PoolDetailComponent } from './pool-detail/pool-detail.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    TabsModule.forRoot(),
+    ProgressbarModule.forRoot(),
+    SharedModule,
+    ComponentsModule,
+    PipesModule,
+    ServicesModule
+  ],
+  declarations: [
+    PoolDetailComponent,
+    IscsiComponent,
+    MirroringComponent,
+    MirrorHealthColorPipe
+  ]
+})
+export class BlockModule { }
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 (file)
index 0000000..68f9326
--- /dev/null
@@ -0,0 +1,20 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item">Block</li>
+    <li i18n
+        class="breadcrumb-item active"
+        aria-current="page">iSCSI</li>
+  </ol>
+</nav>
+
+<legend i18n>Daemons</legend>
+<cd-table [data]="daemons"
+          (fetchData)="refresh()"
+          [columns]="daemonsColumns">
+</cd-table>
+
+<legend i18n>Images</legend>
+<cd-table [data]="images"
+          [columns]="imagesColumns">
+</cd-table>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..78c19b0
--- /dev/null
@@ -0,0 +1,37 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppModule } from '../../../app.module';
+import { TcmuIscsiService } from '../../../shared/services/tcmu-iscsi.service';
+import { IscsiComponent } from './iscsi.component';
+
+describe('IscsiComponent', () => {
+  let component: IscsiComponent;
+  let fixture: ComponentFixture<IscsiComponent>;
+
+  const fakeService = {
+    tcmuiscsi: () => {
+      return new Promise(function(resolve, reject) {
+        return;
+      });
+    },
+  };
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        imports: [AppModule],
+        providers: [{ provide: TcmuIscsiService, useValue: fakeService }]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(IscsiComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..9d700f4
--- /dev/null
@@ -0,0 +1,102 @@
+import { Component } from '@angular/core';
+
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
+import { ListPipe } from '../../../shared/pipes/list.pipe';
+import { RelativeDatePipe } from '../../../shared/pipes/relative-date.pipe';
+import { TcmuIscsiService } from '../../../shared/services/tcmu-iscsi.service';
+
+@Component({
+  selector: 'cd-iscsi',
+  templateUrl: './iscsi.component.html',
+  styleUrls: ['./iscsi.component.scss']
+})
+export class IscsiComponent {
+
+  daemons = [];
+  daemonsColumns: any;
+  images = [];
+  imagesColumns: any;
+
+  constructor(private tcmuIscsiService: TcmuIscsiService,
+              cephShortVersionPipe: CephShortVersionPipe,
+              dimlessBinaryPipe: DimlessBinaryPipe,
+              dimlessPipe: DimlessPipe,
+              relativeDatePipe: RelativeDatePipe,
+              listPipe: ListPipe) {
+    this.daemonsColumns = [
+      {
+        name: 'Hostname',
+        prop: 'server_hostname'
+      },
+      {
+        name: '# Active/Optimized',
+        prop: 'optimized_paths',
+      },
+      {
+        name: '# Active/Non-Optimized',
+        prop: 'non_optimized_paths'
+      },
+      {
+        name: 'Version',
+        prop: 'version',
+        pipe: cephShortVersionPipe
+      }
+    ];
+    this.imagesColumns = [
+      {
+        name: 'Pool',
+        prop: 'pool_name'
+      },
+      {
+        name: 'Image',
+        prop: 'name'
+      },
+      {
+        name: 'Active/Optimized',
+        prop: 'optimized_paths',
+        pipe: listPipe
+      },
+      {
+        name: 'Active/Non-Optimized',
+        prop: 'non_optimized_paths',
+        pipe: listPipe
+      },
+      {
+        name: 'Read Bytes',
+        prop: 'stats.rd_bytes',
+        pipe: dimlessBinaryPipe
+      },
+      {
+        name: 'Write Bytes',
+        prop: 'stats.wr_bytes',
+        pipe: dimlessBinaryPipe
+      },
+      {
+        name: 'Read Ops',
+        prop: 'stats.rd',
+        pipe: dimlessPipe
+      },
+      {
+        name: 'Write Ops',
+        prop: 'stats.wr',
+        pipe: dimlessPipe
+      },
+      {
+        name: 'A/O Since',
+        prop: 'optimized_since',
+        pipe: relativeDatePipe
+      },
+    ];
+
+  }
+
+  refresh() {
+    this.tcmuIscsiService.tcmuiscsi().then((resp) => {
+      this.daemons = resp.daemons;
+      this.images = resp.images;
+    });
+  }
+
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts
new file mode 100644 (file)
index 0000000..f22bcf2
--- /dev/null
@@ -0,0 +1,8 @@
+import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
+
+describe('MirrorHealthColorPipe', () => {
+  it('create an instance', () => {
+    const pipe = new MirrorHealthColorPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.ts
new file mode 100644 (file)
index 0000000..43d880f
--- /dev/null
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'mirrorHealthColor'
+})
+export class MirrorHealthColorPipe implements PipeTransform {
+  transform(value: any, args?: any): any {
+    if (value === 'warning') {
+      return 'label label-warning';
+    } else if (value === 'error') {
+      return 'label label-danger';
+    } else if (value === 'success') {
+      return 'label label-success';
+    }
+    return 'label label-info';
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.html
new file mode 100644 (file)
index 0000000..a76047d
--- /dev/null
@@ -0,0 +1,94 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li class="breadcrumb-item" i18n>Block</li>
+    <li class="breadcrumb-item active"
+        aria-current="page" i18n>Mirroring</li>
+  </ol>
+</nav>
+
+<cd-view-cache [status]="status"></cd-view-cache>
+
+<div class="row">
+  <div class="col-sm-6">
+    <fieldset>
+      <legend i18n>Daemons</legend>
+
+      <cd-table [data]="daemons.data"
+                columnMode="flex"
+                [columns]="daemons.columns"
+                [autoReload]="30000"
+                (fetchData)="refresh()">
+      </cd-table>
+    </fieldset>
+  </div>
+
+  <div class="col-sm-6">
+    <fieldset>
+      <legend i18n>Pools</legend>
+
+      <cd-table [data]="pools.data"
+                columnMode="flex"
+                [autoReload]="0"
+                (fetchData)="refresh()"
+                [columns]="pools.columns">
+      </cd-table>
+    </fieldset>
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-md-12">
+    <fieldset>
+      <legend i18n>Images</legend>
+      <tabset>
+        <tab heading="Issues" i18n-heading>
+          <cd-table [data]="image_error.data"
+                    columnMode="flex"
+                    [autoReload]="0"
+                    (fetchData)="refresh()"
+                    [columns]="image_error.columns">
+          </cd-table>
+        </tab>
+        <tab heading="Syncing" i18n-heading>
+          <cd-table [data]="image_syncing.data"
+                    columnMode="flex"
+                    [autoReload]="0"
+                    (fetchData)="refresh()"
+                    [columns]="image_syncing.columns">
+          </cd-table>
+        </tab>
+        <tab heading="Ready" i18n-heading>
+          <cd-table [data]="image_ready.data"
+                    columnMode="flex"
+                    [autoReload]="0"
+                    (fetchData)="refresh()"
+                    [columns]="image_ready.columns">
+          </cd-table>
+        </tab>
+      </tabset>
+    </fieldset>
+  </div>
+</div>
+
+<ng-template #healthTmpl
+             let-row="row"
+             let-value="value">
+  <span [ngClass]="row.health_color | mirrorHealthColor">{{ value }}</span>
+</ng-template>
+
+<ng-template #stateTmpl
+             let-row="row"
+             let-value="value">
+  <span [ngClass]="row.state_color | mirrorHealthColor">{{ value }}</span>
+</ng-template>
+
+<ng-template #syncTmpl>
+  <span class="label label-info">Syncing</span>
+</ng-template>
+
+<ng-template #progressTmpl
+             let-value="value">
+  <progressbar type="info"
+               [value]="value">
+  </progressbar>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts
new file mode 100644 (file)
index 0000000..f20d048
--- /dev/null
@@ -0,0 +1,50 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BsDropdownModule, TabsModule } from 'ngx-bootstrap';
+import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
+import { Observable } from 'rxjs/Observable';
+
+import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { MirrorHealthColorPipe } from '../mirror-health-color.pipe';
+import { MirroringComponent } from './mirroring.component';
+
+describe('MirroringComponent', () => {
+  let component: MirroringComponent;
+  let fixture: ComponentFixture<MirroringComponent>;
+
+  const fakeService = {
+    get: (service_type: string, service_id: string) => {
+      return Observable.create(observer => {
+        return () => console.log('disposed');
+      });
+    }
+  };
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        declarations: [MirroringComponent, MirrorHealthColorPipe],
+        imports: [
+          SharedModule,
+          BsDropdownModule.forRoot(),
+          TabsModule.forRoot(),
+          ProgressbarModule.forRoot(),
+          HttpClientTestingModule
+        ],
+        providers: [{ provide: RbdMirroringService, useValue: fakeService }]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(MirroringComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.ts
new file mode 100644 (file)
index 0000000..63e960e
--- /dev/null
@@ -0,0 +1,137 @@
+import { HttpClient } from '@angular/common/http';
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
+
+@Component({
+  selector: 'cd-mirroring',
+  templateUrl: './mirroring.component.html',
+  styleUrls: ['./mirroring.component.scss']
+})
+export class MirroringComponent implements OnInit {
+  @ViewChild('healthTmpl') healthTmpl: TemplateRef<any>;
+  @ViewChild('stateTmpl') stateTmpl: TemplateRef<any>;
+  @ViewChild('syncTmpl') syncTmpl: TemplateRef<any>;
+  @ViewChild('progressTmpl') progressTmpl: TemplateRef<any>;
+
+  contentData: any;
+
+  status: ViewCacheStatus;
+  daemons = {
+    data: [],
+    columns: []
+  };
+  pools = {
+    data: [],
+    columns: {}
+  };
+  image_error = {
+    data: [],
+    columns: {}
+  };
+  image_syncing = {
+    data: [],
+    columns: {}
+  };
+  image_ready = {
+    data: [],
+    columns: {}
+  };
+
+  constructor(
+    private http: HttpClient,
+    private rbdMirroringService: RbdMirroringService,
+    private cephShortVersionPipe: CephShortVersionPipe
+  ) { }
+
+  ngOnInit() {
+    this.daemons.columns = [
+      { prop: 'instance_id', name: 'Instance', flexGrow: 2 },
+      { prop: 'id', name: 'ID', flexGrow: 2 },
+      { prop: 'server_hostname', name: 'Hostname', flexGrow: 2 },
+      {
+        prop: 'server_hostname',
+        name: 'Version',
+        pipe: this.cephShortVersionPipe,
+        flexGrow: 2
+      },
+      {
+        prop: 'health',
+        name: 'Health',
+        cellTemplate: this.healthTmpl,
+        flexGrow: 1
+      }
+    ];
+
+    this.pools.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', flexGrow: 2 },
+      { prop: 'image_remote_count', name: '# Remote', flexGrow: 2 },
+      {
+        prop: 'health',
+        name: 'Health',
+        cellTemplate: this.healthTmpl,
+        flexGrow: 1
+      }
+    ];
+
+    this.image_error.columns = [
+      { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
+      { prop: 'name', name: 'Image', flexGrow: 2 },
+      { prop: 'description', name: 'Issue', flexGrow: 4 },
+      {
+        prop: 'state',
+        name: 'State',
+        cellTemplate: this.stateTmpl,
+        flexGrow: 1
+      }
+    ];
+
+    this.image_syncing.columns = [
+      { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
+      { prop: 'name', name: 'Image', flexGrow: 2 },
+      {
+        prop: 'progress',
+        name: 'Progress',
+        cellTemplate: this.progressTmpl,
+        flexGrow: 2
+      },
+      {
+        prop: 'state',
+        name: 'State',
+        cellTemplate: this.syncTmpl,
+        flexGrow: 1
+      }
+    ];
+
+    this.image_ready.columns = [
+      { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
+      { prop: 'name', name: 'Image', flexGrow: 2 },
+      { prop: 'description', name: 'Description', flexGrow: 4 },
+      {
+        prop: 'state',
+        name: 'State',
+        cellTemplate: this.stateTmpl,
+        flexGrow: 1
+      }
+    ];
+  }
+
+  refresh() {
+    this.rbdMirroringService.get().subscribe((data: any) => {
+      this.daemons.data = data.content_data.daemons;
+      this.pools.data = data.content_data.pools;
+      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.status = data.status;
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html
new file mode 100644 (file)
index 0000000..1bdd5a2
--- /dev/null
@@ -0,0 +1,18 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item">Block</li>
+    <li i18n
+        class="breadcrumb-item">Pools</li>
+    <li class="breadcrumb-item active"
+        aria-current="page">{{ name }}</li>
+  </ol>
+</nav>
+
+<cd-view-cache [status]="viewCacheStatus"></cd-view-cache>
+
+<cd-table [data]="images"
+          columnMode="flex"
+          [columns]="columns"
+          (fetchData)="loadImages()">
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts
new file mode 100644 (file)
index 0000000..aea790c
--- /dev/null
@@ -0,0 +1,40 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { AlertModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap';
+
+import { ComponentsModule } from '../../../shared/components/components.module';
+import { SharedModule } from '../../../shared/shared.module';
+import { PoolDetailComponent } from './pool-detail.component';
+
+describe('PoolDetailComponent', () => {
+  let component: PoolDetailComponent;
+  let fixture: ComponentFixture<PoolDetailComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        SharedModule,
+        BsDropdownModule.forRoot(),
+        TabsModule.forRoot(),
+        AlertModule.forRoot(),
+        ComponentsModule,
+        RouterTestingModule,
+        HttpClientTestingModule
+      ],
+      declarations: [ PoolDetailComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(PoolDetailComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts
new file mode 100644 (file)
index 0000000..98ac59c
--- /dev/null
@@ -0,0 +1,92 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
+import { PoolService } from '../../../shared/services/pool.service';
+
+@Component({
+  selector: 'cd-pool-detail',
+  templateUrl: './pool-detail.component.html',
+  styleUrls: ['./pool-detail.component.scss']
+})
+export class PoolDetailComponent implements OnInit, OnDestroy {
+  name: string;
+  images: any;
+  columns: CdTableColumn[];
+  retries: number;
+  routeParamsSubscribe: any;
+  viewCacheStatus: ViewCacheStatus;
+
+  constructor(
+    private route: ActivatedRoute,
+    private poolService: PoolService,
+    dimlessBinaryPipe: DimlessBinaryPipe,
+    dimlessPipe: DimlessPipe
+  ) {
+    this.columns = [
+      {
+        name: 'Name',
+        prop: 'name',
+        flexGrow: 2
+      },
+      {
+        name: 'Size',
+        prop: 'size',
+        flexGrow: 1,
+        cellClass: 'text-right',
+        pipe: dimlessBinaryPipe
+      },
+      {
+        name: 'Objects',
+        prop: 'num_objs',
+        flexGrow: 1,
+        cellClass: 'text-right',
+        pipe: dimlessPipe
+      },
+      {
+        name: 'Object size',
+        prop: 'obj_size',
+        flexGrow: 1,
+        cellClass: 'text-right',
+        pipe: dimlessBinaryPipe
+      },
+      {
+        name: 'Features',
+        prop: 'features_name',
+        flexGrow: 3
+      },
+      {
+        name: 'Parent',
+        prop: 'parent',
+        flexGrow: 2
+      }
+    ];
+  }
+
+  ngOnInit() {
+    this.routeParamsSubscribe = this.route.params.subscribe((params: { name: string }) => {
+      this.name = params.name;
+      this.images = [];
+      this.retries = 0;
+    });
+  }
+
+  ngOnDestroy() {
+    this.routeParamsSubscribe.unsubscribe();
+  }
+
+  loadImages() {
+    this.poolService.rbdPoolImages(this.name).then(
+      resp => {
+        this.viewCacheStatus = resp.status;
+        this.images = resp.value;
+      },
+      () => {
+        this.viewCacheStatus = ViewCacheStatus.ValueException;
+      }
+    );
+  }
+}
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 (file)
index 0000000..0f74b82
--- /dev/null
@@ -0,0 +1,25 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { SharedModule } from '../shared/shared.module';
+import { BlockModule } from './block/block.module';
+import { CephfsModule } from './cephfs/cephfs.module';
+import { ClusterModule } from './cluster/cluster.module';
+import { DashboardModule } from './dashboard/dashboard.module';
+import { PerformanceCounterModule } from './performance-counter/performance-counter.module';
+import { RgwModule } from './rgw/rgw.module';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    ClusterModule,
+    DashboardModule,
+    RgwModule,
+    PerformanceCounterModule,
+    BlockModule,
+    CephfsModule,
+    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 (file)
index 0000000..b98d708
--- /dev/null
@@ -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 (file)
index 0000000..62a023b
--- /dev/null
@@ -0,0 +1,6 @@
+@import '../../../../styles/chart-tooltip.scss';
+
+.chart-container {
+  height: 500px;
+  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 (file)
index 0000000..6d55204
--- /dev/null
@@ -0,0 +1,29 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+
+import { CephfsChartComponent } from './cephfs-chart.component';
+
+describe('CephfsChartComponent', () => {
+  let component: CephfsChartComponent;
+  let fixture: ComponentFixture<CephfsChartComponent>;
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        imports: [ChartsModule],
+        declarations: [CephfsChartComponent]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CephfsChartComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..cca1ae2
--- /dev/null
@@ -0,0 +1,164 @@
+import { Component, ElementRef, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
+
+import * as _ from 'lodash';
+import * as moment from 'moment';
+
+import { ChartTooltip } from '../../../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') chartCanvas: ElementRef;
+  @ViewChild('chartTooltip') chartTooltip: ElementRef;
+
+  @Input() mdsCounter: any;
+
+  lhsCounter = 'mds.inodes';
+  rhsCounter = 'mds_server.handle_client_request';
+
+  chart: any;
+
+  constructor() {}
+
+  ngOnInit() {
+    if (_.isUndefined(this.mdsCounter)) {
+      return;
+    }
+
+    const getTitle = title => {
+      return moment(title).format('LTS');
+    };
+
+    const getStyleTop = tooltip => {
+      return tooltip.caretY - tooltip.height - 15 + 'px';
+    };
+
+    const getStyleLeft = tooltip => {
+      return tooltip.caretX + 'px';
+    };
+
+    const chartTooltip = new ChartTooltip(
+      this.chartCanvas,
+      this.chartTooltip,
+      getStyleLeft,
+      getStyleTop
+    );
+    chartTooltip.getTitle = getTitle;
+    chartTooltip.checkOffset = true;
+
+    const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]);
+    const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]);
+
+    this.chart = {
+      datasets: [
+        {
+          label: this.lhsCounter,
+          yAxisID: 'LHS',
+          data: lhsData,
+          tension: 0.1
+        },
+        {
+          label: this.rhsCounter,
+          yAxisID: 'RHS',
+          data: rhsData,
+          tension: 0.1
+        }
+      ],
+      options: {
+        responsive: true,
+        maintainAspectRatio: false,
+        legend: {
+          position: 'top'
+        },
+        scales: {
+          xAxes: [
+            {
+              position: 'top',
+              type: 'time',
+              time: {
+                displayFormats: {
+                  quarter: 'MMM YYYY'
+                }
+              }
+            }
+          ],
+          yAxes: [
+            {
+              id: 'LHS',
+              type: 'linear',
+              position: 'left',
+              min: 0
+            },
+            {
+              id: 'RHS',
+              type: 'linear',
+              position: 'right',
+              min: 0
+            }
+          ]
+        },
+        tooltips: {
+          enabled: false,
+          mode: 'index',
+          intersect: false,
+          position: 'nearest',
+          custom: tooltip => {
+            chartTooltip.customTooltips(tooltip);
+          }
+        }
+      },
+      chartType: 'line'
+    };
+  }
+
+  ngOnChanges() {
+    if (!this.chart) {
+      return;
+    }
+
+    const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]);
+    const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]);
+
+    this.chart.datasets[0].data = lhsData;
+    this.chart.datasets[1].data = rhsData;
+  }
+
+  // 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)
+  convert_timeseries(sourceSeries) {
+    const data = [];
+    _.each(sourceSeries, dp => {
+      data.push({
+        x: dp[0] * 1000,
+        y: dp[1]
+      });
+    });
+
+    return data;
+  }
+
+  delta_timeseries(sourceSeries) {
+    let i;
+    let prev = sourceSeries[0];
+    const result = [];
+    for (i = 1; i < sourceSeries.length; i++) {
+      const cur = sourceSeries[i];
+      const tdelta = cur[0] - prev[0];
+      const vdelta = cur[1] - prev[1];
+      const rate = vdelta / tdelta;
+
+      result.push({
+        x: cur[0] * 1000,
+        y: rate
+      });
+
+      prev = cur;
+    }
+    return result;
+  }
+}
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 (file)
index 0000000..c47051c
--- /dev/null
@@ -0,0 +1,25 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
+
+import { AppRoutingModule } from '../../app-routing.module';
+import { SharedModule } from '../../shared/shared.module';
+import { CephfsChartComponent } from './cephfs-chart/cephfs-chart.component';
+import { CephfsService } from './cephfs.service';
+import { CephfsComponent } from './cephfs/cephfs.component';
+import { ClientsComponent } from './clients/clients.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    SharedModule,
+    AppRoutingModule,
+    ChartsModule,
+    ProgressbarModule.forRoot()
+  ],
+  declarations: [CephfsComponent, ClientsComponent, CephfsChartComponent],
+  providers: [CephfsService]
+})
+export class CephfsModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts
new file mode 100644 (file)
index 0000000..a9e59a0
--- /dev/null
@@ -0,0 +1,20 @@
+import { HttpClientModule } from '@angular/common/http';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { CephfsService } from './cephfs.service';
+
+describe('CephfsService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpClientModule],
+      providers: [CephfsService]
+    });
+  });
+
+  it(
+    'should be created',
+    inject([CephfsService], (service: CephfsService) => {
+      expect(service).toBeTruthy();
+    })
+  );
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.ts
new file mode 100644 (file)
index 0000000..a5c4994
--- /dev/null
@@ -0,0 +1,21 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class CephfsService {
+  baseURL = 'api/cephfs';
+
+  constructor(private http: HttpClient) {}
+
+  getCephfs(id) {
+    return this.http.get(`${this.baseURL}/data/${id}`);
+  }
+
+  getClients(id) {
+    return this.http.get(`${this.baseURL}/clients/${id}`);
+  }
+
+  getMdsCounters(id) {
+    return this.http.get(`${this.baseURL}/mds_counters/${id}`);
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html
new file mode 100644 (file)
index 0000000..ef62292
--- /dev/null
@@ -0,0 +1,69 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item">Filesystem</li>
+    <li class="breadcrumb-item active"
+        aria-current="page">{{ name }}</li>
+  </ol>
+</nav>
+
+<div class="row">
+  <div class="col-md-12">
+    <i class="fa fa-desktop"></i>
+    <a i18n
+       [routerLink]="['/cephfs/' + id + '/clients']">
+      <span style="font-weight:bold;">{{ clientCount }}</span>
+      Clients
+    </a>
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-sm-6">
+    <fieldset>
+      <legend i18n>Ranks</legend>
+
+      <cd-table [data]="ranks.data"
+                [columns]="ranks.columns"
+                (fetchData)="refresh()"
+                [toolHeader]="false">
+      </cd-table>
+    </fieldset>
+
+    <cd-table-key-value [data]="standbys">
+    </cd-table-key-value>
+  </div>
+
+  <div class="col-sm-6">
+    <fieldset>
+      <legend i18n>Pools</legend>
+
+      <cd-table [data]="pools.data"
+                [columns]="pools.columns"
+                [toolHeader]="false">
+      </cd-table>
+
+    </fieldset>
+  </div>
+</div>
+
+<div class="row"
+     *ngFor="let mdsCounter of objectValues(mdsCounters); trackBy: trackByFn">
+  <div class="cold-md-12">
+    <cd-cephfs-chart [mdsCounter]="mdsCounter"></cd-cephfs-chart>
+  </div>
+</div>
+
+<!-- templates -->
+<ng-template #poolProgressTmpl
+             let-row="row">
+  <progressbar type="danger"
+               [value]="row.used * 100.0 / row.avail">
+  </progressbar>
+</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/cephfs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss
new file mode 100644 (file)
index 0000000..d82829a
--- /dev/null
@@ -0,0 +1,3 @@
+.progress {
+  margin-bottom: 0px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts
new file mode 100644 (file)
index 0000000..3df655d
--- /dev/null
@@ -0,0 +1,57 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+import { BsDropdownModule, ProgressbarModule } from 'ngx-bootstrap';
+import { Observable } from 'rxjs/Observable';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { CephfsChartComponent } from '../cephfs-chart/cephfs-chart.component';
+import { CephfsService } from '../cephfs.service';
+import { CephfsComponent } from './cephfs.component';
+
+describe('CephfsComponent', () => {
+  let component: CephfsComponent;
+  let fixture: ComponentFixture<CephfsComponent>;
+
+  const fakeFilesystemService = {
+    getCephfs: id => {
+      return Observable.create(observer => {
+        return () => console.log('disposed');
+      });
+    },
+    getMdsCounters: id => {
+      return Observable.create(observer => {
+        return () => console.log('disposed');
+      });
+    }
+  };
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        imports: [
+          SharedModule,
+          ChartsModule,
+          RouterTestingModule,
+          BsDropdownModule.forRoot(),
+          ProgressbarModule.forRoot()
+        ],
+        declarations: [CephfsComponent, CephfsChartComponent],
+        providers: [
+          { provide: CephfsService, useValue: fakeFilesystemService }
+        ]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CephfsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts
new file mode 100644 (file)
index 0000000..d8fe382
--- /dev/null
@@ -0,0 +1,126 @@
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+import * as _ from 'lodash';
+import { Subscription } from 'rxjs/Subscription';
+
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
+import { CephfsService } from '../cephfs.service';
+
+@Component({
+  selector: 'cd-cephfs',
+  templateUrl: './cephfs.component.html',
+  styleUrls: ['./cephfs.component.scss']
+})
+export class CephfsComponent implements OnInit, OnDestroy {
+  @ViewChild('poolProgressTmpl') poolProgressTmpl: TemplateRef<any>;
+  @ViewChild('activityTmpl') activityTmpl: TemplateRef<any>;
+
+  routeParamsSubscribe: Subscription;
+
+  objectValues = Object.values;
+
+  id: number;
+  name: string;
+  ranks: any;
+  pools: any;
+  standbys = [];
+  clientCount: number;
+
+  mdsCounters = {};
+
+  constructor(
+    private route: ActivatedRoute,
+    private cephfsService: CephfsService,
+    private dimlessBinary: DimlessBinaryPipe,
+    private dimless: DimlessPipe
+  ) {}
+
+  ngOnInit() {
+    this.ranks = {
+      columns: [
+        { prop: 'rank' },
+        { prop: 'state' },
+        { prop: 'mds', name: 'Daemon' },
+        { prop: 'activity', cellTemplate: this.activityTmpl },
+        { prop: 'dns', name: 'Dentries', pipe: this.dimless },
+        { prop: 'inos', name: 'Inodes', pipe: this.dimless }
+      ],
+      data: []
+    };
+
+    this.pools = {
+      columns: [
+        { prop: 'pool' },
+        { prop: 'type' },
+        { prop: 'used', pipe: this.dimlessBinary },
+        { prop: 'avail', pipe: this.dimlessBinary },
+        {
+          name: 'Usage',
+          cellTemplate: this.poolProgressTmpl,
+          comparator: (valueA, valueB, rowA, rowB, sortDirection) => {
+            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;
+            }
+          }
+        }
+      ],
+      data: []
+    };
+
+    this.routeParamsSubscribe = this.route.params.subscribe((params: { id: number }) => {
+      this.id = params.id;
+
+      this.ranks.data = [];
+      this.pools.data = [];
+      this.standbys = [];
+      this.mdsCounters = {};
+    });
+  }
+
+  ngOnDestroy() {
+    this.routeParamsSubscribe.unsubscribe();
+  }
+
+  refresh() {
+    this.cephfsService.getCephfs(this.id).subscribe((data: any) => {
+      this.ranks.data = data.cephfs.ranks;
+      this.pools.data = data.cephfs.pools;
+      this.standbys = [
+        {
+          key: 'Standby daemons',
+          value: data.standbys.map(value => value.name).join(', ')
+        }
+      ];
+      this.name = data.cephfs.name;
+      this.clientCount = data.cephfs.client_count;
+    });
+
+    this.cephfsService.getMdsCounters(this.id).subscribe(data => {
+      _.each(this.mdsCounters, (value, key) => {
+        if (data[key] === undefined) {
+          delete this.mdsCounters[key];
+        }
+      });
+
+      _.each(data, (mdsData: any, mdsName) => {
+        mdsData.name = mdsName;
+        this.mdsCounters[mdsName] = mdsData;
+      });
+    });
+  }
+
+  trackByFn(index, item) {
+    return item.name;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.html
new file mode 100644 (file)
index 0000000..7832a38
--- /dev/null
@@ -0,0 +1,22 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item">Filesystem</li>
+    <li class="breadcrumb-item">
+      <a [routerLink]="['/cephfs/' + id]">{{ name }}</a>
+    </li>
+    <li i18n
+        class="breadcrumb-item active"
+        aria-current="page">Clients</li>
+  </ol>
+</nav>
+
+<fieldset>
+  <cd-view-cache [status]="viewCacheStatus"></cd-view-cache>
+
+  <cd-table [data]="clients.data"
+            [columns]="clients.columns"
+            (fetchData)="refresh()"
+            [header]="false">
+  </cd-table>
+</fieldset>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts
new file mode 100644 (file)
index 0000000..d3506a9
--- /dev/null
@@ -0,0 +1,51 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+import { Observable } from 'rxjs/Observable';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { CephfsService } from '../cephfs.service';
+import { ClientsComponent } from './clients.component';
+
+describe('ClientsComponent', () => {
+  let component: ClientsComponent;
+  let fixture: ComponentFixture<ClientsComponent>;
+
+  const fakeFilesystemService = {
+    getCephfs: id => {
+      return Observable.create(observer => {
+        return () => console.log('disposed');
+      });
+    },
+    getClients: id => {
+      return Observable.create(observer => {
+        return () => console.log('disposed');
+      });
+    }
+  };
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        imports: [
+          RouterTestingModule,
+          BsDropdownModule.forRoot(),
+          SharedModule
+        ],
+        declarations: [ClientsComponent],
+        providers: [{ provide: CephfsService, useValue: fakeFilesystemService }]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ClientsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.ts
new file mode 100644 (file)
index 0000000..fc2cbde
--- /dev/null
@@ -0,0 +1,56 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+import { CephfsService } from '../cephfs.service';
+
+@Component({
+  selector: 'cd-clients',
+  templateUrl: './clients.component.html',
+  styleUrls: ['./clients.component.scss']
+})
+export class ClientsComponent implements OnInit, OnDestroy {
+  routeParamsSubscribe: any;
+
+  id: number;
+  name: string;
+  clients: any;
+  viewCacheStatus: ViewCacheStatus;
+
+  constructor(private route: ActivatedRoute, private cephfsService: CephfsService) {}
+
+  ngOnInit() {
+    this.clients = {
+      columns: [
+        { prop: 'id' },
+        { prop: 'type' },
+        { prop: 'state' },
+        { prop: 'version' },
+        { prop: 'hostname', name: 'Host' },
+        { prop: 'root' }
+      ],
+      data: []
+    };
+
+    this.routeParamsSubscribe = this.route.params.subscribe((params: { id: number }) => {
+      this.id = params.id;
+      this.clients.data = [];
+      this.viewCacheStatus = ViewCacheStatus.ValueNone;
+
+      this.cephfsService.getCephfs(this.id).subscribe((data: any) => {
+        this.name = data.cephfs.name;
+      });
+    });
+  }
+
+  ngOnDestroy() {
+    this.routeParamsSubscribe.unsubscribe();
+  }
+
+  refresh() {
+    this.cephfsService.getClients(this.id).subscribe((data: any) => {
+      this.viewCacheStatus = data.status;
+      this.clients.data = data.data;
+    });
+  }
+}
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 (file)
index 0000000..d661f51
--- /dev/null
@@ -0,0 +1,48 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { ComponentsModule } from '../../shared/components/components.module';
+import { SharedModule } from '../../shared/shared.module';
+import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
+import { ConfigurationComponent } from './configuration/configuration.component';
+import { HostsComponent } from './hosts/hosts.component';
+import { MonitorService } from './monitor.service';
+import { MonitorComponent } from './monitor/monitor.component';
+import { OsdDetailsComponent } from './osd/osd-details/osd-details.component';
+import { OsdListComponent } from './osd/osd-list/osd-list.component';
+import {
+  OsdPerformanceHistogramComponent
+} from './osd/osd-performance-histogram/osd-performance-histogram.component';
+import { OsdService } from './osd/osd.service';
+
+@NgModule({
+  entryComponents: [
+    OsdDetailsComponent
+  ],
+  imports: [
+    CommonModule,
+    PerformanceCounterModule,
+    ComponentsModule,
+    TabsModule.forRoot(),
+    SharedModule,
+    RouterModule,
+    FormsModule
+  ],
+  declarations: [
+    HostsComponent,
+    MonitorComponent,
+    ConfigurationComponent,
+    OsdListComponent,
+    OsdDetailsComponent,
+    OsdPerformanceHistogramComponent
+  ],
+  providers: [
+    MonitorService,
+    OsdService
+  ]
+})
+export class ClusterModule {}
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 (file)
index 0000000..efe071a
--- /dev/null
@@ -0,0 +1,67 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li class="breadcrumb-item">Cluster</li>
+    <li class="breadcrumb-item active"
+        aria-current="page">Configuration Documentation</li>
+  </ol>
+</nav>
+
+<div class="dataTables_wrapper">
+  <div class="dataTables_header clearfix form-inline">
+    <!-- filters -->
+    <div class="form-group pull-right filter"
+         *ngFor="let filter of filters">
+      <label>{{ filter.label }}: </label>
+      <select class="form-control input-sm"
+              [(ngModel)]="filter.value"
+              (ngModelChange)="updateFilter()">
+        <option *ngFor="let opt of filter.options">{{ opt }}</option>
+      </select>
+    </div>
+    <!-- end filters -->
+  </div>
+
+  <table class="oadatatable table table-striped table-condensed table-bordered table-hover">
+    <thead class="datatable-header">
+      <tr>
+        <th >Name</th>
+        <th style="width:400px;">Description</th>
+        <th>Type</th>
+        <th>Level</th>
+        <th style="width: 200px">Default</th>
+        <th>Tags</th>
+        <th>Services</th>
+        <th>See_also</th>
+        <th>Max</th>
+        <th>Min</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr *ngFor="let row of data | filter:filters">
+        <td >{{ row.name }}</td>
+        <td>
+          <p>
+            {{ row.desc }}</p>
+          <p *ngIf="row.long_desc"
+             class=text-muted>{{ row.long_desc }}</p>
+        </td>
+        <td>{{ row.type }}</td>
+        <td>{{ row.level }}</td>
+        <td class="wrap">
+          {{ row.default }} {{ row.daemon_default }}
+        </td>
+        <td>
+          <p *ngFor="let item of row.tags">{{ item }}</p>
+        </td>
+        <td>
+          <p *ngFor="let item of row.services">{{ item }}</p>
+        </td>
+        <td class="wrap">
+          <p *ngFor="let item of row.see_also">{{ item }}</p>
+        </td>
+        <td>{{ row.max }}</td>
+        <td>{{ row.min }}</td>
+      </tr>
+    </tbody>
+  </table>
+</div>
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 (file)
index 0000000..e968d6d
--- /dev/null
@@ -0,0 +1,5 @@
+@import '../../../shared/datatable/table/table.component.scss';
+
+td.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 (file)
index 0000000..0d98766
--- /dev/null
@@ -0,0 +1,41 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+
+import { Observable } from 'rxjs/Observable';
+
+import { ConfigurationService } from '../../../shared/services/configuration.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { ConfigurationComponent } from './configuration.component';
+
+describe('ConfigurationComponent', () => {
+  let component: ConfigurationComponent;
+  let fixture: ComponentFixture<ConfigurationComponent>;
+
+  const fakeService = {
+    getConfigData: () => {
+      return Observable.create(observer => {
+        return () => console.log('disposed');
+      });
+    }
+  };
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        declarations: [ConfigurationComponent],
+        providers: [{ provide: ConfigurationService, useValue: fakeService }],
+        imports: [SharedModule, FormsModule]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ConfigurationComponent);
+    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.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts
new file mode 100644 (file)
index 0000000..7c6ed68
--- /dev/null
@@ -0,0 +1,79 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { ConfigurationService } from '../../../shared/services/configuration.service';
+
+@Component({
+  selector: 'cd-configuration',
+  templateUrl: './configuration.component.html',
+  styleUrls: ['./configuration.component.scss']
+})
+export class ConfigurationComponent implements OnInit {
+  @ViewChild('arrayTmpl') arrayTmpl: TemplateRef<any>;
+
+  data = [];
+  columns: any;
+
+  filters = [
+    {
+      label: 'Level',
+      prop: 'level',
+      value: 'basic',
+      options: ['basic', 'advanced', 'dev'],
+      applyFilter: (row, value) => {
+        enum Level {
+          basic = 0,
+          advanced = 1,
+          dev = 2
+        }
+
+        const levelVal = Level[value];
+
+        return Level[row.level] <= levelVal;
+      }
+    },
+    {
+      label: 'Service',
+      prop: 'services',
+      value: 'any',
+      options: ['mon', 'mgr', 'osd', 'mds', 'common', 'mds_client', 'rgw', 'any'],
+      applyFilter: (row, value) => {
+        if (value === 'any') {
+          return true;
+        }
+
+        return row.services.includes(value);
+      }
+    }
+  ];
+
+  constructor(private configurationService: ConfigurationService) {}
+
+  ngOnInit() {
+    this.columns = [
+      { flexGrow: 2, canAutoResize: true, prop: 'name' },
+      { flexGrow: 2, prop: 'desc', name: 'Description' },
+      { flexGrow: 2, prop: 'long_desc', name: 'Long description' },
+      { flexGrow: 1, prop: 'type' },
+      { flexGrow: 1, prop: 'level' },
+      { flexGrow: 1, prop: 'default' },
+      { flexGrow: 2, prop: 'daemon_default', name: 'Daemon default' },
+      { flexGrow: 1, prop: 'tags', name: 'Tags', cellTemplate: this.arrayTmpl },
+      { flexGrow: 1, prop: 'services', name: 'Services', cellTemplate: this.arrayTmpl },
+      { flexGrow: 1, prop: 'see_also', name: 'See_also', cellTemplate: this.arrayTmpl },
+      { flexGrow: 1, prop: 'max', name: 'Max' },
+      { flexGrow: 1, prop: 'min', name: 'Min' }
+    ];
+
+    this.fetchData();
+  }
+
+  fetchData() {
+    this.configurationService.getConfigData().subscribe((data: any) => {
+      this.data = data;
+    });
+  }
+
+  updateFilter() {
+    this.data = [...this.data];
+  }
+}
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 (file)
index 0000000..f2935c3
--- /dev/null
@@ -0,0 +1,19 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item">Cluster</li>
+    <li i18n
+        class="breadcrumb-item active"
+        aria-current="page">Hosts</li>
+  </ol>
+</nav>
+<cd-table [data]="hosts"
+          [columns]="columns"
+          columnMode="flex"
+          (fetchData)="getHosts()">
+  <ng-template #servicesTpl let-value="value">
+    <span *ngFor="let service of value; last as isLast">
+      <a [routerLink]="[service.cdLink]">{{ service.type }}.{{ service.id }}</a>{{ !isLast ? ", " : "" }}
+    </span>
+  </ng-template>
+</cd-table>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..90eb5e6
--- /dev/null
@@ -0,0 +1,40 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+
+import { ComponentsModule } from '../../../shared/components/components.module';
+import { SharedModule } from '../../../shared/shared.module';
+import { HostsComponent } from './hosts.component';
+
+describe('HostsComponent', () => {
+  let component: HostsComponent;
+  let fixture: ComponentFixture<HostsComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        SharedModule,
+        HttpClientTestingModule,
+        ComponentsModule,
+        BsDropdownModule.forRoot(),
+        RouterTestingModule
+      ],
+      declarations: [
+        HostsComponent
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(HostsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..28a193f
--- /dev/null
@@ -0,0 +1,64 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { HostService } from '../../../shared/services/host.service';
+
+@Component({
+  selector: 'cd-hosts',
+  templateUrl: './hosts.component.html',
+  styleUrls: ['./hosts.component.scss']
+})
+export class HostsComponent implements OnInit {
+
+  columns: Array<CdTableColumn> = [];
+  hosts: Array<object> = [];
+  isLoadingHosts = false;
+
+  @ViewChild('servicesTpl') public servicesTpl: TemplateRef<any>;
+
+  constructor(private hostService: HostService,
+              private cephShortVersionPipe: CephShortVersionPipe) { }
+
+  ngOnInit() {
+    this.columns = [
+      {
+        name: 'Hostname',
+        prop: 'hostname',
+        flexGrow: 1
+      },
+      {
+        name: 'Services',
+        prop: 'services',
+        flexGrow: 3,
+        cellTemplate: this.servicesTpl
+      },
+      {
+        name: 'Version',
+        prop: 'ceph_version',
+        flexGrow: 1,
+        pipe: this.cephShortVersionPipe
+      }
+    ];
+  }
+
+  getHosts() {
+    if (this.isLoadingHosts) {
+      return;
+    }
+    this.isLoadingHosts = true;
+    this.hostService.list().then((resp) => {
+      resp.map((host) => {
+        host.services.map((service) => {
+          service.cdLink = `/perf_counters/${service.type}/${service.id}`;
+          return service;
+        });
+        return host;
+      });
+      this.hosts = resp;
+      this.isLoadingHosts = false;
+    }).catch(() => {
+      this.isLoadingHosts = false;
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.spec.ts
new file mode 100644 (file)
index 0000000..1d5f7de
--- /dev/null
@@ -0,0 +1,21 @@
+import { HttpClientModule } from '@angular/common/http';
+import {
+  HttpClientTestingModule,
+  HttpTestingController
+} from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { MonitorService } from './monitor.service';
+
+describe('MonitorService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [MonitorService],
+      imports: [HttpClientTestingModule, HttpClientModule]
+    });
+  });
+
+  it('should be created', inject([MonitorService], (service: MonitorService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.ts
new file mode 100644 (file)
index 0000000..32057f3
--- /dev/null
@@ -0,0 +1,11 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class MonitorService {
+  constructor(private http: HttpClient) {}
+
+  getMonitor() {
+    return this.http.get('api/monitor');
+  }
+}
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 (file)
index 0000000..d59de84
--- /dev/null
@@ -0,0 +1,72 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item">Cluster</li>
+    <li i18n
+        class="breadcrumb-item active"
+        aria-current="page">Monitors</li>
+  </ol>
+</nav>
+
+<div class="row">
+  <div class="col-md-4">
+    <fieldset>
+      <legend i18n>Status</legend>
+      <table class="table table-striped"
+             *ngIf="mon_status">
+        <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 }}</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>
+      </table>
+    </fieldset>
+  </div>
+
+  <div class="col-md-8">
+    <fieldset>
+      <legend i18n
+              class="in-quorum">In Quorum</legend>
+      <cd-table [data]="inQuorum.data"
+                [columns]="inQuorum.columns">
+      </cd-table>
+
+      <legend i18n
+              class="in-quorum">Not In Quorum</legend>
+      <cd-table [data]="notInQuorum.data"
+                (fetchData)="refresh()"
+                [columns]="notInQuorum.columns">
+      </cd-table>
+    </fieldset>
+  </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 (file)
index 0000000..e69de29
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 (file)
index 0000000..906581e
--- /dev/null
@@ -0,0 +1,23 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppModule } from '../../../app.module';
+import { MonitorComponent } from './monitor.component';
+
+describe('MonitorComponent', () => {
+  let component: MonitorComponent;
+  let fixture: ComponentFixture<MonitorComponent>;
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        imports: [AppModule]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(MonitorComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+});
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 (file)
index 0000000..0a23129
--- /dev/null
@@ -0,0 +1,66 @@
+import { Component } from '@angular/core';
+
+import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { MonitorService } from '../monitor.service';
+
+@Component({
+  selector: 'cd-monitor',
+  templateUrl: './monitor.component.html',
+  styleUrls: ['./monitor.component.scss']
+})
+export class MonitorComponent {
+
+  mon_status: any;
+  inQuorum: any;
+  notInQuorum: any;
+
+  interval: any;
+  sparklineStyle = {
+    height: '30px',
+    width: '50%'
+  };
+
+  constructor(private monitorService: MonitorService) {
+    this.inQuorum = {
+      columns: [
+        { prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
+        { prop: 'rank', name: 'Rank' },
+        { prop: 'public_addr', name: 'Public Address' },
+        {
+          prop: 'cdOpenSessions',
+          name: 'Open Sessions',
+          cellTransformation: CellTemplate.sparkline
+        }
+      ],
+      data: []
+    };
+
+    this.notInQuorum = {
+      columns: [
+        { prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
+        { prop: 'rank', name: 'Rank' },
+        { prop: 'public_addr', name: 'Public Address' }
+      ],
+      data: []
+    };
+  }
+
+  refresh() {
+    this.monitorService.getMonitor().subscribe((data: any) => {
+      data.in_quorum.map((row) => {
+        row.cdOpenSessions = row.stats.num_sessions.map(i => i[1]);
+        row.cdLink = '/perf_counters/mon/' + row.name;
+        return row;
+      });
+
+      data.out_quorum.map((row) => {
+        row.cdLink = '/perf_counters/mon/' + row.name;
+        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-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 (file)
index 0000000..c511d54
--- /dev/null
@@ -0,0 +1,36 @@
+<tabset *ngIf="selection.hasSingleSelection">
+  <tab heading="Attributes (OSD map)">
+    <cd-table-key-value *ngIf="osd.loaded"
+                        [data]="osd.details.osd_map">
+    </cd-table-key-value>
+  </tab>
+  <tab heading="Metadata">
+    <cd-table-key-value *ngIf="osd.loaded"
+                        (fetchData)="osd.autoRefresh()"
+                        [data]="osd.details.osd_metadata">
+    </cd-table-key-value>
+  </tab>
+  <tab heading="Performance counter">
+    <cd-table-performance-counter *ngIf="osd.loaded"
+                                  serviceType="osd"
+                                  [serviceId]="osd.id">
+    </cd-table-performance-counter>
+  </tab>
+  <tab heading="Histogram">
+    <h3 *ngIf="osd.loaded && osd.histogram_failed">
+      Histogram not available -> <span class="text-warning">{{ osd.histogram_failed }}</span>
+    </h3>
+    <div class="row" *ngIf="osd.loaded && osd.details.histogram">
+      <div class="col-md-6">
+        <h4>Writes</h4>
+        <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_w_latency_in_bytes_histogram">
+        </cd-osd-performance-histogram>
+      </div>
+      <div class="col-md-6">
+        <h4>Reads</h4>
+        <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_r_latency_out_bytes_histogram">
+        </cd-osd-performance-histogram>
+      </div>
+    </div>
+  </tab>
+</tabset>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..c246182
--- /dev/null
@@ -0,0 +1,48 @@
+import { HttpClientModule } from '@angular/common/http';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap';
+
+import { DataTableModule } from '../../../../shared/datatable/datatable.module';
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
+import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
+import {
+  OsdPerformanceHistogramComponent
+} from '../osd-performance-histogram/osd-performance-histogram.component';
+import { OsdService } from '../osd.service';
+import { OsdDetailsComponent } from './osd-details.component';
+
+describe('OsdDetailsComponent', () => {
+  let component: OsdDetailsComponent;
+  let fixture: ComponentFixture<OsdDetailsComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        HttpClientModule,
+        TabsModule.forRoot(),
+        PerformanceCounterModule,
+        DataTableModule
+      ],
+      declarations: [
+        OsdDetailsComponent,
+        OsdPerformanceHistogramComponent
+      ],
+      providers: [OsdService]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(OsdDetailsComponent);
+    component = fixture.componentInstance;
+
+    component.selection = new CdTableSelection();
+
+    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 (file)
index 0000000..7f2af37
--- /dev/null
@@ -0,0 +1,44 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
+import { OsdService } from '../osd.service';
+
+@Component({
+  selector: 'cd-osd-details',
+  templateUrl: './osd-details.component.html',
+  styleUrls: ['./osd-details.component.scss']
+})
+export class OsdDetailsComponent implements OnChanges {
+  @Input() selection: CdTableSelection;
+
+  osd: any;
+
+  constructor(private osdService: OsdService) {}
+
+  ngOnChanges() {
+    this.osd = {
+      loaded: false
+    };
+    if (this.selection.hasSelection) {
+      this.osd = this.selection.first();
+      this.osd.autoRefresh = () => {
+        this.refresh();
+      };
+      this.refresh();
+    }
+  }
+
+  refresh() {
+    this.osdService.getDetails(this.osd.tree.id)
+      .subscribe((data: any) => {
+        this.osd.details = data;
+        if (!_.isObject(data.histogram)) {
+          this.osd.histogram_failed = data.histogram;
+          this.osd.details.histogram = undefined;
+        }
+        this.osd.loaded = true;
+      });
+  }
+}
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 (file)
index 0000000..2683102
--- /dev/null
@@ -0,0 +1,25 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li class="breadcrumb-item">Cluster</li>
+    <li class="breadcrumb-item active">OSDs</li>
+  </ol>
+</nav>
+<cd-table [data]="osds"
+          (fetchData)="getOsdList()"
+          [columns]="columns"
+          selectionType="single"
+          (updateSelection)="updateSelection($event)">
+  <cd-osd-details cdTableDetail
+                  [selection]="selection">
+  </cd-osd-details>
+</cd-table>
+
+<ng-template #statusColor
+             let-value="value">
+  <span *ngFor="let state of value; last as last">
+    <span [class.text-success]="'up' === state || 'in' === state"
+          [class.text-warning]="'down' === state || 'out' === state">
+      {{ state }}</span><span *ngIf="!last">, </span>
+    <!-- Has to be on the same line to prevent a space between state and comma. -->
+  </span>
+</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 (file)
index 0000000..e69de29
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 (file)
index 0000000..506b536
--- /dev/null
@@ -0,0 +1,47 @@
+import { HttpClientModule } from '@angular/common/http';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { DataTableModule } from '../../../../shared/datatable/datatable.module';
+import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe';
+import { FormatterService } from '../../../../shared/services/formatter.service';
+import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
+import { OsdDetailsComponent } from '../osd-details/osd-details.component';
+import {
+  OsdPerformanceHistogramComponent
+} from '../osd-performance-histogram/osd-performance-histogram.component';
+import { OsdService } from '../osd.service';
+import { OsdListComponent } from './osd-list.component';
+
+describe('OsdListComponent', () => {
+  let component: OsdListComponent;
+  let fixture: ComponentFixture<OsdListComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        HttpClientModule,
+        PerformanceCounterModule,
+        TabsModule.forRoot(),
+        DataTableModule
+      ],
+      declarations: [
+        OsdListComponent,
+        OsdDetailsComponent,
+        OsdPerformanceHistogramComponent
+      ],
+      providers: [OsdService, DimlessPipe, FormatterService]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(OsdListComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..29f0f22
--- /dev/null
@@ -0,0 +1,75 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { CellTemplate } from '../../../../shared/enum/cell-template.enum';
+import { CdTableColumn } from '../../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
+import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe';
+import { OsdService } from '../osd.service';
+
+@Component({
+  selector: 'cd-osd-list',
+  templateUrl: './osd-list.component.html',
+  styleUrls: ['./osd-list.component.scss']
+})
+
+export class OsdListComponent implements OnInit {
+  @ViewChild('statusColor') statusColor: TemplateRef<any>;
+
+  osds = [];
+  columns: CdTableColumn[];
+  selection = new CdTableSelection();
+
+  constructor(
+    private osdService: OsdService,
+    private dimlessPipe: DimlessPipe
+  ) { }
+
+  ngOnInit() {
+    this.columns = [
+      {prop: 'host.name', name: 'Host'},
+      {prop: 'id', name: 'ID', cellTransformation: CellTemplate.bold},
+      {prop: 'collectedStates', name: 'Status', cellTemplate: this.statusColor},
+      {prop: 'stats.numpg', name: 'PGs'},
+      {prop: 'usedPercent', name: 'Usage'},
+      {
+        prop: 'stats_history.out_bytes',
+        name: 'Read bytes',
+        cellTransformation: CellTemplate.sparkline
+      },
+      {
+        prop: 'stats_history.in_bytes',
+        name: 'Writes bytes',
+        cellTransformation: CellTemplate.sparkline
+      },
+      {prop: 'stats.op_r', name: 'Read ops', cellTransformation: CellTemplate.perSecond},
+      {prop: 'stats.op_w', name: 'Write ops', cellTransformation: CellTemplate.perSecond}
+    ];
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+
+  getOsdList() {
+    this.osdService.getList().subscribe((data: any[]) => {
+      this.osds = data;
+      data.map((osd) => {
+        osd.collectedStates = this.collectStates(osd);
+        osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map(i => i[1]);
+        osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map(i => i[1]);
+        osd.usedPercent = this.dimlessPipe.transform(osd.stats.stat_bytes_used) + ' / ' +
+          this.dimlessPipe.transform(osd.stats.stat_bytes);
+        return osd;
+      });
+    });
+  }
+
+  collectStates(osd) {
+    const select = (onState, offState) => osd[onState] ? onState : offState;
+    return [select('up', 'down'), select('in', 'out')];
+  }
+
+  beforeShowDetails(selection: CdTableSelection) {
+    return selection.hasSingleSelection;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html
new file mode 100644 (file)
index 0000000..080f121
--- /dev/null
@@ -0,0 +1,9 @@
+<table>
+  <tr style="height: 10px;"
+      *ngFor="let row of valuesStyle">
+    <td style="width: 10px; height: 10px;"
+        *ngFor="let col of row"
+        [ngStyle]="col">
+    </td>
+  </tr>
+</table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts
new file mode 100644 (file)
index 0000000..7ff7d64
--- /dev/null
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { OsdPerformanceHistogramComponent } from './osd-performance-histogram.component';
+
+describe('OsdPerformanceHistogramComponent', () => {
+  let component: OsdPerformanceHistogramComponent;
+  let fixture: ComponentFixture<OsdPerformanceHistogramComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ OsdPerformanceHistogramComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(OsdPerformanceHistogramComponent);
+    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-performance-histogram/osd-performance-histogram.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts
new file mode 100644 (file)
index 0000000..c3f0645
--- /dev/null
@@ -0,0 +1,61 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import * as _ from 'lodash';
+
+@Component({
+  selector: 'cd-osd-performance-histogram',
+  templateUrl: './osd-performance-histogram.component.html',
+  styleUrls: ['./osd-performance-histogram.component.scss']
+})
+export class OsdPerformanceHistogramComponent implements OnChanges {
+  @Input() histogram: any;
+  valuesStyle: any;
+  last = {};
+
+  constructor() { }
+
+  ngOnChanges() {
+    this.render();
+  }
+
+  hexdigits(v): string {
+    const i = Math.floor(v * 255).toString(16);
+    return i.length === 1 ? '0' + i : i;
+  }
+
+  hexcolor(r, g, b) {
+    return '#' + this.hexdigits(r) + this.hexdigits(g) + this.hexdigits(b);
+  }
+
+  render() {
+    if (!this.histogram) {
+      return;
+    }
+    let sum = 0;
+    let max = 0;
+
+    _.each(this.histogram.values, (row, i) => {
+      _.each(row, (col, j) => {
+        let val;
+        if (this.last && this.last[i] && this.last[i][j]) {
+          val = col - this.last[i][j];
+        } else {
+          val = col;
+        }
+        sum += val;
+        max = Math.max(max, val);
+      });
+    });
+
+    this.valuesStyle = this.histogram.values.map((row, i) => {
+      return row.map((col, j) => {
+        const val = this.last && this.last[i] && this.last[i][j] ? col - this.last[i][j] : col;
+        const g = max ? val / max : 0;
+        const r = 1 - g;
+        return {backgroundColor: this.hexcolor(r, g, 0)};
+      });
+    });
+
+    this.last = this.histogram.values;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts
new file mode 100644 (file)
index 0000000..115d6a4
--- /dev/null
@@ -0,0 +1,19 @@
+import { HttpClientModule } from '@angular/common/http';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { OsdService } from './osd.service';
+
+describe('OsdService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [OsdService],
+      imports: [
+        HttpClientModule,
+      ],
+    });
+  });
+
+  it('should be created', inject([OsdService], (service: OsdService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.ts
new file mode 100644 (file)
index 0000000..cf9adf1
--- /dev/null
@@ -0,0 +1,17 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class OsdService {
+  private path = 'api/osd';
+
+  constructor (private http: HttpClient) {}
+
+  getList () {
+    return this.http.get(`${this.path}`);
+  }
+
+  getDetails(id: number) {
+    return this.http.get(`${this.path}/${id}`);
+  }
+}
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 (file)
index 0000000..cf4c025
--- /dev/null
@@ -0,0 +1,37 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { ChartsModule } from 'ng2-charts';
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { SharedModule } from '../../shared/shared.module';
+import { DashboardService } from './dashboard.service';
+import { DashboardComponent } from './dashboard/dashboard.component';
+import { HealthPieComponent } from './health-pie/health-pie.component';
+import { HealthComponent } from './health/health.component';
+import { LogColorPipe } from './log-color.pipe';
+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 { PgStatusStylePipe } from './pg-status-style.pipe';
+import { PgStatusPipe } from './pg-status.pipe';
+
+@NgModule({
+  imports: [CommonModule, TabsModule.forRoot(), SharedModule, ChartsModule, RouterModule],
+  declarations: [
+    HealthComponent,
+    DashboardComponent,
+    MonSummaryPipe,
+    OsdSummaryPipe,
+    LogColorPipe,
+    MgrSummaryPipe,
+    PgStatusPipe,
+    MdsSummaryPipe,
+    PgStatusStylePipe,
+    HealthPieComponent
+  ],
+  providers: [DashboardService]
+})
+export class DashboardModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts
new file mode 100644 (file)
index 0000000..bf061e9
--- /dev/null
@@ -0,0 +1,23 @@
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { appendFile } from 'fs';
+
+import { DashboardService } from './dashboard.service';
+
+describe('DashboardService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [DashboardService],
+      imports: [HttpClientTestingModule, HttpClientModule]
+    });
+  });
+
+  it(
+    'should be created',
+    inject([DashboardService], (service: DashboardService) => {
+      expect(service).toBeTruthy();
+    })
+  );
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.ts
new file mode 100644 (file)
index 0000000..cb51cb4
--- /dev/null
@@ -0,0 +1,11 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class DashboardService {
+  constructor(private http: HttpClient) {}
+
+  getHealth() {
+    return this.http.get('api/dashboard/health');
+  }
+}
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 (file)
index 0000000..89a37fd
--- /dev/null
@@ -0,0 +1,12 @@
+<div>
+  <tabset *ngIf="hasGrafana">
+    <tab i18n-heading
+         heading="Health">
+      <cd-health></cd-health>
+    </tab>
+    <tab i18n-heading
+         heading="Statistics">
+    </tab>
+  </tabset>
+  <cd-health *ngIf="!hasGrafana"></cd-health>
+</div>
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 (file)
index 0000000..04eee2d
--- /dev/null
@@ -0,0 +1,3 @@
+div {
+  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 (file)
index 0000000..80500c0
--- /dev/null
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DashboardComponent } from './dashboard.component';
+
+describe('DashboardComponent', () => {
+  let component: DashboardComponent;
+  let fixture: ComponentFixture<DashboardComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ DashboardComponent ]
+    })
+    .compileComponents();
+  }));
+
+  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 (file)
index 0000000..fc676c7
--- /dev/null
@@ -0,0 +1,16 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'cd-dashboard',
+  templateUrl: './dashboard.component.html',
+  styleUrls: ['./dashboard.component.scss']
+})
+export class DashboardComponent implements OnInit {
+  hasGrafana = false; // TODO: Temporary var, remove when grafana is implemented
+
+  constructor() { }
+
+  ngOnInit() {
+  }
+
+}
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 (file)
index 0000000..7135f96
--- /dev/null
@@ -0,0 +1,15 @@
+<div class="chart-container">
+  <canvas baseChart
+          #chartCanvas
+          [datasets]="chart.dataset"
+          [chartType]="chart.chartType"
+          [options]="chart.options"
+          [labels]="chart.labels"
+          [colors]="chart.colors"
+          width="120"
+          height="120"></canvas>
+  <div class="chartjs-tooltip"
+       #chartTooltip>
+    <table></table>
+  </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 (file)
index 0000000..b3abf86
--- /dev/null
@@ -0,0 +1 @@
+@import '../../../../styles/chart-tooltip.scss';
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 (file)
index 0000000..dca539f
--- /dev/null
@@ -0,0 +1,30 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { HealthPieComponent } from './health-pie.component';
+
+describe('HealthPieComponent', () => {
+  let component: HealthPieComponent;
+  let fixture: ComponentFixture<HealthPieComponent>;
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        imports: [ChartsModule, SharedModule],
+        declarations: [HealthPieComponent]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(HealthPieComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..196d871
--- /dev/null
@@ -0,0 +1,117 @@
+import {
+  Component,
+  ElementRef,
+  EventEmitter,
+  Input,
+  OnChanges,
+  OnInit,
+  Output,
+  ViewChild
+} from '@angular/core';
+
+import * as Chart from 'chart.js';
+import * as _ from 'lodash';
+
+import { ChartTooltip } from '../../../shared/models/chart-tooltip';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+
+@Component({
+  selector: 'cd-health-pie',
+  templateUrl: './health-pie.component.html',
+  styleUrls: ['./health-pie.component.scss']
+})
+export class HealthPieComponent implements OnChanges, OnInit {
+  @ViewChild('chartCanvas') chartCanvasRef: ElementRef;
+  @ViewChild('chartTooltip') chartTooltipRef: ElementRef;
+
+  @Input() data: any;
+  @Input() tooltipFn: any;
+  @Output() prepareFn = new EventEmitter();
+
+  chart: any = {
+    chartType: 'doughnut',
+    dataset: [
+      {
+        label: null,
+        borderWidth: 0
+      }
+    ],
+    options: {
+      responsive: true,
+      legend: { display: false },
+      animation: { duration: 0 },
+
+      tooltips: {
+        enabled: false
+      }
+    },
+    colors: [
+      {
+        borderColor: 'transparent'
+      }
+    ]
+  };
+
+  constructor(private dimlessBinary: DimlessBinaryPipe) {}
+
+  ngOnInit() {
+    // An extension to Chart.js to enable rendering some
+    // text in the middle of a doughnut
+    Chart.pluginService.register({
+      beforeDraw: function(chart) {
+        if (!chart.options.center_text) {
+          return;
+        }
+
+        const width = chart.chart.width,
+          height = chart.chart.height,
+          ctx = chart.chart.ctx;
+
+        ctx.restore();
+        const fontSize = (height / 114).toFixed(2);
+        ctx.font = fontSize + 'em sans-serif';
+        ctx.textBaseline = 'middle';
+
+        const text = chart.options.center_text,
+          textX = Math.round((width - ctx.measureText(text).width) / 2),
+          textY = height / 2;
+
+        ctx.fillText(text, textX, textY);
+        ctx.save();
+      }
+    });
+
+    const getStyleTop = (tooltip, positionY) => {
+      return positionY + tooltip.caretY - tooltip.height - 10 + 'px';
+    };
+
+    const getStyleLeft = (tooltip, positionX) => {
+      return positionX + tooltip.caretX + 'px';
+    };
+
+    const getBody = (body) => {
+      const bodySplit = body[0].split(': ');
+      bodySplit[1] = this.dimlessBinary.transform(bodySplit[1]);
+      return bodySplit.join(': ');
+    };
+
+    const chartTooltip = new ChartTooltip(
+      this.chartCanvasRef,
+      this.chartTooltipRef,
+      getStyleLeft,
+      getStyleTop,
+    );
+    chartTooltip.getBody = getBody;
+
+    const self = this;
+    this.chart.options.tooltips.custom = (tooltip) => {
+      chartTooltip.customTooltips(tooltip);
+    };
+
+    this.prepareFn.emit([this.chart, this.data]);
+  }
+
+  ngOnChanges() {
+    this.prepareFn.emit([this.chart, this.data]);
+  }
+}
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 (file)
index 0000000..348324e
--- /dev/null
@@ -0,0 +1,209 @@
+<div *ngIf="contentData">
+  <div class="row">
+    <!-- HEALTH -->
+    <div class="col-md-6">
+      <div class="well">
+        <fieldset>
+          <legend i18n>Health</legend>
+          <ng-container i18n>Overall status:</ng-container>
+          <span [ngStyle]="contentData.health.status | healthColor">{{ contentData.health.status }}</span>
+          <ul>
+            <li *ngFor="let check of contentData.health.checks">
+              <span [ngStyle]="check.severity | healthColor">{{ check.type }}</span>: {{ check.summary.message }}
+            </li>
+          </ul>
+        </fieldset>
+      </div>
+    </div>
+
+    <div class="col-md-6">
+      <!--STATS -->
+      <div class="row">
+        <div class="col-md-6">
+          <div class="well">
+            <div class="media">
+              <div class="media-left">
+                <i class="fa fa-database fa-fw"></i>
+              </div>
+              <div class="media-body">
+                <span class="media-heading"
+                      i18n="ceph monitors">
+                  <a routerLink="/monitor/">Monitors</a>
+                </span>
+                <span class="media-text">{{ contentData.mon_status | monSummary }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="col-md-6">
+          <div class="well">
+            <div class="media">
+              <div class="media-left">
+                <i class="fa fa-hdd-o fa-fw"></i>
+              </div>
+              <div class="media-body">
+                <span class="media-heading"
+                      i18n="ceph OSDs">
+                  <a routerLink="/osd/">OSDs</a>
+                </span>
+                <span class="media-text">{{ contentData.osd_map | osdSummary }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-md-6">
+          <div class="well">
+            <div class="media">
+              <div class="media-left">
+                <i class="fa fa-folder fa-fw"></i>
+              </div>
+              <div class="media-body">
+                <span class="media-heading"
+                      i18n>Metadata servers</span>
+                <span class="media-text">{{ contentData.fs_map | mdsSummary }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="col-md-6">
+          <div class="well">
+            <div class="media">
+              <div class="media-left">
+                <i class="fa fa-cog fa-fw"></i>
+              </div>
+              <div class="media-body">
+                <span class="media-heading"
+                      i18n>Manager daemons</span>
+                <span class="media-text">{{ contentData.mgr_map | mgrSummary }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div class="row">
+    <!-- USAGE -->
+    <div class="col-md-6">
+      <div class="well">
+        <fieldset class="usage">
+          <legend i18n>Usage</legend>
+
+          <table class="ceph-chartbox">
+            <tr>
+              <td>
+                <span style="font-size: 45px;">{{ contentData.df.stats.total_objects | dimless }}</span>
+              </td>
+              <td>
+                <div class="center-block pie">
+                  <cd-health-pie [data]="contentData"
+                                 (prepareFn)="prepareRawUsage($event[0], $event[1])"></cd-health-pie>
+                </div>
+              </td>
+              <td>
+                <div class="center-block pie">
+                  <cd-health-pie [data]="contentData"
+                                 (prepareFn)="preparePoolUsage($event[0], $event[1])"></cd-health-pie>
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <td i18n>Objects</td>
+              <td>
+                <ng-container i18n>Raw capacity</ng-container>
+                <br>
+                <ng-container i18n="disk used">({{ contentData.df.stats.total_used_bytes | dimlessBinary }} used)</ng-container>
+              </td>
+              <td i18n>Usage by pool</td>
+            </tr>
+          </table>
+        </fieldset>
+      </div>
+    </div>
+
+    <div class="col-md-6">
+      <div class="well">
+        <fieldset>
+          <legend i18n>Pools</legend>
+          <table class="table table-condensed">
+            <thead>
+              <tr>
+                <th i18n>Name</th>
+                <th i18n>PG status</th>
+                <th i18n>Usage</th>
+                <th colspan="2"
+                    i18n>Read</th>
+                <th colspan="2"
+                    i18n>Write</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr *ngFor="let pool of contentData.pools">
+                <td>{{ pool.pool_name }}</td>
+                <td [ngStyle]="pool.pg_status | pgStatusStyle">
+                  {{ pool.pg_status | pgStatus }}
+                </td>
+                <td>
+                  {{ pool.stats.bytes_used.latest | dimlessBinary }} / {{ pool.stats.max_avail.latest | dimlessBinary }}
+                </td>
+                <td>
+                  {{ pool.stats.rd_bytes.rate | dimless }}
+                </td>
+                <td>
+                  {{ pool.stats.rd.rate | dimless }} ops
+                </td>
+                <td>
+                  {{ pool.stats.wr_bytes.rate | dimless }}
+                </td>
+                <td>
+                  {{ pool.stats.wr.rate | dimless }} ops
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </fieldset>
+      </div>
+    </div>
+  </div>
+
+  <div class="row">
+    <div class="col-md-12">
+      <!-- LOGS -->
+      <div class="well">
+        <fieldset>
+          <legend i18n>Logs</legend>
+
+          <tabset>
+            <tab heading="Cluster log"
+                 class="text-monospace"
+                 i18n-heading>
+              <span *ngFor="let line of contentData.clog">
+                {{ line.stamp }}&nbsp;{{ line.priority }}&nbsp;
+                <span [ngStyle]="line | logColor">
+                  {{ line.message }}
+                  <br>
+                </span>
+              </span>
+            </tab>
+            <tab heading="Audit log"
+                 class="text-monospace"
+                 i18n-heading>
+              <span *ngFor="let line of contentData.audit_log">
+                {{ line.stamp }}&nbsp;{{ line.priority }}&nbsp;
+                <span [ngStyle]="line | logColor">
+                  <span style="font-weight: bold;">
+                    {{ line.message }}
+                  </span>
+                  <br>
+                </span>
+              </span>
+            </tab>
+          </tabset>
+        </fieldset>
+      </div>
+    </div>
+  </div>
+</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 (file)
index 0000000..919b41d
--- /dev/null
@@ -0,0 +1,62 @@
+table.ceph-chartbox {
+  width: 100%;
+
+  td {
+    text-align: center;
+    font-weight: bold;
+  }
+}
+
+.center-block {
+  width: 120px;
+}
+
+.pie {
+  height: 120px;
+  width: 120px;
+}
+
+.media {
+  display: block;
+  min-height: 60px;
+  width: 100%;
+
+  .media-left {
+    border-top-left-radius: 2px;
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 2px;
+    display: block;
+    float: left;
+    height: 60px;
+    width: 60px;
+    text-align: center;
+    font-size: 40px;
+    line-height: 60px;
+    padding-right: 0;
+
+    .fa {
+      font-size: 45px;
+    }
+  }
+
+  .media-body {
+    padding: 5px 10px;
+    margin-left: 60px;
+
+    .media-heading {
+      text-transform: uppercase;
+      display: block;
+      font-size: 14px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+
+    .media-text {
+      display: block;
+      font-weight: bold;
+      font-size: 18px;
+    }
+  }
+}
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 (file)
index 0000000..983b145
--- /dev/null
@@ -0,0 +1,35 @@
+import { HttpClientModule } from '@angular/common/http';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { DashboardService } from '../dashboard.service';
+import { HealthComponent } from './health.component';
+
+describe('HealthComponent', () => {
+  let component: HealthComponent;
+  let fixture: ComponentFixture<HealthComponent>;
+
+  const fakeService = {
+    getHealth() {
+      return {};
+    }
+  };
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        providers: [{ provide: DashboardService, useValue: fakeService }],
+        imports: [SharedModule],
+        declarations: [HealthComponent]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(HealthComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+});
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 (file)
index 0000000..3cdddc9
--- /dev/null
@@ -0,0 +1,99 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { DashboardService } from '../dashboard.service';
+
+@Component({
+  selector: 'cd-health',
+  templateUrl: './health.component.html',
+  styleUrls: ['./health.component.scss']
+})
+export class HealthComponent implements OnInit, OnDestroy {
+  contentData: any;
+  interval: number;
+
+  constructor(private dashboardService: DashboardService) {}
+
+  ngOnInit() {
+    this.getInfo();
+    this.interval = window.setInterval(() => {
+      this.getInfo();
+    }, 5000);
+  }
+
+  ngOnDestroy() {
+    clearInterval(this.interval);
+  }
+
+  getInfo() {
+    this.dashboardService.getHealth().subscribe((data: any) => {
+      this.contentData = data;
+    });
+  }
+
+  prepareRawUsage(chart, data) {
+    let rawUsageChartColor;
+
+    const rawUsageText =
+      Math.round(100 * (data.df.stats.total_used_bytes / data.df.stats.total_bytes)) + '%';
+
+    if (data.df.stats.total_used_bytes / data.df.stats.total_bytes >= data.osd_map.full_ratio) {
+      rawUsageChartColor = '#ff0000';
+    } else if (
+      data.df.stats.total_used_bytes / data.df.stats.total_bytes >=
+      data.osd_map.backfillfull_ratio
+    ) {
+      rawUsageChartColor = '#ff6600';
+    } else if (
+      data.df.stats.total_used_bytes / data.df.stats.total_bytes >=
+      data.osd_map.nearfull_ratio
+    ) {
+      rawUsageChartColor = '#ffc200';
+    } else {
+      rawUsageChartColor = '#00bb00';
+    }
+
+    chart.dataset[0].data = [data.df.stats.total_used_bytes, data.df.stats.total_avail_bytes];
+    chart.options.center_text = rawUsageText;
+    chart.colors = [{ backgroundColor: [rawUsageChartColor, '#424d52'] }];
+    chart.labels = ['Raw Used', 'Raw Available'];
+  }
+
+  preparePoolUsage(chart, data) {
+    const colors = [
+      '#3366CC',
+      '#109618',
+      '#990099',
+      '#3B3EAC',
+      '#0099C6',
+      '#DD4477',
+      '#66AA00',
+      '#B82E2E',
+      '#316395',
+      '#994499',
+      '#22AA99',
+      '#AAAA11',
+      '#6633CC',
+      '#E67300',
+      '#8B0707',
+      '#329262',
+      '#5574A6',
+      '#FF9900',
+      '#DC3912',
+      '#3B3EAC'
+    ];
+
+    const poolLabels = [];
+    const poolData = [];
+
+    _.each(data.df.pools, (pool, i) => {
+      poolLabels.push(pool['name']);
+      poolData.push(pool['stats']['bytes_used']);
+    });
+
+    chart.dataset[0].data = poolData;
+    chart.colors = [{ backgroundColor: colors }];
+    chart.labels = poolLabels;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts
new file mode 100644 (file)
index 0000000..43af68d
--- /dev/null
@@ -0,0 +1,8 @@
+import { LogColorPipe } from './log-color.pipe';
+
+describe('LogColorPipe', () => {
+  it('create an instance', () => {
+    const pipe = new LogColorPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.ts
new file mode 100644 (file)
index 0000000..eb60ddb
--- /dev/null
@@ -0,0 +1,21 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'logColor'
+})
+export class LogColorPipe implements PipeTransform {
+  transform(value: any, args?: any): any {
+    if (value.priority === '[INF]') {
+      return ''; // Inherit
+    } else if (value.priority === '[WRN]') {
+      return {
+        color: '#ffa500',
+        'font-weight': 'bold'
+      };
+    } else if (value.priority === '[ERR]') {
+      return { color: '#FF2222' };
+    } else {
+      return '';
+    }
+  }
+}
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 (file)
index 0000000..37883a8
--- /dev/null
@@ -0,0 +1,8 @@
+import { MdsSummaryPipe } from './mds-summary.pipe';
+
+describe('MdsSummaryPipe', () => {
+  it('create an instance', () => {
+    const pipe = new MdsSummaryPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..9e6eeca
--- /dev/null
@@ -0,0 +1,38 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+  name: 'mdsSummary'
+})
+export class MdsSummaryPipe implements PipeTransform {
+  transform(value: any, args?: any): any {
+    if (!value) {
+      return '';
+    }
+
+    let standbys = 0;
+    let active = 0;
+    let standbyReplay = 0;
+    _.each(value.standbys, (s, i) => {
+      standbys += 1;
+    });
+
+    if (value.standbys && !value.filesystems) {
+      return standbys + ', no filesystems';
+    } else if (value.filesystems.length === 0) {
+      return 'no filesystems';
+    } else {
+      _.each(value.filesystems, (fs, i) => {
+        _.each(fs.mdsmap.info, (mds, j) => {
+          if (mds.state === 'up:standby-replay') {
+            standbyReplay += 1;
+          } else {
+            active += 1;
+          }
+        });
+      });
+
+      return active + ' active, ' + (standbys + standbyReplay) + ' standby';
+    }
+  }
+}
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 (file)
index 0000000..fdab76c
--- /dev/null
@@ -0,0 +1,8 @@
+import { MgrSummaryPipe } from './mgr-summary.pipe';
+
+describe('MgrSummaryPipe', () => {
+  it('create an instance', () => {
+    const pipe = new MgrSummaryPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..cf793e6
--- /dev/null
@@ -0,0 +1,22 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+  name: 'mgrSummary'
+})
+export class MgrSummaryPipe implements PipeTransform {
+  transform(value: any, args?: any): any {
+    if (!value) {
+      return '';
+    }
+
+    let result = 'active: ';
+    result += _.isUndefined(value.active_name) ? 'n/a' : value.active_name;
+
+    if (value.standbys.length) {
+      result += ', ' + value.standbys.length + ' standbys';
+    }
+
+    return result;
+  }
+}
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 (file)
index 0000000..49526cf
--- /dev/null
@@ -0,0 +1,8 @@
+import { MonSummaryPipe } from './mon-summary.pipe';
+
+describe('MonSummaryPipe', () => {
+  it('create an instance', () => {
+    const pipe = new MonSummaryPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..6877e22
--- /dev/null
@@ -0,0 +1,18 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'monSummary'
+})
+export class MonSummaryPipe implements PipeTransform {
+  transform(value: any, args?: any): any {
+    if (!value) {
+      return '';
+    }
+
+    let result = value.monmap.mons.length.toString() + ' (quorum ';
+    result += value.quorum.join(', ');
+    result += ')';
+
+    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 (file)
index 0000000..466eec1
--- /dev/null
@@ -0,0 +1,8 @@
+import { OsdSummaryPipe } from './osd-summary.pipe';
+
+describe('OsdSummaryPipe', () => {
+  it('create an instance', () => {
+    const pipe = new OsdSummaryPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..b02d976
--- /dev/null
@@ -0,0 +1,26 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+  name: 'osdSummary'
+})
+export class OsdSummaryPipe implements PipeTransform {
+  transform(value: any, args?: any): any {
+    if (!value) {
+      return '';
+    }
+
+    let inCount = 0;
+    let upCount = 0;
+    _.each(value.osds, (osd, i) => {
+      if (osd.in) {
+        inCount++;
+      }
+      if (osd.up) {
+        upCount++;
+      }
+    });
+
+    return value.osds.length + ' (' + upCount + ' up, ' + inCount + ' in)';
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts
new file mode 100644 (file)
index 0000000..67c5f10
--- /dev/null
@@ -0,0 +1,8 @@
+import { PgStatusStylePipe } from './pg-status-style.pipe';
+
+describe('PgStatusStylePipe', () => {
+  it('create an instance', () => {
+    const pipe = new PgStatusStylePipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts
new file mode 100644 (file)
index 0000000..4e9afab
--- /dev/null
@@ -0,0 +1,40 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+  name: 'pgStatusStyle'
+})
+export class PgStatusStylePipe implements PipeTransform {
+  transform(pgStatus: any, args?: any): any {
+    let warning = false;
+    let error = false;
+
+    _.each(pgStatus, (value, state) => {
+      if (
+        state.includes('inconsistent') ||
+        state.includes('incomplete') ||
+        !state.includes('active')
+      ) {
+        error = true;
+      }
+
+      if (
+        state !== 'active+clean' &&
+        state !== 'active+clean+scrubbing' &&
+        state !== 'active+clean+scrubbing+deep'
+      ) {
+        warning = true;
+      }
+    });
+
+    if (error) {
+      return { color: '#FF0000' };
+    }
+
+    if (warning) {
+      return { color: '#FFC200' };
+    }
+
+    return { color: '#00BB00' };
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts
new file mode 100644 (file)
index 0000000..d7d5592
--- /dev/null
@@ -0,0 +1,8 @@
+import { PgStatusPipe } from './pg-status.pipe';
+
+describe('PgStatusPipe', () => {
+  it('create an instance', () => {
+    const pipe = new PgStatusPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts
new file mode 100644 (file)
index 0000000..5c6c7b3
--- /dev/null
@@ -0,0 +1,16 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+  name: 'pgStatus'
+})
+export class PgStatusPipe implements PipeTransform {
+  transform(pgStatus: any, args?: any): any {
+    const strings = [];
+    _.each(pgStatus, (count, state) => {
+      strings.push(count + ' ' + state);
+    });
+
+    return strings.join(', ');
+  }
+}
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 (file)
index 0000000..f2c2029
--- /dev/null
@@ -0,0 +1,31 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { SharedModule } from '../../shared/shared.module';
+import {
+  PerformanceCounterComponent
+} from './performance-counter/performance-counter.component';
+import { TablePerformanceCounterService } from './services/table-performance-counter.service';
+import {
+  TablePerformanceCounterComponent
+} from './table-performance-counter/table-performance-counter.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    SharedModule,
+    RouterModule
+  ],
+  declarations: [
+    TablePerformanceCounterComponent,
+    PerformanceCounterComponent
+  ],
+  providers: [
+    TablePerformanceCounterService
+  ],
+  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 (file)
index 0000000..ebb9ba9
--- /dev/null
@@ -0,0 +1,7 @@
+<fieldset>
+  <legend i18n>Performance Counters</legend>
+  <h3>{{ serviceType }}.{{ serviceId }}</h3>
+  <cd-table-performance-counter [serviceType]="serviceType"
+                                [serviceId]="serviceId">
+  </cd-table-performance-counter>
+</fieldset>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..a4cc717
--- /dev/null
@@ -0,0 +1,49 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+
+import { PerformanceCounterModule } from '../performance-counter.module';
+import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
+import { PerformanceCounterComponent } from './performance-counter.component';
+
+describe('PerformanceCounterComponent', () => {
+  let component: PerformanceCounterComponent;
+  let fixture: ComponentFixture<PerformanceCounterComponent>;
+
+  const fakeService = {
+    get: (service_type: string, service_id: string) => {
+      return new Promise(function(resolve, reject) {
+        return [];
+      });
+    },
+    list: () => {
+      return new Promise(function(resolve, reject) {
+        return {};
+      });
+    }
+  };
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        imports: [
+          PerformanceCounterModule,
+          BsDropdownModule.forRoot(),
+          RouterTestingModule
+        ],
+        providers: [{ provide: TablePerformanceCounterService, useValue: fakeService }]
+      }).compileComponents();
+    })
+  );
+
+  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 (file)
index 0000000..25fa82e
--- /dev/null
@@ -0,0 +1,26 @@
+import { Component, OnDestroy } 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 implements OnDestroy {
+  serviceId: string;
+  serviceType: string;
+  routeParamsSubscribe: any;
+
+  constructor(private route: ActivatedRoute) {
+    this.routeParamsSubscribe = this.route.params.subscribe(
+      (params: { type: string; id: string }) => {
+        this.serviceId = params.id;
+        this.serviceType = params.type;
+      }
+    );
+  }
+
+  ngOnDestroy() {
+    this.routeParamsSubscribe.unsubscribe();
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts
new file mode 100644 (file)
index 0000000..6f0af94
--- /dev/null
@@ -0,0 +1,27 @@
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+
+import { TablePerformanceCounterService } from './table-performance-counter.service';
+
+describe('TablePerformanceCounterService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [TablePerformanceCounterService],
+      imports: [
+        HttpClientTestingModule,
+        BsDropdownModule.forRoot(),
+        HttpClientModule
+      ]
+    });
+  });
+
+  it(
+    'should be created',
+    inject([TablePerformanceCounterService], (service: TablePerformanceCounterService) => {
+      expect(service).toBeTruthy();
+    })
+  );
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts
new file mode 100644 (file)
index 0000000..b6ac5d5
--- /dev/null
@@ -0,0 +1,28 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class TablePerformanceCounterService {
+
+  private url = 'api/perf_counters';
+
+  constructor(private http: HttpClient) { }
+
+  list() {
+    return this.http.get(this.url)
+      .toPromise()
+      .then((resp: object): object => {
+        return resp;
+      });
+  }
+
+  get(service_type: string, service_id: string) {
+    const serviceType = service_type.replace('-', '_');
+
+    return this.http.get(`${this.url}/${serviceType}/${service_id}`)
+      .toPromise()
+      .then((resp: object): Array<object> => {
+        return resp['counters'];
+      });
+  }
+}
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 (file)
index 0000000..6564dc1
--- /dev/null
@@ -0,0 +1,8 @@
+<cd-table [data]="counters"
+          [columns]="columns"
+          columnMode="flex"
+          (fetchData)="getCounters()">
+  <ng-template #valueTpl let-row="row">
+    {{ row.value | dimless }} {{ row.unit }}
+  </ng-template>
+</cd-table>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..4baefe8
--- /dev/null
@@ -0,0 +1,38 @@
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
+import { TablePerformanceCounterComponent } from './table-performance-counter.component';
+
+describe('TablePerformanceCounterComponent', () => {
+  let component: TablePerformanceCounterComponent;
+  let fixture: ComponentFixture<TablePerformanceCounterComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ TablePerformanceCounterComponent ],
+      imports: [
+        HttpClientTestingModule,
+        HttpClientModule,
+        BsDropdownModule.forRoot(),
+        SharedModule
+      ],
+      providers: [ TablePerformanceCounterService ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(TablePerformanceCounterComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..6ac05c9
--- /dev/null
@@ -0,0 +1,59 @@
+import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
+
+/**
+ * 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: TablePerformanceCounterService) { }
+
+  ngOnInit() {
+    this.columns = [
+      {
+        name: 'Name',
+        prop: 'name',
+        flexGrow: 1
+      },
+      {
+        name: 'Description',
+        prop: 'description',
+        flexGrow: 1
+      },
+      {
+        name: 'Value',
+        cellTemplate: this.valueTpl,
+        flexGrow: 1
+      }
+    ];
+  }
+
+  getCounters() {
+    this.performanceCounterService.get(this.serviceType, this.serviceId)
+      .then((resp) => {
+        this.counters = resp;
+      });
+  }
+}
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 (file)
index 0000000..81c5919
--- /dev/null
@@ -0,0 +1,14 @@
+<tabset *ngIf="selection.hasSingleSelection">
+  <tab i18n-heading
+       heading="Details">
+    <cd-table-key-value [data]="metadata"
+                        (fetchData)="getMetaData()">
+    </cd-table-key-value>
+  </tab>
+  <tab i18n-heading
+       heading="Performance Counters">
+    <cd-table-performance-counter serviceType="rgw"
+                                  [serviceId]="serviceId">
+    </cd-table-performance-counter>
+  </tab>
+</tabset>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..afce56a
--- /dev/null
@@ -0,0 +1,47 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { SharedModule } from '../../../shared/shared.module';
+import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
+import { RgwDaemonService } from '../services/rgw-daemon.service';
+import { RgwDaemonDetailsComponent } from './rgw-daemon-details.component';
+
+describe('RgwDaemonDetailsComponent', () => {
+  let component: RgwDaemonDetailsComponent;
+  let fixture: ComponentFixture<RgwDaemonDetailsComponent>;
+
+  const fakeService = {
+    get: (id: string) => {
+      return new Promise(function(resolve, reject) {
+        return [];
+      });
+    }
+  };
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ RgwDaemonDetailsComponent ],
+      imports: [
+        SharedModule,
+        PerformanceCounterModule,
+        TabsModule.forRoot()
+      ],
+      providers: [{ provide: RgwDaemonService, useValue: fakeService }]
+    });
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwDaemonDetailsComponent);
+    component = fixture.componentInstance;
+
+    component.selection = new CdTableSelection();
+
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..8ac62fa
--- /dev/null
@@ -0,0 +1,36 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { RgwDaemonService } from '../services/rgw-daemon.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 = '';
+
+  @Input() selection: CdTableSelection;
+
+  constructor(private rgwDaemonService: RgwDaemonService) {}
+
+  ngOnChanges() {
+    // Get the service id of the first selected row.
+    if (this.selection.hasSelection) {
+      this.serviceId = this.selection.first().id;
+    }
+  }
+
+  getMetaData() {
+    if (_.isEmpty(this.serviceId)) {
+      return;
+    }
+    this.rgwDaemonService.get(this.serviceId).then(resp => {
+      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 (file)
index 0000000..64b703f
--- /dev/null
@@ -0,0 +1,18 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item active"
+        aria-current="page">Object Gateway</li>
+  </ol>
+</nav>
+
+<cd-table [data]="daemons"
+          [columns]="columns"
+          columnMode="flex"
+          selectionType="single"
+          (updateSelection)="updateSelection($event)"
+          (fetchData)="getDaemonList()">
+  <cd-rgw-daemon-details cdTableDetail
+                         [selection]="selection">
+  </cd-rgw-daemon-details>
+</cd-table>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..c0d331e
--- /dev/null
@@ -0,0 +1,41 @@
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { DataTableModule } from '../../../shared/datatable/datatable.module';
+import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
+import { RgwDaemonDetailsComponent } from '../rgw-daemon-details/rgw-daemon-details.component';
+import { RgwDaemonService } from '../services/rgw-daemon.service';
+import { RgwDaemonListComponent } from './rgw-daemon-list.component';
+
+describe('RgwDaemonListComponent', () => {
+  let component: RgwDaemonListComponent;
+  let fixture: ComponentFixture<RgwDaemonListComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ RgwDaemonListComponent, RgwDaemonDetailsComponent ],
+      imports: [
+        DataTableModule,
+        HttpClientTestingModule,
+        HttpClientModule,
+        TabsModule.forRoot(),
+        PerformanceCounterModule
+      ],
+      providers: [ RgwDaemonService ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwDaemonListComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..ce1c245
--- /dev/null
@@ -0,0 +1,51 @@
+import { Component } from '@angular/core';
+
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { RgwDaemonService } from '../services/rgw-daemon.service';
+
+@Component({
+  selector: 'cd-rgw-daemon-list',
+  templateUrl: './rgw-daemon-list.component.html',
+  styleUrls: ['./rgw-daemon-list.component.scss']
+})
+export class RgwDaemonListComponent {
+
+  columns: Array<CdTableColumn> = [];
+  daemons: Array<object> = [];
+  selection = new CdTableSelection();
+
+  constructor(private rgwDaemonService: RgwDaemonService,
+              cephShortVersionPipe: CephShortVersionPipe) {
+    this.columns = [
+      {
+        name: 'ID',
+        prop: 'id',
+        flexGrow: 2
+      },
+      {
+        name: 'Hostname',
+        prop: 'server_hostname',
+        flexGrow: 2
+      },
+      {
+        name: 'Version',
+        prop: 'version',
+        flexGrow: 1,
+        pipe: cephShortVersionPipe
+      }
+    ];
+  }
+
+  getDaemonList() {
+    this.rgwDaemonService.list()
+      .then((resp) => {
+        this.daemons = resp;
+      });
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+}
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 (file)
index 0000000..a888940
--- /dev/null
@@ -0,0 +1,34 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { SharedModule } from '../../shared/shared.module';
+import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
+import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component';
+import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component';
+import { RgwDaemonService } from './services/rgw-daemon.service';
+
+@NgModule({
+  entryComponents: [
+    RgwDaemonDetailsComponent
+  ],
+  imports: [
+    CommonModule,
+    SharedModule,
+    PerformanceCounterModule,
+    TabsModule.forRoot()
+  ],
+  exports: [
+    RgwDaemonListComponent,
+    RgwDaemonDetailsComponent
+  ],
+  declarations: [
+    RgwDaemonListComponent,
+    RgwDaemonDetailsComponent
+  ],
+  providers: [
+    RgwDaemonService
+  ]
+})
+export class RgwModule { }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts
new file mode 100644 (file)
index 0000000..691cc78
--- /dev/null
@@ -0,0 +1,21 @@
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { RgwDaemonService } from './rgw-daemon.service';
+
+describe('RgwDaemonService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [RgwDaemonService],
+      imports: [HttpClientTestingModule, HttpClientModule]
+    });
+  });
+
+  it(
+    'should be created',
+    inject([RgwDaemonService], (service: RgwDaemonService) => {
+      expect(service).toBeTruthy();
+    })
+  );
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts
new file mode 100644 (file)
index 0000000..907537e
--- /dev/null
@@ -0,0 +1,26 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class RgwDaemonService {
+
+  private url = 'api/rgw/daemon';
+
+  constructor(private http: HttpClient) { }
+
+  list() {
+    return this.http.get(this.url)
+      .toPromise()
+      .then((resp: any) => {
+        return resp;
+      });
+  }
+
+  get(id: string) {
+    return this.http.get(`${this.url}/${id}`)
+      .toPromise()
+      .then((resp: any) => {
+        return resp;
+      });
+  }
+}
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 (file)
index 0000000..e96b1b3
--- /dev/null
@@ -0,0 +1,18 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { SharedModule } from '../../shared/shared.module';
+
+import { LoginComponent } from './login/login.component';
+import { LogoutComponent } from './logout/logout.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    SharedModule
+  ],
+  declarations: [LoginComponent, LogoutComponent],
+  exports: [LogoutComponent]
+})
+export class AuthModule { }
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 (file)
index 0000000..e0b33c8
--- /dev/null
@@ -0,0 +1,73 @@
+<div class="login">
+  <div class="row full-height vertical-align">
+    <div class="col-sm-6 hidden-xs">
+      <img src="assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png"
+           alt="Ceph"
+           class="pull-right">
+    </div>
+    <div class="col-xs-10 col-sm-4 col-lg-3 col-xs-offset-1 col-sm-offset-0 col-md-offset-0 col-lg-offset-0">
+      <h1 i18n="The welcome message on the login page">Welcome to Ceph!</h1>
+      <form name="loginForm"
+            (ngSubmit)="login()"
+            #loginForm="ngForm"
+            novalidate>
+
+        <!-- Username -->
+        <div class="form-group has-feedback"
+             [ngClass]="{'has-error': (loginForm.submitted || username.dirty) && username.invalid}">
+          <input name="username"
+                 [(ngModel)]="model.username"
+                 #username="ngModel"
+                 type="text"
+                 placeholder="Enter your username..."
+                 class="form-control"
+                 required
+                 autofocus>
+          <div class="help-block"
+               *ngIf="(loginForm.submitted || username.dirty) && username.invalid">Username is required</div>
+        </div>
+
+        <!-- Password -->
+        <div class="form-group has-feedback"
+             [ngClass]="{'has-error': (loginForm.submitted || password.dirty) && password.invalid}">
+          <div class="input-group">
+            <input id="password"
+                   name="password"
+                   [(ngModel)]="model.password"
+                   #password="ngModel"
+                   type="password"
+                   placeholder="Enter your password..."
+                   class="form-control"
+                   required>
+            <span class="input-group-btn">
+              <button type="button"
+                      class="btn btn-default btn-password"
+                      cdPasswordButton="password">
+              </button>
+            </span>
+          </div>
+          <div class="help-block"
+               *ngIf="(loginForm.submitted || password.dirty) && password.invalid">Password is required
+          </div>
+        </div>
+
+        <!-- Stay signed in -->
+        <div class="checkbox checkbox-primary">
+          <input id="stay_signed_in"
+                 name="stay_signed_in"
+                 type="checkbox"
+                 [(ngModel)]="model.stay_signed_in">
+          <label for="stay_signed_in"
+                 i18n="A checkbox on the login page to do not expire session on browser close">
+            Keep me logged in
+          </label>
+        </div>
+
+        <input type="submit"
+               class="btn btn-openattic btn-block"
+               [disabled]="loginForm.invalid"
+               value="Login">
+      </form>
+    </div>
+  </div>
+</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 (file)
index 0000000..1f77356
--- /dev/null
@@ -0,0 +1,31 @@
+@import '../../../../defaults';
+
+.login {
+  height: 100%;
+
+  .row {
+    color: #ececec;
+    background-color: #474544;
+  }
+
+  h1 {
+    margin-top: 0;
+    margin-bottom: 30px;
+  }
+
+  .btn-password,
+  .form-control {
+    color: #ececec;
+    background-color: #555555;
+  }
+
+  .btn-password:focus {
+    outline-color: #66afe9;
+  }
+
+  .checkbox-primary input[type="checkbox"]:checked + label::before,
+  .checkbox-primary input[type="radio"]:checked + label::before {
+    background-color: $oa-color-blue;
+    border-color: $oa-color-blue;
+  }
+}
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 (file)
index 0000000..b8307b5
--- /dev/null
@@ -0,0 +1,40 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { LoginComponent } from './login.component';
+
+describe('LoginComponent', () => {
+  let component: LoginComponent;
+  let fixture: ComponentFixture<LoginComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        FormsModule,
+        SharedModule,
+        RouterTestingModule,
+        HttpClientTestingModule,
+        ToastModule.forRoot()
+      ],
+      declarations: [
+        LoginComponent
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(LoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..f8f4625
--- /dev/null
@@ -0,0 +1,38 @@
+import { Component, OnInit, ViewContainerRef } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ToastsManager } from 'ng2-toastr';
+
+import { Credentials } from '../../../shared/models/credentials';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { AuthService } from '../../../shared/services/auth.service';
+
+@Component({
+  selector: 'cd-login',
+  templateUrl: './login.component.html',
+  styleUrls: ['./login.component.scss']
+})
+export class LoginComponent implements OnInit {
+
+  model = new Credentials();
+
+  constructor(private authService: AuthService,
+              private authStorageService: AuthStorageService,
+              private router: Router,
+              public toastr: ToastsManager,
+              private vcr: ViewContainerRef) {
+    this.toastr.setRootViewContainerRef(vcr);
+  }
+
+  ngOnInit() {
+    if (this.authStorageService.isLoggedIn()) {
+      this.router.navigate(['']);
+    }
+  }
+
+  login() {
+    this.authService.login(this.model).then(() => {
+      this.router.navigate(['']);
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.html
new file mode 100644 (file)
index 0000000..993fd95
--- /dev/null
@@ -0,0 +1,6 @@
+<a i18n-title
+   title="Sign Out"
+   (click)="logout()">
+  <i class="fa fa-sign-out"></i>
+  <ng-container i18n>Logout</ng-container>
+</a>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.spec.ts
new file mode 100644 (file)
index 0000000..318ca82
--- /dev/null
@@ -0,0 +1,35 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { LogoutComponent } from './logout.component';
+
+describe('LogoutComponent', () => {
+  let component: LogoutComponent;
+  let fixture: ComponentFixture<LogoutComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        SharedModule,
+        RouterTestingModule,
+        HttpClientTestingModule
+      ],
+      declarations: [
+        LogoutComponent
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(LogoutComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.ts
new file mode 100644 (file)
index 0000000..4bf11e3
--- /dev/null
@@ -0,0 +1,24 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { AuthService } from '../../../shared/services/auth.service';
+
+@Component({
+  selector: 'cd-logout',
+  templateUrl: './logout.component.html',
+  styleUrls: ['./logout.component.scss']
+})
+export class LogoutComponent implements OnInit {
+
+  constructor(private authService: AuthService,
+              private router: Router) { }
+
+  ngOnInit() {
+  }
+
+  logout() {
+    this.authService.logout().then(() => {
+      this.router.navigate(['/login']);
+    });
+  }
+}
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 (file)
index 0000000..bd17681
--- /dev/null
@@ -0,0 +1,17 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { AuthModule } from './auth/auth.module';
+import { NavigationModule } from './navigation/navigation.module';
+import { NotFoundComponent } from './not-found/not-found.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    NavigationModule,
+    AuthModule
+  ],
+  exports: [NavigationModule],
+  declarations: [NotFoundComponent]
+})
+export class CoreModule { }
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 (file)
index 0000000..823d4fe
--- /dev/null
@@ -0,0 +1,24 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
+
+import { AppRoutingModule } from '../../app-routing.module';
+import { SharedModule } from '../../shared/shared.module';
+import { AuthModule } from '../auth/auth.module';
+import { NavigationComponent } from './navigation/navigation.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    AuthModule,
+    BsDropdownModule.forRoot(),
+    AppRoutingModule,
+    SharedModule,
+    RouterModule
+  ],
+  declarations: [NavigationComponent],
+  exports: [NavigationComponent]
+})
+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 (file)
index 0000000..cd7e804
--- /dev/null
@@ -0,0 +1,228 @@
+<nav class="navbar navbar-default navbar-openattic">
+  <!-- Brand and toggle get grouped for better mobile display -->
+
+  <div class="navbar-header tc_logo_component">
+    <a class="navbar-brand"
+       href="#">
+      <img src="assets/Ceph_Logo_Standard_RGB_White_120411_fa.png"
+           alt="Ceph">
+    </a>
+
+    <button type="button"
+            class="navbar-toggle collapsed"
+            data-toggle="collapse"
+            data-target="#bs-example-navbar-collapse-1">
+      <span i18n
+            class="sr-only">Toggle navigation
+      </span>
+      <span class="icon-bar"></span>
+      <span class="icon-bar"></span>
+      <span class="icon-bar"></span>
+    </button>
+  </div>
+
+  <!-- Collect the nav links, forms, and other content for toggling -->
+  <div class="collapse navbar-collapse"
+       id="bs-example-navbar-collapse-1">
+    <ul class="nav navbar-nav navbar-primary">
+
+      <!-- Dashboard -->
+      <li routerLinkActive="active"
+          class="tc_menuitem tc_menuitem_dashboard">
+        <a i18n
+           routerLink="/dashboard">
+          <i class="fa fa-heartbeat fa-fw"
+             [ngStyle]="summaryData?.health_status | healthColor"></i>
+          <span>Dashboard</span>
+        </a>
+      </li>
+
+      <!-- Cluster -->
+      <li dropdown
+          routerLinkActive="active"
+          class="dropdown tc_menuitem tc_menuitem_cluster">
+        <a dropdownToggle
+           class="dropdown-toggle"
+           data-toggle="dropdown">
+          <ng-container i18n>Cluster</ng-container>
+          <span class="caret"></span>
+        </a>
+        <ul *dropdownMenu
+            class="dropdown-menu">
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_hosts">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/hosts">Hosts
+            </a>
+          </li>
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_cluster_monitor">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/monitor/"> Monitors
+            </a>
+          </li>
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_hosts">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/osd">OSDs
+            </a>
+          </li>
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_configuration">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/configuration">Configuration Doc.
+            </a>
+          </li>
+        </ul>
+      </li>
+
+      <!-- Block -->
+      <li dropdown
+          routerLinkActive="active"
+          class="dropdown tc_menuitem tc_menuitem_block">
+        <a dropdownToggle
+           class="dropdown-toggle"
+           data-toggle="dropdown"
+           [ngStyle]="blockHealthColor()">
+          <ng-container i18n>Block</ng-container>
+          <span class="caret"></span>
+        </a>
+
+        <ul class="dropdown-menu">
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_block_mirroring">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/mirroring/"> Mirroring
+              <small *ngIf="summaryData?.rbd_mirroring?.warnings !== 0"
+                     class="label label-warning">{{ summaryData?.rbd_mirroring?.warnings }}</small>
+              <small *ngIf="summaryData?.rbd_mirroring?.errors !== 0"
+                     class="label label-danger">{{ summaryData?.rbd_mirroring?.errors }}</small>
+            </a>
+          </li>
+
+          <li routerLinkActive="active">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/block/iscsi">iSCSI</a>
+          </li>
+
+          <li class="dropdown-submenu">
+            <a class="dropdown-toggle"
+               data-toggle="dropdown">Pools</a>
+            <ul *dropdownMenu
+                class="dropdown-menu">
+              <li routerLinkActive="active"
+                  class="tc_submenuitem tc_submenuitem_pools"
+                  *ngFor="let rbdPool of rbdPools">
+                <a i18n
+                   class="dropdown-item"
+                   routerLink="/block/pool/{{ rbdPool }}">{{ rbdPool }}
+                </a>
+              </li>
+              <li class="tc_submenuitem tc_submenuitem_cephfs_nofs"
+                  *ngIf="rbdPools.length === 0">
+                <a class="dropdown-item disabled"
+                   i18n>There are no pools</a>
+              </li>
+            </ul>
+          </li>
+        </ul>
+      </li>
+
+      <!-- Filesystem -->
+      <li dropdown
+          routerLinkActive="active"
+          class="dropdown tc_menuitem tc_menuitem_cephs">
+        <a dropdownToggle
+           class="dropdown-toggle"
+           data-toggle="dropdown">
+          <ng-container i18n>Filesystems</ng-container>
+          <span class="caret"></span>
+        </a>
+        <ul *dropdownMenu
+            class="dropdown-menu">
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_cephfs_fs"
+              *ngFor="let fs of summaryData?.filesystems">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/cephfs/{{fs.id}}">{{ fs.name }}
+            </a>
+          </li>
+          <li class="tc_submenuitem tc_submenuitem_cephfs_nofs"
+              *ngIf="summaryData.filesystems.length === 0">
+            <span i18n>There are no filesystems</span>
+          </li>
+        </ul>
+      </li>
+      <!--
+  <li routerLinkActive="active"
+          class="tc_menuitem tc_menuitem_ceph_osds">
+        <a i18n
+           routerLink="/cephOsds">OSDs
+        </a>
+      </li>
+      <li routerLinkActive="active"
+          class="tc_menuitem tc_menuitem_ceph_pools">
+        <a i18n
+           routerLink="/cephPools">Pools
+        </a>
+      </li>
+      -->
+
+      <!-- Object Gateway -->
+      <li routerLinkActive="active"
+          class="tc_menuitem tc_menuitem_rgw">
+        <a i18n
+           routerLink="/rgw">Object Gateway
+        </a>
+      </li>
+
+      <!--<li class="dropdown tc_menuitem tc_menuitem_ceph_rgw">
+        <a href=""
+           class="dropdown-toggle"
+           data-toggle="dropdown">
+          <ng-container i18n>Object Gateway</ng-container>
+          <span class="caret"></span>
+        </a>
+        <ul *dropdownMenu
+            class="dropdown-menu">
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_rgw_users">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/rgw-users">Users
+            </a>
+          </li>
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_rgw_buckets">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/rgw-buckets">Buckets
+            </a>
+          </li>
+        </ul>
+      </li>
+      <li routerLinkActive="active"
+          class="tc_menuitem tc_submenuitem_settings">
+        <a i18n
+           routerLink="/settings">Settings
+        </a>
+      </li> -->
+    </ul>
+    <!-- /.navbar-primary -->
+
+    <ul class="nav navbar-nav navbar-utility">
+      <li class="tc_logout">
+        <cd-logout class="oa-navbar"></cd-logout>
+      </li>
+    </ul>
+    <!-- /.navbar-utility -->
+  </div>
+  <!-- /.navbar-collapse -->
+</nav>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..7548b2b
--- /dev/null
@@ -0,0 +1,37 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { LogoutComponent } from '../../auth/logout/logout.component';
+import { NavigationComponent } from './navigation.component';
+
+describe('NavigationComponent', () => {
+  let component: NavigationComponent;
+  let fixture: ComponentFixture<NavigationComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        SharedModule,
+        RouterTestingModule,
+        HttpClientTestingModule
+      ],
+      declarations: [
+        NavigationComponent,
+        LogoutComponent
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NavigationComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).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 (file)
index 0000000..ee61c41
--- /dev/null
@@ -0,0 +1,31 @@
+import { Component, OnInit } from '@angular/core';
+import { SummaryService } from '../../../shared/services/summary.service';
+
+@Component({
+  selector: 'cd-navigation',
+  templateUrl: './navigation.component.html',
+  styleUrls: ['./navigation.component.scss']
+})
+export class NavigationComponent implements OnInit {
+  summaryData: any;
+  rbdPools: Array<any> = [];
+
+  constructor(private summaryService: SummaryService) {}
+
+  ngOnInit() {
+    this.summaryService.summaryData$.subscribe((data: any) => {
+      this.summaryData = data;
+      this.rbdPools = data.rbd_pools;
+    });
+  }
+
+  blockHealthColor() {
+    if (this.summaryData && this.summaryData.rbd_mirroring) {
+      if (this.summaryData.rbd_mirroring.errors > 0) {
+        return { color: '#d9534f' };
+      } else if (this.summaryData.rbd_mirroring.warnings > 0) {
+        return { color: '#f0ad4e' };
+      }
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html
new file mode 100644 (file)
index 0000000..0f3847b
--- /dev/null
@@ -0,0 +1,14 @@
+<div class="row">
+  <div class="col-md-12 text-center">
+    <h1 i18n>Sorry, we could not find what you were looking for</h1>
+
+    <img class="img-responsive center-block img-rounded"
+         src="/assets/1280px-Mimic_Octopus2.jpg">
+    <span>
+      "<a href="https://www.flickr.com/photos/37707866@N00/4838953223">Mimic Octopus</a>" by prilfish is licensed under
+      <a rel="nofollow"
+         class="external text"
+         href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0</a>
+    </span>
+  </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss
new file mode 100644 (file)
index 0000000..e94d9f2
--- /dev/null
@@ -0,0 +1,15 @@
+h1 {
+  font-size: -webkit-xxx-large;
+}
+
+h2 {
+  font-size: xx-large;
+}
+
+*{
+  font-family: monospace;
+}
+
+img{
+  width: 50vw;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts
new file mode 100644 (file)
index 0000000..35189ed
--- /dev/null
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NotFoundComponent } from './not-found.component';
+
+describe('NotFoundComponent', () => {
+  let component: NotFoundComponent;
+  let fixture: ComponentFixture<NotFoundComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ NotFoundComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NotFoundComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts
new file mode 100644 (file)
index 0000000..d12bc32
--- /dev/null
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+  selector: 'cd-not-found',
+  templateUrl: './not-found.component.html',
+  styleUrls: ['./not-found.component.scss']
+})
+export class NotFoundComponent {
+  constructor() {}
+}
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 (file)
index 0000000..fe65bea
--- /dev/null
@@ -0,0 +1,26 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+import { AlertModule } from 'ngx-bootstrap';
+
+import { SparklineComponent } from './sparkline/sparkline.component';
+import { ViewCacheComponent } from './view-cache/view-cache.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    AlertModule.forRoot(),
+    ChartsModule
+  ],
+  declarations: [
+    ViewCacheComponent,
+    SparklineComponent
+  ],
+  providers: [],
+  exports: [
+    ViewCacheComponent,
+    SparklineComponent
+  ]
+})
+export class ComponentsModule { }
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 (file)
index 0000000..4b7a1b8
--- /dev/null
@@ -0,0 +1,13 @@
+<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 (file)
index 0000000..ec7d982
--- /dev/null
@@ -0,0 +1,5 @@
+@import '../../../../styles/chart-tooltip.scss';
+
+.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 (file)
index 0000000..4a879c3
--- /dev/null
@@ -0,0 +1,27 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppModule } from '../../../app.module';
+import { SparklineComponent } from './sparkline.component';
+
+describe('SparklineComponent', () => {
+  let component: SparklineComponent;
+  let fixture: ComponentFixture<SparklineComponent>;
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        imports: [AppModule]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(SparklineComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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 (file)
index 0000000..fa20ce3
--- /dev/null
@@ -0,0 +1,108 @@
+import { Component, ElementRef, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
+import { Input } from '@angular/core';
+
+import { ChartTooltip } from '../../../shared/models/chart-tooltip';
+
+@Component({
+  selector: 'cd-sparkline',
+  templateUrl: './sparkline.component.html',
+  styleUrls: ['./sparkline.component.scss']
+})
+export class SparklineComponent implements OnInit, OnChanges {
+  @ViewChild('sparkCanvas') chartCanvasRef: ElementRef;
+  @ViewChild('sparkTooltip') chartTooltipRef: ElementRef;
+
+  @Input() data: any;
+  @Input()
+  style = {
+    height: '30px',
+    width: '100px'
+  };
+
+  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 = {
+    animation: {
+      duration: 0
+    },
+    responsive: true,
+    maintainAspectRatio: false,
+    legend: {
+      display: false
+    },
+    elements: {
+      line: {
+        borderWidth: 1
+      }
+    },
+    tooltips: {
+      enabled: false,
+      mode: 'index',
+      intersect: false,
+      custom: undefined
+    },
+    scales: {
+      yAxes: [
+        {
+          display: false
+        }
+      ],
+      xAxes: [
+        {
+          display: false
+        }
+      ]
+    }
+  };
+
+  public datasets: Array<any> = [
+    {
+      data: []
+    }
+  ];
+
+  public labels: Array<any> = [];
+
+  constructor() {}
+
+  ngOnInit() {
+    const getStyleTop = (tooltip, positionY) => {
+      return (tooltip.caretY - tooltip.height - tooltip.yPadding - 5) + 'px';
+    };
+
+    const getStyleLeft = (tooltip, positionX) => {
+      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 => {
+      chartTooltip.customTooltips(tooltip);
+    };
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    this.datasets[0].data = changes['data'].currentValue;
+    this.datasets = [...this.datasets];
+    this.labels = [...Array(changes['data'].currentValue.length)];
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.html
new file mode 100644 (file)
index 0000000..1d71da2
--- /dev/null
@@ -0,0 +1,17 @@
+<alert i18n
+       type="info"
+       *ngIf="status === vcs.ValueNone">
+  Retrieving data, please wait.
+</alert>
+
+<alert i18n
+       type="warning"
+       *ngIf="status === vcs.ValueStale">
+  Displaying previously cached data.
+</alert>
+
+<alert i18n
+       type="danger"
+       *ngIf="status === vcs.ValueException">
+  Could not load data. Please check the cluster health.
+</alert>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts
new file mode 100644 (file)
index 0000000..da68def
--- /dev/null
@@ -0,0 +1,28 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AlertModule } from 'ngx-bootstrap';
+
+import { ViewCacheComponent } from './view-cache.component';
+
+describe('ViewCacheComponent', () => {
+  let component: ViewCacheComponent;
+  let fixture: ComponentFixture<ViewCacheComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ ViewCacheComponent ],
+      imports: [AlertModule.forRoot()]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ViewCacheComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.ts
new file mode 100644 (file)
index 0000000..63bc979
--- /dev/null
@@ -0,0 +1,17 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+
+@Component({
+  selector: 'cd-view-cache',
+  templateUrl: './view-cache.component.html',
+  styleUrls: ['./view-cache.component.scss']
+})
+export class ViewCacheComponent implements OnInit {
+  @Input() status: ViewCacheStatus;
+  vcs = ViewCacheStatus;
+
+  constructor() {}
+
+  ngOnInit() {}
+}
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 (file)
index 0000000..b09a31e
--- /dev/null
@@ -0,0 +1,34 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
+
+import { ComponentsModule } from '../components/components.module';
+import { PipesModule } from '../pipes/pipes.module';
+import { TableKeyValueComponent } from './table-key-value/table-key-value.component';
+import { TableComponent } from './table/table.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    NgxDatatableModule,
+    FormsModule,
+    BsDropdownModule.forRoot(),
+    PipesModule,
+    ComponentsModule,
+    RouterModule
+  ],
+  declarations: [
+    TableComponent,
+    TableKeyValueComponent
+  ],
+  exports: [
+    TableComponent,
+    NgxDatatableModule,
+    TableKeyValueComponent
+  ]
+})
+export class DataTableModule { }
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 (file)
index 0000000..d0ab74c
--- /dev/null
@@ -0,0 +1,9 @@
+<cd-table [data]="tableData"
+          [columns]="columns"
+          columnMode="flex"
+          [toolHeader]="false"
+          [header]="false"
+          [footer]="false"
+          [limit]="0"
+          (fetchData)="reloadData()">
+</cd-table>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..16e05bd
--- /dev/null
@@ -0,0 +1,99 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+
+import { ComponentsModule } from '../../components/components.module';
+import { TableComponent } from '../table/table.component';
+import { TableKeyValueComponent } from './table-key-value.component';
+
+describe('TableKeyValueComponent', () => {
+  let component: TableKeyValueComponent;
+  let fixture: ComponentFixture<TableKeyValueComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ TableComponent, TableKeyValueComponent ],
+      imports: [ FormsModule, NgxDatatableModule, ComponentsModule, RouterTestingModule ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(TableKeyValueComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should make key value object pairs out of arrays with length two', () => {
+    component.data = [
+      ['someKey', 0],
+      [3, 'something'],
+    ];
+    component.ngOnInit();
+    expect(component.tableData.length).toBe(2);
+    expect(component.tableData[0].key).toBe('someKey');
+    expect(component.tableData[1].value).toBe('something');
+  });
+
+  it('should transform arrays', () => {
+    component.data = [
+      ['someKey', [1, 2, 3]],
+      [3, 'something']
+    ];
+    component.ngOnInit();
+    expect(component.tableData.length).toBe(2);
+    expect(component.tableData[0].key).toBe('someKey');
+    expect(component.tableData[0].value).toBe('1, 2, 3');
+    expect(component.tableData[1].value).toBe('something');
+  });
+
+  it('should remove pure object values', () => {
+    component.data = [
+      [3, 'something'],
+      ['will be removed', { a: 3, b: 4, c: 5}]
+    ];
+    component.ngOnInit();
+    expect(component.tableData.length).toBe(1);
+    expect(component.tableData[0].value).toBe('something');
+  });
+
+  it('should make key value object pairs out of an object', () => {
+    component.data = {
+      3: 'something',
+      someKey: 0
+    };
+    component.ngOnInit();
+    expect(component.tableData.length).toBe(2);
+    expect(component.tableData[0].value).toBe('something');
+    expect(component.tableData[1].key).toBe('someKey');
+  });
+
+  it('should make do nothing if data is correct', () => {
+    component.data = [
+      {
+        key: 3,
+        value: 'something'
+      },
+      {
+        key: 'someKey',
+        value: 0
+      }
+    ];
+    component.ngOnInit();
+    expect(component.tableData.length).toBe(2);
+    expect(component.tableData[0].value).toBe('something');
+    expect(component.tableData[1].key).toBe('someKey');
+  });
+
+  it('should throw error if miss match', () => {
+    component.data = 38;
+    expect(() => component.ngOnInit()).toThrowError('Wrong data format');
+    component.data = [['someKey', 0, 3]];
+    expect(() => component.ngOnInit()).toThrowError('Wrong array format');
+  });
+});
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 (file)
index 0000000..101580f
--- /dev/null
@@ -0,0 +1,100 @@
+import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { CellTemplate } from '../../enum/cell-template.enum';
+import { CdTableColumn } from '../../models/cd-table-column';
+
+/**
+ * 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 {
+
+  columns: Array<CdTableColumn> = [];
+
+  @Input() data: any;
+
+  tableData: {
+    key: string,
+    value: any
+  }[];
+
+  /**
+   * The function that will be called to update the input data.
+   */
+  @Output() fetchData = new EventEmitter();
+
+  constructor() { }
+
+  ngOnInit() {
+    this.columns = [
+      {
+        prop: 'key',
+        flexGrow: 1,
+        cellTransformation: CellTemplate.bold
+      },
+      {
+        prop: 'value',
+        flexGrow: 3
+      }
+    ];
+    this.useData();
+  }
+
+  ngOnChanges(changes) {
+    this.useData();
+  }
+
+  useData() {
+    let temp = [];
+    if (!this.data) {
+      return; // Wait for data
+    } else if (_.isArray(this.data)) {
+      const first = this.data[0];
+      if (_.isPlainObject(first) && _.has(first, 'key') && _.has(first, 'value')) {
+        temp = [...this.data];
+      } else {
+        if (_.isArray(first)) {
+          if (first.length === 2) {
+            temp = this.data.map(a => ({
+              key: a[0],
+              value: a[1]
+            }));
+          } else {
+            throw new Error('Wrong array format');
+          }
+        }
+      }
+    } else if (_.isPlainObject(this.data)) {
+      temp = Object.keys(this.data).map(k => ({
+        key: k,
+        value: this.data[k]
+      }));
+    } else {
+      throw new Error('Wrong data format');
+    }
+    this.tableData = temp.map(o => {
+      if (_.isArray(o.value)) {
+        o.value = o.value.join(', ');
+      } else if (_.isObject(o.value)) {
+        return;
+      }
+      return o;
+    }).filter(o => o); // Filters out undefined
+  }
+
+  reloadData() {
+    // Forward event triggered by the 'cd-table' datatable.
+    this.fetchData.emit();
+  }
+}
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 (file)
index 0000000..ba6adf5
--- /dev/null
@@ -0,0 +1,120 @@
+<div class="dataTables_wrapper">
+  <div class="dataTables_header clearfix"
+       *ngIf="toolHeader">
+    <!-- actions -->
+    <div class="oadatatableactions">
+      <ng-content select="table-actions"></ng-content>
+    </div>
+    <!-- end actions -->
+
+    <!-- search -->
+    <div class="input-group">
+      <span class="input-group-addon">
+        <i class="glyphicon glyphicon-search"></i>
+      </span>
+      <input class="form-control"
+             type="text"
+             [(ngModel)]="search"
+             (keyup)='updateFilter($event)'>
+      <span class="input-group-btn">
+        <button type="button"
+                class="btn btn-default clear-input tc_clearInputBtn"
+                (click)="updateFilter()">
+          <i class="icon-prepend fa fa-remove"></i>
+        </button>
+      </span>
+    </div>
+    <!-- end search -->
+
+    <!-- pagination limit -->
+    <div class="input-group dataTables_paginate">
+      <input class="form-control"
+             type="number"
+             min="1"
+             max="9999"
+             [value]="limit"
+             (click)="setLimit($event)"
+             (keyup)="setLimit($event)"
+             (blur)="setLimit($event)">
+    </div>
+    <!-- end pagination limit-->
+
+    <!-- show hide columns -->
+    <div class="widget-toolbar">
+      <div dropdown
+           class="dropdown tc_menuitem tc_menuitem_cluster">
+        <a dropdownToggle
+           class="btn btn-sm btn-default dropdown-toggle tc_columnBtn"
+           data-toggle="dropdown">
+          <i class="fa fa-lg fa-table"></i>
+        </a>
+        <ul *dropdownMenu
+            class="dropdown-menu">
+          <li *ngFor="let column of columns">
+            <label>
+              <input type="checkbox"
+                     (change)="toggleColumn($event)"
+                     [name]="column.prop"
+                     [checked]="!column.isHidden">
+              <span>{{ column.name }}</span>
+            </label>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <!-- end show hide columns -->
+
+    <!-- refresh button -->
+    <div class="widget-toolbar tc_refreshBtn">
+      <a (click)="refreshBtn()">
+        <i class="fa fa-lg fa-refresh"
+           [class.fa-spin]="updating || loadingIndicator"></i>
+      </a>
+    </div>
+    <!-- end refresh button -->
+  </div>
+  <ngx-datatable #table
+                 class="bootstrap oadatatable"
+                 [cssClasses]="paginationClasses"
+                 [selectionType]="selectionType"
+                 [selected]="selection.selected"
+                 (select)="onSelect()"
+                 [sorts]="sorts"
+                 [columns]="tableColumns"
+                 [columnMode]="columnMode"
+                 [rows]="rows"
+                 [rowClass]="getRowClass()"
+                 [headerHeight]="header ? 'auto' : 0"
+                 [footerHeight]="footer ? 'auto' : 0"
+                 [limit]="limit > 0 ? limit : undefined"
+                 [loadingIndicator]="loadingIndicator"
+                 [rowIdentity]="rowIdentity()"
+                 [rowHeight]="'auto'">
+  </ngx-datatable>
+</div>
+
+<!-- Table Details -->
+<ng-content select="[cdTableDetail]"></ng-content>
+
+<!-- cell templates that can be accessed from outside -->
+<ng-template #tableCellBoldTpl
+             let-value="value">
+  <strong>{{ value }}</strong>
+</ng-template>
+
+<ng-template #sparklineTpl
+             let-value="value">
+  <cd-sparkline [data]="value"></cd-sparkline>
+</ng-template>
+
+<ng-template #routerLinkTpl
+             let-row="row"
+             let-value="value">
+  <a [routerLink]="[row.cdLink]">{{ value }}</a>
+</ng-template>
+
+<ng-template #perSecondTpl
+             let-row="row"
+             let-value="value">
+  {{ value }} /s
+</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 (file)
index 0000000..db9c4eb
--- /dev/null
@@ -0,0 +1,245 @@
+@import '../../../../defaults';
+
+.dataTables_wrapper {
+  margin-bottom: 25px;
+  .separator {
+    height: 30px;
+    border-left: 1px solid rgba(0,0,0,.09);
+    padding-left: 5px;
+    margin-left: 5px;
+    display: inline-block;
+    vertical-align: middle;
+  }
+  .widget-toolbar {
+    display: inline-block;
+    float: right;
+    width: auto;
+    height: 30px;
+    line-height: 28px;
+    position: relative;
+    border-left: 1px solid rgba(0,0,0,.09);
+    cursor: pointer;
+    padding: 0 8px;
+    text-align: center;
+  }
+  .dropdown-menu {
+    white-space: nowrap;
+    & > li {
+      cursor: pointer;
+      & > label {
+        width: 100%;
+        margin-bottom: 0;
+        padding-left: 20px;
+        padding-right: 20px;
+        cursor: pointer;
+        &:hover {
+          background-color: #f5f5f5;
+        }
+        & > input {
+          cursor: pointer;
+        }
+      }
+    }
+  }
+  th.oadatatablecheckbox {
+    width: 16px;
+  }
+  .dataTables_length>input {
+    line-height: 25px;
+    text-align: right;
+  }
+}
+.dataTables_header {
+  background-color: #f6f6f6;
+  border: 1px solid #d1d1d1;
+  border-bottom: none;
+  padding: 5px;
+  position: relative;
+  .oadatatableactions {
+    display: inline-block;
+  }
+  .form-group {
+    padding-left: 8px;
+  }
+  .input-group {
+    float: right;
+    border-left: 1px solid rgba(0,0,0,.09);
+    padding-left: 8px;
+    width: 40%;
+    max-width: 350px;
+    .form-control {
+      height: 30px;
+    }
+    .clear-input {
+      height: 30px;
+      i {
+        vertical-align: text-top;
+      }
+    }
+  }
+  .input-group.dataTables_paginate {
+    width: 8%;
+    min-width: 85px;
+  }
+}
+
+::ng-deep .oadatatable {
+  border: $border-color;
+  margin-bottom: 0;
+  max-width: none!important;
+  .progress-linear {
+    display: block;
+    position: relative;
+    width: 100%;
+    height: 5px;
+    padding: 0;
+    margin: 0;
+    .container {
+      background-color: $oa-color-light-blue;
+      .bar {
+        left: 0;
+        height: 100%;
+        width: 100%;
+        position: absolute;
+        overflow: hidden;
+        background-color: $oa-color-light-blue;
+      }
+      .bar:before{
+        display: block;
+        position: absolute;
+        content: "";
+        left: -200px;
+        width: 200px;
+        height: 100%;
+        background-color: $oa-color-blue;
+        animation: progress-loading 3s linear infinite;
+      }
+    }
+  }
+  .datatable-header {
+    background-clip: padding-box;
+    background-color: #f9f9f9;
+    background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
+    background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
+    background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
+    background-repeat: repeat-x;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
+    .sort-asc, .sort-desc {
+      color: $oa-color-blue;
+    }
+    .datatable-header-cell{
+      @include table-cell;
+      text-align: left;
+      font-weight: bold;
+      .datatable-header-cell-label {
+        &:after {
+          font-family: FontAwesome;
+          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: none;
+      }
+    }
+  }
+  .datatable-body {
+    .empty-row {
+      background-color: $warning-background-color;
+      text-align: center;
+      font-weight: bold;
+      font-style: italic;
+      padding-top: 5px;
+      padding-bottom: 5px;
+    }
+    .datatable-body-row {
+      &.clickable:hover .datatable-row-group {
+        background-color: #eee;
+        transition-property: background;
+        transition-duration: .3s;
+        transition-timing-function: linear;
+      }
+      &.datatable-row-even {
+        background-color: #ffffff;
+      }
+      &.datatable-row-odd {
+        background-color: #f6f6f6;
+      }
+      &.active, &.active:hover {
+        background-color: $bg-color-light-blue;
+      }
+      .datatable-body-cell{
+        @include table-cell;
+        &:first-child {
+          border-left: none;
+        }
+        .datatable-body-cell-label {
+          display: block;
+        }
+      }
+    }
+  }
+  .datatable-footer {
+    .selected-count, .page-count {
+      font-style: italic;
+      padding-left: 5px;
+    }
+    .datatable-pager .pager {
+      margin-right: 5px;
+      .pages {
+        & > a, & > span {
+          display: inline-block;
+          padding: 5px 10px;
+          margin-bottom: 5px;
+          border: none;
+        }
+        a:hover {
+          background-color: $oa-color-light-blue;
+        }
+        &.active > a {
+          background-color: $bg-color-light-blue;
+        }
+      }
+    }
+  }
+}
+
+@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 (file)
index 0000000..60ec7d0
--- /dev/null
@@ -0,0 +1,145 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgxDatatableModule, TableColumn } from '@swimlane/ngx-datatable';
+
+import { ComponentsModule } from '../../components/components.module';
+import { TableComponent } from './table.component';
+
+describe('TableComponent', () => {
+  let component: TableComponent;
+  let fixture: ComponentFixture<TableComponent>;
+  const columns: TableColumn[] = [];
+  const createFakeData = (n) => {
+    const data = [];
+    for (let i = 0; i < n; i++) {
+      data.push({
+        a: i,
+        b: i * i,
+        c: -(i % 10)
+      });
+    }
+    return data;
+  };
+
+  beforeEach(
+    async(() => {
+      TestBed.configureTestingModule({
+        declarations: [TableComponent],
+        imports: [NgxDatatableModule, FormsModule, ComponentsModule, RouterTestingModule]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(TableComponent);
+    component = fixture.componentInstance;
+  });
+
+  beforeEach(() => {
+    component.data = createFakeData(100);
+    component.useData();
+    component.columns = [
+      {prop: 'a'},
+      {prop: 'b'},
+      {prop: 'c'}
+    ];
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should have rows', () => {
+    expect(component.data.length).toBe(100);
+    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.limit).toBe(1);
+    expect(component.limit).toEqual(jasmine.any(Number));
+    e.target.value = '-20';
+    component.setLimit(e);
+    expect(component.limit).toBe(1);
+  });
+
+  it('should search for 13', () => {
+    component.search = '13';
+    expect(component.rows.length).toBe(100);
+    component.updateFilter(true);
+    expect(component.rows[0].a).toBe(13);
+    expect(component.rows[1].b).toBe(1369);
+    expect(component.rows[2].b).toBe(3136);
+    expect(component.rows.length).toBe(3);
+  });
+
+  it('should restore full table after search', () => {
+    component.search = '13';
+    expect(component.rows.length).toBe(100);
+    component.updateFilter(true);
+    expect(component.rows.length).toBe(3);
+    component.updateFilter();
+    expect(component.rows.length).toBe(100);
+  });
+
+  describe('after ngInit', () => {
+    const toggleColumn = (prop, checked) => {
+      component.toggleColumn({
+        target: {
+          name: prop,
+          checked: checked
+        }
+      });
+    };
+
+    beforeEach(() => {
+      component.ngOnInit();
+      component.table.sorts = component.sorts;
+    });
+
+    it('should have updated the column definitions', () => {
+      expect(component.columns[0].flexGrow).toBe(1);
+      expect(component.columns[1].flexGrow).toBe(2);
+      expect(component.columns[2].flexGrow).toBe(2);
+      expect(component.columns[2].resizeable).toBe(false);
+    });
+
+    it('should have table columns', () => {
+      expect(component.tableColumns.length).toBe(3);
+      expect(component.tableColumns).toEqual(component.columns);
+    });
+
+    it('should have a unique identifier which is search for', () => {
+      expect(component.identifier).toBe('a');
+      expect(component.sorts[0].prop).toBe('a');
+      expect(component.sorts).toEqual(component.createSortingDefinition('a'));
+    });
+
+    it('should remove column "a"', () => {
+      toggleColumn('a', false);
+      expect(component.table.sorts[0].prop).toBe('b');
+      expect(component.tableColumns.length).toBe(2);
+    });
+
+    it('should not be able to remove all columns', () => {
+      toggleColumn('a', false);
+      toggleColumn('b', false);
+      toggleColumn('c', false);
+      expect(component.table.sorts[0].prop).toBe('c');
+      expect(component.tableColumns.length).toBe(1);
+    });
+
+    it('should enable column "a" again', () => {
+      toggleColumn('a', false);
+      toggleColumn('a', true);
+      expect(component.table.sorts[0].prop).toBe('b');
+      expect(component.tableColumns.length).toBe(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 (file)
index 0000000..9f04e91
--- /dev/null
@@ -0,0 +1,282 @@
+import {
+  AfterContentChecked,
+  Component,
+  EventEmitter,
+  Input,
+  OnChanges,
+  OnDestroy,
+  OnInit,
+  Output,
+  TemplateRef,
+  Type,
+  ViewChild
+} from '@angular/core';
+import {
+  DatatableComponent,
+  SortDirection,
+  SortPropDir,
+  TableColumnProp
+} from '@swimlane/ngx-datatable';
+
+import * as _ from 'lodash';
+import 'rxjs/add/observable/timer';
+import { Observable } from 'rxjs/Observable';
+
+import { CdTableColumn } from '../../models/cd-table-column';
+import { CdTableSelection } from '../../models/cd-table-selection';
+
+@Component({
+  selector: 'cd-table',
+  templateUrl: './table.component.html',
+  styleUrls: ['./table.component.scss']
+})
+export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy {
+  @ViewChild(DatatableComponent) table: DatatableComponent;
+  @ViewChild('tableCellBoldTpl') tableCellBoldTpl: TemplateRef<any>;
+  @ViewChild('sparklineTpl') sparklineTpl: TemplateRef<any>;
+  @ViewChild('routerLinkTpl') routerLinkTpl: TemplateRef<any>;
+  @ViewChild('perSecondTpl') perSecondTpl: 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 the tool header, including reload button, pagination and search fields?
+  @Input() toolHeader ?= 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 ?= 10;
+
+  /**
+   * 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.
+   */
+  @Input() autoReload: any = 5000;
+
+  // Which row property is unique for a row
+  @Input() identifier = 'id';
+  // Allows other components to specify which type of selection they want,
+  // e.g. 'single' or 'multi'.
+  @Input() selectionType: string = undefined;
+
+  /**
+   * 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();
+
+  /**
+   * 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();
+
+  /**
+   * Use this variable to access the selected row(s).
+   */
+  selection = new CdTableSelection();
+
+  tableColumns: CdTableColumn[];
+  cellTemplates: {
+    [key: string]: TemplateRef<any>
+  } = {};
+  search = '';
+  rows = [];
+  loadingIndicator = true;
+  paginationClasses = {
+    pagerLeftArrow: 'i fa fa-angle-double-left',
+    pagerRightArrow: 'i fa fa-angle-double-right',
+    pagerPrevious: 'i fa fa-angle-left',
+    pagerNext: 'i fa fa-angle-right'
+  };
+  private subscriber;
+  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;
+
+  constructor() {}
+
+  ngOnInit() {
+    this._addTemplates();
+    if (!this.sorts) {
+      this.identifier = this.columns.some(c => c.prop === this.identifier) ?
+        this.identifier :
+        this.columns[0].prop + '';
+      this.sorts = this.createSortingDefinition(this.identifier);
+    }
+    this.columns.map(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;
+      }
+      return c;
+    });
+    this.tableColumns = this.columns.filter(c => !c.isHidden);
+    if (this.autoReload) { // Also if nothing is bound to fetchData nothing will be triggered
+      // Force showing the loading indicator because it has been set to False in
+      // useData() when this method was triggered by ngOnChanges().
+      this.loadingIndicator = true;
+      this.subscriber = Observable.timer(0, this.autoReload).subscribe(x => {
+        return this.reloadData();
+      });
+    }
+  }
+
+  ngOnDestroy() {
+    if (this.subscriber) {
+      this.subscriber.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;
+      this.table.recalculate();
+    }
+  }
+
+  _addTemplates() {
+    this.cellTemplates.bold = this.tableCellBoldTpl;
+    this.cellTemplates.sparkline = this.sparklineTpl;
+    this.cellTemplates.routerLink = this.routerLinkTpl;
+    this.cellTemplates.perSecond = this.perSecondTpl;
+  }
+
+  ngOnChanges(changes) {
+    this.useData();
+  }
+
+  setLimit(e) {
+    const value = parseInt(e.target.value, 10);
+    if (value > 0) {
+      this.limit = value;
+    }
+  }
+
+  reloadData() {
+    if (!this.updating) {
+      this.fetchData.emit();
+      this.updating = true;
+    }
+  }
+
+  refreshBtn () {
+    this.loadingIndicator = true;
+    this.reloadData();
+  }
+
+  rowIdentity() {
+    return (row) => {
+      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.rows = [...this.data];
+    if (this.search.length > 0) {
+      this.updateFilter(true);
+    }
+    this.loadingIndicator = false;
+    this.updating = false;
+  }
+
+  onSelect() {
+    this.selection.update();
+    this.updateSelection.emit(_.clone(this.selection));
+  }
+
+  toggleColumn($event: any) {
+    const prop: TableColumnProp = $event.target.name;
+    const hide = !$event.target.checked;
+    if (hide && this.tableColumns.length === 1) {
+      $event.target.checked = true;
+      return;
+    }
+    _.find(this.columns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
+    this.updateColumns();
+  }
+
+  updateColumns () {
+    this.tableColumns = this.columns.filter(c => !c.isHidden);
+    const sortProp = this.table.sorts[0].prop;
+    if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) {
+      this.table.onColumnSort({sorts: this.createSortingDefinition(this.tableColumns[0].prop)});
+    }
+    this.table.recalculate();
+  }
+
+  createSortingDefinition (prop: TableColumnProp): SortPropDir[] {
+    return [
+      {
+        prop: prop,
+        dir: SortDirection.asc
+      }
+    ];
+  }
+
+  updateFilter(event?) {
+    if (!event) {
+      this.search = '';
+    }
+    const val = this.search.toLowerCase();
+    const columns = this.columns;
+    // update the rows
+    this.rows = this.data.filter((d) => {
+      return (
+        columns.filter(c => {
+          return (
+            (_.isString(d[c.prop]) || _.isNumber(d[c.prop])) &&
+            (d[c.prop] + '').toLowerCase().indexOf(val) !== -1
+          );
+        }).length > 0
+      );
+    });
+    // Whenever the filter changes, always go back to the first page
+    this.table.offset = 0;
+  }
+
+  getRowClass() {
+    // Return the function used to populate a row's CSS classes.
+    return () => {
+      return {
+        clickable: !_.isUndefined(this.selectionType)
+      };
+    };
+  }
+}
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 (file)
index 0000000..1fc8f9c
--- /dev/null
@@ -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 (file)
index 0000000..b375ba2
--- /dev/null
@@ -0,0 +1,40 @@
+import { Directive, ElementRef, HostListener, Input, OnInit, Renderer2 } from '@angular/core';
+
+@Directive({
+  selector: '[cdPasswordButton]'
+})
+export class PasswordButtonDirective implements OnInit {
+  private inputElement: any;
+  private iElement: any;
+
+  @Input('cdPasswordButton') private cdPasswordButton: string;
+
+  constructor(private el: ElementRef, private renderer: Renderer2) { }
+
+  ngOnInit() {
+    this.inputElement = document.getElementById(this.cdPasswordButton);
+    this.iElement = this.renderer.createElement('i');
+    this.renderer.addClass(this.iElement, 'icon-prepend');
+    this.renderer.addClass(this.iElement, 'fa');
+    this.renderer.appendChild(this.el.nativeElement, this.iElement);
+    this.update();
+  }
+
+  private update() {
+    if (this.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() {
+    // Modify the type of the input field.
+    this.inputElement.type = (this.inputElement.type === 'password') ? 'text' : 'password';
+    // Update the button icon/tooltip.
+    this.update();
+  }
+}
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 (file)
index 0000000..7c1c216
--- /dev/null
@@ -0,0 +1,6 @@
+export enum CellTemplate {
+  bold = 'bold',
+  sparkline = 'sparkline',
+  perSecond = 'perSecond',
+  routerLink = 'routerLink'
+}
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 (file)
index 0000000..169059c
--- /dev/null
@@ -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/models/cd-table-column.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts
new file mode 100644 (file)
index 0000000..bf45c48
--- /dev/null
@@ -0,0 +1,7 @@
+import { TableColumn } from '@swimlane/ngx-datatable';
+import { CellTemplate } from '../enum/cell-template.enum';
+
+export interface CdTableColumn extends TableColumn {
+  cellTransformation?: CellTemplate;
+  isHidden?: boolean;
+}
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 (file)
index 0000000..9732abc
--- /dev/null
@@ -0,0 +1,28 @@
+export class CdTableSelection {
+  selected: any[] = [];
+  hasMultiSelection: boolean;
+  hasSingleSelection: boolean;
+  hasSelection: boolean;
+
+  constructor() {
+    this.update();
+  }
+
+  /**
+   * Recalculate the variables based on the current number
+   * of selected rows.
+   */
+  update() {
+    this.hasSelection = this.selected.length > 0;
+    this.hasSingleSelection = this.selected.length === 1;
+    this.hasMultiSelection = this.selected.length > 1;
+  }
+
+  /**
+   * 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/chart-tooltip.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/chart-tooltip.ts
new file mode 100644 (file)
index 0000000..56962f3
--- /dev/null
@@ -0,0 +1,117 @@
+import { ElementRef } from '@angular/core';
+
+import * as _ from 'lodash';
+
+export class ChartTooltip {
+  tooltipEl: any;
+  chartEl: any;
+  getStyleLeft: Function;
+  getStyleTop: Function;
+  customColors = {
+    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) {
+    // 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 => {
+        return bodyItem.lines;
+      });
+
+      let innerHtml = '<thead>';
+
+      titleLines.forEach(title => {
+        innerHtml += '<tr><th>' + this.getTitle(title) + '</th></tr>';
+      });
+      innerHtml += '</thead><tbody>';
+
+      bodyLines.forEach((body, i) => {
+        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) {
+    return body;
+  }
+
+  getTitle(title) {
+    return title;
+  }
+}
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 (file)
index 0000000..b33c366
--- /dev/null
@@ -0,0 +1,5 @@
+export class Credentials {
+  username: string;
+  password: string;
+  stay_signed_in = false;
+}
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 (file)
index 0000000..bfe10c2
--- /dev/null
@@ -0,0 +1,8 @@
+import { CephShortVersionPipe } from './ceph-short-version.pipe';
+
+describe('CephShortVersionPipe', () => {
+  it('create an instance', () => {
+    const pipe = new CephShortVersionPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..9599112
--- /dev/null
@@ -0,0 +1,18 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'cephShortVersion'
+})
+export class CephShortVersionPipe implements PipeTransform {
+  transform(value: any, args?: 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.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts
new file mode 100644 (file)
index 0000000..2424ebc
--- /dev/null
@@ -0,0 +1,10 @@
+import { FormatterService } from '../services/formatter.service';
+import { DimlessBinaryPipe } from './dimless-binary.pipe';
+
+describe('DimlessBinaryPipe', () => {
+  it('create an instance', () => {
+    const formatterService = new FormatterService();
+    const pipe = new DimlessBinaryPipe(formatterService);
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..92f0008
--- /dev/null
@@ -0,0 +1,20 @@
+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, args?: any): any {
+    return this.formatter.format_number(value, 1024, [
+      'B',
+      'KiB',
+      'MiB',
+      'GiB',
+      'TiB',
+      'PiB'
+    ]);
+  }
+}
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 (file)
index 0000000..4bbfdd8
--- /dev/null
@@ -0,0 +1,10 @@
+import { FormatterService } from '../services/formatter.service';
+import { DimlessPipe } from './dimless.pipe';
+
+describe('DimlessPipe', () => {
+  it('create an instance', () => {
+    const formatterService = new FormatterService();
+    const pipe = new DimlessPipe(formatterService);
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..5e02846
--- /dev/null
@@ -0,0 +1,20 @@
+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, args?: any): any {
+    return this.formatter.format_number(value, 1000, [
+      ' ',
+      'k',
+      'M',
+      'G',
+      'T',
+      'P'
+    ]);
+  }
+}
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 (file)
index 0000000..1427de3
--- /dev/null
@@ -0,0 +1,8 @@
+import { FilterPipe } from './filter.pipe';
+
+describe('FilterPipe', () => {
+  it('create an instance', () => {
+    const pipe = new FilterPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..21115a7
--- /dev/null
@@ -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 => {
+      let result = true;
+
+      args.forEach(filter => {
+        if (!filter.value) {
+          return;
+        }
+
+        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 (file)
index 0000000..e0e44e0
--- /dev/null
@@ -0,0 +1,8 @@
+import { HealthColorPipe } from './health-color.pipe';
+
+describe('HealthColorPipe', () => {
+  it('create an instance', () => {
+    const pipe = new HealthColorPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..9d82475
--- /dev/null
@@ -0,0 +1,18 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'healthColor'
+})
+export class HealthColorPipe implements PipeTransform {
+  transform(value: any, args?: any): any {
+    if (value === 'HEALTH_OK') {
+      return { color: '#00bb00' };
+    } else if (value === 'HEALTH_WARN') {
+      return { color: '#ffa500' };
+    } else if (value === 'HEALTH_ERR') {
+      return { color: '#ff0000' };
+    } else {
+      return null;
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.spec.ts
new file mode 100644 (file)
index 0000000..768f12a
--- /dev/null
@@ -0,0 +1,8 @@
+import { ListPipe } from './list.pipe';
+
+describe('ListPipe', () => {
+  it('create an instance', () => {
+    const pipe = new ListPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.ts
new file mode 100644 (file)
index 0000000..1e37919
--- /dev/null
@@ -0,0 +1,10 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'list'
+})
+export class ListPipe implements PipeTransform {
+  transform(value: any, args?: any): any {
+    return value.join(', ');
+  }
+}
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 100644 (file)
index 0000000..51dc736
--- /dev/null
@@ -0,0 +1,40 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { CephShortVersionPipe } from './ceph-short-version.pipe';
+import { DimlessBinaryPipe } from './dimless-binary.pipe';
+import { DimlessPipe } from './dimless.pipe';
+import { FilterPipe } from './filter.pipe';
+import { HealthColorPipe } from './health-color.pipe';
+import { ListPipe } from './list.pipe';
+import { RelativeDatePipe } from './relative-date.pipe';
+
+@NgModule({
+  imports: [CommonModule],
+  declarations: [
+    DimlessBinaryPipe,
+    HealthColorPipe,
+    DimlessPipe,
+    CephShortVersionPipe,
+    RelativeDatePipe,
+    ListPipe,
+    FilterPipe
+  ],
+  exports: [
+    DimlessBinaryPipe,
+    HealthColorPipe,
+    DimlessPipe,
+    CephShortVersionPipe,
+    RelativeDatePipe,
+    ListPipe,
+    FilterPipe
+  ],
+  providers: [
+    CephShortVersionPipe,
+    DimlessBinaryPipe,
+    DimlessPipe,
+    RelativeDatePipe,
+    ListPipe
+  ]
+})
+export class PipesModule {}
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 (file)
index 0000000..1295b0d
--- /dev/null
@@ -0,0 +1,8 @@
+import { RelativeDatePipe } from './relative-date.pipe';
+
+describe('RelativeDatePipe', () => {
+  it('create an instance', () => {
+    const pipe = new RelativeDatePipe();
+    expect(pipe).toBeTruthy();
+  });
+});
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 (file)
index 0000000..6bfa395
--- /dev/null
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import * as moment from 'moment';
+
+@Pipe({
+  name: 'relativeDate'
+})
+export class RelativeDatePipe implements PipeTransform {
+  constructor() {}
+
+  transform(value: any, args?: any): any {
+    if (!value) {
+      return 'unknown';
+    }
+    return moment(value * 1000).fromNow();
+  }
+}
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 (file)
index 0000000..a3ec803
--- /dev/null
@@ -0,0 +1,19 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
+
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class AuthGuardService implements CanActivate {
+
+  constructor(private router: Router, private authStorageService: AuthStorageService) {
+  }
+
+  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+    if (this.authStorageService.isLoggedIn()) {
+      return true;
+    }
+    this.router.navigate(['/login']);
+    return false;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-interceptor.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-interceptor.service.ts
new file mode 100644 (file)
index 0000000..f09250d
--- /dev/null
@@ -0,0 +1,42 @@
+import {
+  HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest,
+  HttpResponse
+} from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ToastsManager } from 'ng2-toastr';
+import 'rxjs/add/operator/do';
+import { Observable } from 'rxjs/Observable';
+
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class AuthInterceptorService implements HttpInterceptor {
+
+  constructor(private router: Router,
+              private authStorageService: AuthStorageService,
+              public toastr: ToastsManager) {
+  }
+
+  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+    return next.handle(request).do((event: HttpEvent<any>) => {
+      if (event instanceof HttpResponse) {
+        // do nothing
+      }
+    }, (err: any) => {
+      if (err instanceof HttpErrorResponse) {
+        if (err.status === 404) {
+          this.router.navigate(['/404']);
+          return;
+        }
+
+        this.toastr.error(err.error.detail || '', `${err.status} - ${err.statusText}`);
+        if (err.status === 401) {
+          this.authStorageService.remove();
+          this.router.navigate(['/login']);
+        }
+      }
+    });
+  }
+}
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 (file)
index 0000000..cd6dbbe
--- /dev/null
@@ -0,0 +1,21 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class AuthStorageService {
+
+  constructor() {
+  }
+
+  set(username: string) {
+    localStorage.setItem('dashboard_username', username);
+  }
+
+  remove() {
+    localStorage.removeItem('dashboard_username');
+  }
+
+  isLoggedIn() {
+    return localStorage.getItem('dashboard_username') !== null;
+  }
+
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth.service.ts
new file mode 100644 (file)
index 0000000..88a7136
--- /dev/null
@@ -0,0 +1,25 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Credentials } from '../models/credentials';
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class AuthService {
+
+  constructor(private authStorageService: AuthStorageService,
+              private http: HttpClient) {
+  }
+
+  login(credentials: Credentials) {
+    return this.http.post('api/auth', credentials).toPromise().then((resp: Credentials) => {
+      this.authStorageService.set(resp.username);
+    });
+  }
+
+  logout() {
+    return this.http.delete('api/auth').toPromise().then(() => {
+      this.authStorageService.remove();
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.spec.ts
new file mode 100644 (file)
index 0000000..dcb5a9e
--- /dev/null
@@ -0,0 +1,21 @@
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { ConfigurationService } from './configuration.service';
+
+describe('ConfigurationService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [ConfigurationService],
+      imports: [HttpClientTestingModule, HttpClientModule]
+    });
+  });
+
+  it(
+    'should be created',
+    inject([ConfigurationService], (service: ConfigurationService) => {
+      expect(service).toBeTruthy();
+    })
+  );
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.ts
new file mode 100644 (file)
index 0000000..41ac7bb
--- /dev/null
@@ -0,0 +1,11 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class ConfigurationService {
+  constructor(private http: HttpClient) {}
+
+  getConfigData() {
+    return this.http.get('api/cluster_conf/');
+  }
+}
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 (file)
index 0000000..f3a99b5
--- /dev/null
@@ -0,0 +1,15 @@
+import { inject, TestBed } from '@angular/core/testing';
+
+import { FormatterService } from './formatter.service';
+
+describe('FormatterService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [FormatterService]
+    });
+  });
+
+  it('should be created', inject([FormatterService], (service: FormatterService) => {
+    expect(service).toBeTruthy();
+  }));
+});
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 (file)
index 0000000..3986408
--- /dev/null
@@ -0,0 +1,51 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class FormatterService {
+  constructor() {}
+
+  truncate(n, maxWidth) {
+    const stringized = n.toString();
+    const parts = stringized.split('.');
+    if (parts.length === 1) {
+      // Just an int
+      return stringized;
+    } else {
+      const fractionalDigits = maxWidth - parts[0].length - 1;
+      if (fractionalDigits <= 0) {
+        // No width available for the fractional part, drop
+        // it and the decimal point
+        return parts[0];
+      } else {
+        return stringized.substring(0, maxWidth);
+      }
+    }
+  }
+
+  format_number(n, divisor, units) {
+    const width = 4;
+    let unit = 0;
+
+    if (n == null) {
+      // People shouldn't really be passing null, but let's
+      // do something sensible instead of barfing.
+      return '-';
+    }
+
+    while (Math.floor(n / divisor ** unit).toString().length > width - 1) {
+      unit = unit + 1;
+    }
+
+    let truncatedFloat;
+    if (unit > 0) {
+      truncatedFloat = this.truncate(
+        (n / Math.pow(divisor, unit)).toString(),
+        width
+      );
+    } else {
+      truncatedFloat = this.truncate(n, width);
+    }
+
+    return truncatedFloat === '' ? '-' : (truncatedFloat + units[unit]);
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/host.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/host.service.ts
new file mode 100644 (file)
index 0000000..3d28cd7
--- /dev/null
@@ -0,0 +1,15 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class HostService {
+
+  constructor(private http: HttpClient) {
+  }
+
+  list() {
+    return this.http.get('api/host').toPromise().then((resp: any) => {
+      return resp;
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/pool.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/pool.service.ts
new file mode 100644 (file)
index 0000000..8ac6de9
--- /dev/null
@@ -0,0 +1,15 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class PoolService {
+
+  constructor(private http: HttpClient) {
+  }
+
+  rbdPoolImages(pool) {
+    return this.http.get(`api/rbd/${pool}`).toPromise().then((resp: any) => {
+      return resp;
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts
new file mode 100644 (file)
index 0000000..0f59831
--- /dev/null
@@ -0,0 +1,18 @@
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { RbdMirroringService } from './rbd-mirroring.service';
+
+describe('RbdMirroringService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [RbdMirroringService],
+      imports: [HttpClientTestingModule, HttpClientModule]
+    });
+  });
+
+  it('should be created', inject([RbdMirroringService], (service: RbdMirroringService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.ts
new file mode 100644 (file)
index 0000000..b840b30
--- /dev/null
@@ -0,0 +1,11 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class RbdMirroringService {
+  constructor(private http: HttpClient) {}
+
+  get() {
+    return this.http.get('api/rbdmirror');
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/services.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/services.module.ts
new file mode 100644 (file)
index 0000000..04d4a3c
--- /dev/null
@@ -0,0 +1,21 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ConfigurationService } from './configuration.service';
+import { FormatterService } from './formatter.service';
+import { RbdMirroringService } from './rbd-mirroring.service';
+import { SummaryService } from './summary.service';
+import { TcmuIscsiService } from './tcmu-iscsi.service';
+
+@NgModule({
+  imports: [CommonModule],
+  declarations: [],
+  providers: [
+    FormatterService,
+    SummaryService,
+    TcmuIscsiService,
+    ConfigurationService,
+    RbdMirroringService
+  ]
+})
+export class ServicesModule { }
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 (file)
index 0000000..23af983
--- /dev/null
@@ -0,0 +1,21 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { SharedModule } from '../shared.module';
+import { SummaryService } from './summary.service';
+
+describe('SummaryService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [SummaryService],
+      imports: [HttpClientTestingModule, SharedModule]
+    });
+  });
+
+  it(
+    'should be created',
+    inject([SummaryService], (service: SummaryService) => {
+      expect(service).toBeTruthy();
+    })
+  );
+});
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 (file)
index 0000000..9556930
--- /dev/null
@@ -0,0 +1,31 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Subject } from 'rxjs/Subject';
+
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class SummaryService {
+  // Observable sources
+  private summaryDataSource = new Subject();
+
+  // Observable streams
+  summaryData$ = this.summaryDataSource.asObservable();
+
+  constructor(private http: HttpClient, private authStorageService: AuthStorageService) {
+    this.refresh();
+  }
+
+  refresh() {
+    if (this.authStorageService.isLoggedIn()) {
+      this.http.get('api/summary').subscribe(data => {
+        this.summaryDataSource.next(data);
+      });
+    }
+
+    setTimeout(() => {
+      this.refresh();
+    }, 5000);
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/tcmu-iscsi.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/tcmu-iscsi.service.ts
new file mode 100644 (file)
index 0000000..2f36bb8
--- /dev/null
@@ -0,0 +1,15 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class TcmuIscsiService {
+
+  constructor(private http: HttpClient) {
+  }
+
+  tcmuiscsi() {
+    return this.http.get('api/tcmuiscsi').toPromise().then((resp: any) => {
+      return resp;
+    });
+  }
+}
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 (file)
index 0000000..7651338
--- /dev/null
@@ -0,0 +1,43 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ComponentsModule } from './components/components.module';
+import { DataTableModule } from './datatable/datatable.module';
+import { PasswordButtonDirective } from './directives/password-button.directive';
+import { PipesModule } from './pipes/pipes.module';
+import { AuthGuardService } from './services/auth-guard.service';
+import { AuthStorageService } from './services/auth-storage.service';
+import { AuthService } from './services/auth.service';
+import { FormatterService } from './services/formatter.service';
+import { HostService } from './services/host.service';
+import { PoolService } from './services/pool.service';
+import { ServicesModule } from './services/services.module';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    PipesModule,
+    ComponentsModule,
+    ServicesModule,
+    DataTableModule
+  ],
+  declarations: [
+    PasswordButtonDirective
+  ],
+  exports: [
+    ComponentsModule,
+    PipesModule,
+    ServicesModule,
+    PasswordButtonDirective,
+    DataTableModule
+  ],
+  providers: [
+    AuthService,
+    AuthStorageService,
+    AuthGuardService,
+    PoolService,
+    FormatterService,
+    HostService
+  ],
+})
+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 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/1280px-Mimic_Octopus2.jpg b/src/pybind/mgr/dashboard/frontend/src/assets/1280px-Mimic_Octopus2.jpg
new file mode 100644 (file)
index 0000000..f8cf2a8
Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/1280px-Mimic_Octopus2.jpg differ
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png
new file mode 100644 (file)
index 0000000..26d602b
Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png differ
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png
new file mode 100644 (file)
index 0000000..0f07b83
Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png 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 (executable)
index 0000000..8fb88de
Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/loading.gif 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 (file)
index 0000000..b3446a8
Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/logo-mini.png differ
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/notification-icons.png b/src/pybind/mgr/dashboard/frontend/src/assets/notification-icons.png
new file mode 100644 (file)
index 0000000..d609a7c
Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/notification-icons.png differ
diff --git a/src/pybind/mgr/dashboard/frontend/src/defaults.scss b/src/pybind/mgr/dashboard/frontend/src/defaults.scss
new file mode 100644 (file)
index 0000000..8e25b71
--- /dev/null
@@ -0,0 +1,11 @@
+$warning-background-color: #fff3cd;
+$oa-color-blue: #288cea;
+$oa-color-light-blue: #afd9ee;
+$bg-color-light-blue: #d9edf7;
+$border-color: 1px solid #d1d1d1;
+@mixin table-cell {
+  padding: 5px;
+  border: none;
+  border-left: $border-color;
+  border-bottom: $border-color;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/environments/environment.prod.ts b/src/pybind/mgr/dashboard/frontend/src/environments/environment.prod.ts
new file mode 100644 (file)
index 0000000..3612073
--- /dev/null
@@ -0,0 +1,3 @@
+export const environment = {
+  production: true
+};
diff --git a/src/pybind/mgr/dashboard/frontend/src/environments/environment.ts b/src/pybind/mgr/dashboard/frontend/src/environments/environment.ts
new file mode 100644 (file)
index 0000000..b7f639a
--- /dev/null
@@ -0,0 +1,8 @@
+// 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 = {
+  production: false
+};
diff --git a/src/pybind/mgr/dashboard/frontend/src/favicon.ico b/src/pybind/mgr/dashboard/frontend/src/favicon.ico
new file mode 100644 (file)
index 0000000..90e538b
Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/favicon.ico 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 (file)
index 0000000..05a8f70
--- /dev/null
@@ -0,0 +1,28 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Ceph</title>
+
+  <script>
+    document.write('<base href="' + document.location+ '" />');
+  </script>
+
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="icon" type="image/x-icon" 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/main.ts b/src/pybind/mgr/dashboard/frontend/src/main.ts
new file mode 100644 (file)
index 0000000..91ec6da
--- /dev/null
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+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)
+  .catch(err => console.log(err));
diff --git a/src/pybind/mgr/dashboard/frontend/src/openattic-theme.scss b/src/pybind/mgr/dashboard/frontend/src/openattic-theme.scss
new file mode 100755 (executable)
index 0000000..aa819a2
--- /dev/null
@@ -0,0 +1,1184 @@
+/*
+  Basics
+  Branding
+  Breadcrumb
+  Buttons
+  Dropdown
+  Grid
+  Modal
+  Navbar
+  Navs
+  Notifications
+  Pagination
+  Panel
+  Table
+  Typo
+
+  Login
+  Statistics
+
+  ApiRecorder
+  Caret
+  Datatables
+  Feedback
+  FlexElement
+  Grafana
+  Graph
+  Progressbar
+  TagForm
+  Trees
+  CSS Fix
+*/
+
+@import 'defaults';
+
+$fa-font-path: "../node_modules/font-awesome/fonts";
+@import "../node_modules/font-awesome/scss/font-awesome";
+
+/* Basics */
+html {
+  background-color: #ffffff;
+}
+html,
+body {
+  width: 100%;
+  height: 100%;
+  font-size: 12px;
+}
+optgroup {
+  font-weight: bold;
+  font-style: italic;
+}
+option {
+  font-weight: normal;
+  font-style: normal;
+}
+.full-height {
+  height: 100%;
+}
+.vertical-align {
+  display: flex;
+  align-items: center;
+}
+.loading {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+}
+.bg-color-darken {
+  background-color: #404040!important;
+}
+.bg-color-greenLight {
+  background-color: #71843f!important;
+}
+.bg-color-red {
+  background-color: #a90329!important;
+}
+.no-margin {
+  margin: 0;
+}
+.margin-left-md {
+  margin-left: 15px
+}
+.margin-right-md {
+  margin-right: 15px
+}
+.margin-right-sm {
+  margin-right: 10px
+}
+.margin-bottom-md {
+  margin-bottom: 15px
+}
+.no-padding {
+  padding: 0;
+}
+.small-padding {
+  padding: 5px;
+}
+.no-border {
+  border: 0px;
+  box-shadow: 0px 0px 0px !important;
+}
+.no-wrap {
+  white-space: nowrap;
+}
+.strikethrough {
+  text-decoration: line-through;
+}
+.italic {
+  font-style: italic;
+}
+.bold {
+  font-weight: bold;
+}
+.text-right {
+  text-align: right;
+}
+.text-monospace {
+  font-family: monospace;
+}
+
+/* Branding */
+.navbar-openattic .navbar-brand,
+.navbar-openattic .navbar-brand:hover{
+  color: #ececec;
+  height: auto;
+  margin: 15px 0 15px 20px;
+  padding: 0;
+  -webkit-align-self: flex-start;
+  align-self: flex-start;
+}
+.navbar-openattic .navbar-brand>img {
+  height: 25px;
+}
+
+/* Breadcrumb */
+.breadcrumb {
+  padding: 8px 0;
+  background-color: transparent;
+  border-radius: 0;
+}
+.breadcrumb>li+li:before {
+  padding: 0 5px 0 7px;
+  color: #474544;
+  font-family: "FontAwesome";
+  content: "\f101";
+}
+.breadcrumb>li>span {
+  color: #474544;
+}
+
+/* Icons */
+.icon-warning {
+  color: #f0ad4e;
+}
+.icon-danger {
+  color: #c9302c;
+}
+
+/* Buttons */
+.btn-openattic {
+  color: #ececec;
+  background-color: $oa-color-blue;
+  border-color: $oa-color-blue;
+}
+.btn-primary {
+  color: #ececec;
+  background-color: $oa-color-blue;
+  border-color: #2172bf;
+}
+.btn-primary:hover,
+.btn-primary:focus,
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+  color: #ececec;
+  background-color: #2582D9;
+  border-color: #2172bf;
+}
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+  background-image: none;
+}
+.btn-primary.disabled,
+.btn-primary[disabled],
+fieldset[disabled] .btn-primary,
+.btn-primary.disabled:hover,
+.btn-primary[disabled]:hover,
+fieldset[disabled] .btn-primary:hover,
+.btn-primary.disabled:focus,
+.btn-primary[disabled]:focus,
+fieldset[disabled] .btn-primary:focus,
+.btn-primary.disabled:active,
+.btn-primary[disabled]:active,
+fieldset[disabled] .btn-primary:active,
+.btn-primary.disabled.active,
+.btn-primary[disabled].active,
+fieldset[disabled] .btn-primary.active {
+  background-color: $oa-color-blue;
+  border-color: #2172bf;
+}
+.btn-primary .badge {
+  color: $oa-color-blue;
+  background-color: #ececec;
+}
+.btn-primary .caret {
+  color: #ececec;
+}
+.btn-group>.btn>i.fa,
+button.btn.btn-label>i.fa {
+  /** Add space between icon and text */
+  padding-right: 5px;
+}
+
+/* Dropdown */
+.dropdown-menu {
+  min-width: 50px;
+}
+.dropdown-menu>li>a {
+  color: #474544;
+  cursor: pointer;
+}
+.dropdown-menu>li>a>i.fa {
+  /** Add space between icon and text */
+  padding-right: 5px;
+}
+.dropdown-menu>.active>a {
+  color: #ececec;
+  background-color: $oa-color-blue;
+}
+.dataTables_wrapper .dropdown-menu>li.divider {
+  cursor: auto;
+}
+
+/* Grid */
+.container,
+.container-fluid {
+  padding-left: 30px;
+  padding-right: 30px;
+}
+.row {
+  margin-left: -30px;
+  margin-right: -30px;
+}
+.col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9,
+.col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9,
+.col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9,
+.col-xs-1, .col-xs-10, .col-xs-11, .col-xs-12, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9 {
+  padding-left: 30px;
+  padding-right: 30px;
+}
+
+/* Modal */
+.modal-dialog {
+  margin: 30px auto !important;
+}
+.modal .modal-content .openattic-modal-header,
+.modal .modal-content .openattic-modal-content,
+.modal .modal-content .openattic-modal-footer {
+  padding: 10px 20px;
+}
+.modal .modal-content .openattic-modal-header {
+  border-bottom: 1px solid #cccccc;
+  border-radius: 5px 5px 0 0;
+  background-color: #f5f5f5;
+}
+.modal .modal-content .openattic-modal-content {
+  padding: 20px 20px 10px 20px;
+  overflow-x: auto;
+  max-height: 70vh;
+}
+.modal .modal-content .openattic-modal-content p {
+  margin-bottom: 10px;
+}
+.modal .modal-content .openattic-modal-content legend {
+  font-size: 1.833em;
+}
+.modal .modal-content .openattic-modal-footer {
+  border-top: 1px solid #cccccc;
+  border-radius: 0 0 5px 5px;
+  background-color: #f5f5f5;
+}
+.modal .modal-content .openattic-modal-header span {
+  display: block;
+  font-size: 16px; /* Same as .panel-title */
+}
+
+/* Modal Table (Task Queue) */
+table.task-queue-table thead {
+  display: flex;
+  flex-flow: row;
+}
+table.task-queue-table thead tr {
+  display: flex;
+  align-items: stretch;
+  width: 100%;
+}
+table.task-queue-table tbody {
+  display: flex;
+  flex-flow: row wrap;
+}
+table.task-queue-table tbody tr {
+  display: flex;
+  width: 100%
+}
+table.task-queue-table > * > tr > * {
+  flex: 1;
+}
+table.task-queue-table > * > tr > .oadatatablecheckbox {
+  flex: 0;
+}
+div.task-queue-modal-content {
+  height: 40em;
+}
+div.openattic-modal-content div.modal-scroll {
+  max-height: 26em;
+  overflow: auto;
+  border-bottom: 1px solid #e1e1e1;
+}
+div.task-queue-modal-content div.dataTables_wrapper {
+  margin-bottom: 0;
+}
+div.task-queue-modal-content div.dataTables_wrapper th.oadatatablecheckbox {
+  width: 100%;
+}
+div.task-queue-modal-content div.dataTables_wrapper div.widget-toolbar.tc_refreshBtn{
+  width: 36px;
+}
+ul.task-queue-pagination {
+  display: table;
+  margin: auto;
+  padding-top: 10px;
+}
+
+/* Navbar */
+.navbar-openattic {
+  margin-bottom: 0;
+  background: #474544;
+  border: 0;
+  border-radius: 0;
+  border-top: 4px solid $oa-color-blue;
+  font-size: 1.2em;
+}
+.navbar-openattic .navbar-header {
+  display: flex;
+  float: none;
+}
+.navbar-openattic .navbar-toggle {
+  margin-left: auto;
+  border: 0;
+}
+.navbar-openattic .navbar-toggle:focus,
+.navbar-openattic .navbar-toggle:hover {
+  background-color: transparent;
+  outline: 0;
+}
+.navbar-openattic .navbar-toggle .icon-bar {
+  background-color: #ececec;
+}
+.navbar-openattic .navbar-toggle:focus .icon-bar,
+.navbar-openattic .navbar-toggle:hover .icon-bar {
+  -webkit-box-shadow: 0 0 3px #fff;
+  box-shadow: 0 0 3px #fff;
+}
+.navbar-openattic .navbar-collapse {
+  padding: 0;
+}
+.navbar-openattic .navbar-nav>li>a,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a {
+  color: #ececec;
+  line-height: 1;
+  padding: 10px 20px;
+  position: relative;
+  display: block;
+  text-decoration: none;
+}
+.navbar-openattic .navbar-nav>li>a:focus,
+.navbar-openattic .navbar-nav>li>a:hover,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a:focus,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
+  color: #ececec;
+}
+.navbar-openattic .navbar-nav>li>a:hover,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
+  background-color: #505050;
+}
+.navbar-openattic .navbar-nav>.open>a,
+.navbar-openattic .navbar-nav>.open>a:hover,
+.navbar-openattic .navbar-nav>.open>a:focus,
+.navbar-openattic .navbar-nav>.open>.oa-navbar>a,
+.navbar-openattic .navbar-nav>.open>.oa-navbar>a:hover,
+.navbar-openattic .navbar-nav>.open>.oa-navbar>a:focus {
+  color: #ececec;
+  border-color: transparent;
+  background-color: transparent;
+}
+.navbar-openattic .navbar-primary>li>a {
+  border: 0;
+}
+.navbar-openattic .navbar-primary>.active>a,
+.navbar-openattic .navbar-primary>.active>a:hover,
+.navbar-openattic .navbar-primary>.active>a:focus {
+  color: #ececec;
+  background-color: $oa-color-blue;
+  border: 0;
+}
+.navbar-openattic .navbar-utility a,
+.navbar-openattic .navbar-utility .fa{
+  font-size: 1.0em;
+}
+.navbar-openattic .navbar-utility>.active>a {
+  color: #ececec;
+  background-color: #505050;
+}
+.navbar-openattic .navbar-utility>li>.open>a,
+.navbar-openattic .navbar-utility>li>.open>a:hover,
+.navbar-openattic .navbar-utility>li>.open>a:focus {
+  color: #ececec;
+  border-color: transparent;
+  background-color: transparent;
+}
+@media (min-width: 768px) {
+  .navbar-openattic .navbar-primary>li>a {
+    border-bottom: 4px solid transparent;
+  }
+  .navbar-openattic .navbar-primary>.active>a,
+  .navbar-openattic .navbar-primary>.active>a:hover,
+  .navbar-openattic .navbar-primary>.active>a:focus {
+    background-color: transparent;
+    border-bottom: 4px solid $oa-color-blue;
+  }
+  .navbar-openattic .navbar-utility {
+    border-bottom: 0;
+    font-size: 11px;
+    position: absolute;
+    right: 0;
+    top: 0;
+  }
+}
+@media (max-width: 767px) {
+  .navbar-openattic .navbar-nav {
+    margin: 0;
+  }
+  .navbar-openattic .navbar-collapse,
+  .navbar-openattic .navbar-form {
+    border-color: #ececec;
+  }
+  .navbar-openattic .navbar-collapse {
+    padding: 0;
+  }
+  .navbar-nav .open .dropdown-menu {
+    padding-top: 0;
+    padding-bottom: 0;
+    background-color: #505050;
+  }
+  .navbar-nav .open .dropdown-menu .dropdown-header,
+  .navbar-nav .open .dropdown-menu>li>a {
+    padding: 5px 15px 5px 35px;
+  }
+  .navbar-openattic .navbar-nav .open .dropdown-menu>li>a {
+    color: #ececec;
+  }
+  .navbar-openattic .navbar-nav .open .dropdown-menu>.active>a {
+    color: #ececec;
+    background-color: $oa-color-blue;
+  }
+  .navbar-openattic .navbar-nav>li>a:hover {
+    background-color: $oa-color-blue;
+  }
+  .navbar-openattic .navbar-utility {
+    border-top: 1px solid #ececec;
+  }
+  .navbar-openattic .navbar-primary>.active>a,
+  .navbar-openattic .navbar-primary>.active>a:hover,
+  .navbar-openattic .navbar-primary>.active>a:focus {
+    background-color: $oa-color-blue;
+  }
+}
+
+/* Navs */
+.nav-tabs {
+  margin-bottom: 15px;
+}
+.nav-tabs-openattic {
+  margin-top: -15px;
+  margin-bottom: 15px;
+}
+.nav-tabs-openattic>li>a {
+  padding: 7px 15px 4px 15px;
+}
+.nav-tabs-openattic>li.active>a,
+.nav-tabs-openattic>li.active>a:active,
+.nav-tabs-openattic>li.active>a:focus,
+.nav-tabs-openattic>li.active>a:hover {
+  border: 0!important;
+  border-bottom: 3px solid $oa-color-blue!important;
+}
+
+/* Notifications */
+#toasty .toast.toasty-theme-bootstrap {
+  opacity: 1
+}
+
+/* Pagination */
+.pagination {
+  display: block;
+  margin: 0;
+}
+.pagination>.disabled>a,
+.pagination>.disabled>a:focus,
+.pagination>.disabled>a:hover,
+.pagination>.disabled>span,
+.pagination>.disabled>span:focus,
+.pagination>.disabled>span:hover {
+  -webkit-box-shadow: none;
+  box-shadow: none;
+  cursor: not-allowed;
+  background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
+  background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
+  background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
+}
+.pagination>.active>a,
+.pagination>.active>a:focus,
+.pagination>.active>a:hover,
+.pagination>.active>span,
+.pagination>.active>span:focus,
+.pagination>.active>span:hover,
+.pagination>.disabled>a,
+.pagination>.disabled>a:focus,
+.pagination>.disabled>a:hover,
+.pagination>.disabled>span,
+.pagination>.disabled>span:focus,
+.pagination>.disabled>span:hover,
+.pagination>li>a,
+.pagination>li>span,
+.panel-group
+.panel-heading {
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
+}
+.pagination>li>a,
+.pagination>li>span {
+  background-color: #eee;
+  background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
+  background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
+  background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
+  border-color: #b7b7b7;
+  color: #4d5258;
+  cursor: pointer;
+  font-weight: 600;
+  padding: 2px 10px;
+}
+.pagination>.active>span,
+.pagination>.active>span:focus,
+.pagination>.active>span:hover {
+  color: $oa-color-blue;
+  border-color: #fff #e1e1e1 #f4f4f4;
+  border-width: 0 1px;
+}
+
+/* Panel */
+.panel .panel-toolbar {
+  float: right;
+}
+.panel .panel-toolbar div {
+  display: inline-block;
+}
+.panel .panel-toolbar>a,
+.panel .panel-toolbar>.dropdown>a {
+  padding-left: 5px;
+}
+.panel-dashboard {
+  height: 100%;
+  padding-top: 60px;
+}
+.panel-dashboard>.panel-heading {
+  cursor: move;
+  position: relative;
+  margin-top: -60px;
+  width: 100%;
+}
+.panel-dashboard>.panel-heading>.panel-title {
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  -o-text-overflow: ellipsis;
+}
+.panel-dashboard>.panel-heading>.toolbar a {
+  text-decoration: none;
+}
+.panel-dashboard>.panel-body {
+  height: 100%;
+  overflow: auto;
+}
+.panel-dashboard>.panel-body .indent {
+  margin-top: 10px;
+  margin-left: 10px;
+}
+.panel-dashboard .overlay {
+  position: absolute;
+  bottom: 5px;
+  right: 5px;
+  z-index: 10;
+}
+.panel-dashboard .max-height {
+  height: 100%;
+}
+.panel-dashboard .max-height.alert-is-shown {
+  height: 85%;
+}
+.panel-dashboard .fa-2x{
+  vertical-align: middle;
+  margin-right: 0.5em;
+}
+.panel-dashboard .alert.bottom-margin-zero {
+  margin-bottom: 0;
+}
+.panel-openattic {
+  border: $border-color;
+  border-top: 0;
+  border-radius: 0;
+}
+.panel-openattic>.panel-heading {
+  border-top: 2px solid $oa-color-blue;
+  border-radius: 0;
+  padding: 20px 15px;
+}
+.panel-openattic>.panel-heading>.panel-title {
+  color: #333333;
+  font-size: 1.333em;
+  margin: 0;
+  padding: 0;
+}
+.panel-openattic>.panel-body {
+  background: #ffffff;
+  border-top: $border-color;
+  padding: 10px 15px;
+}
+.panel-openattic>.panel-footer {
+  background: #ffffff;
+  border-top: $border-color;
+}
+
+/* Typo */
+a {
+  color: $oa-color-blue;
+}
+a:hover,
+a:focus{
+  color: #474544;
+}
+h1 {
+  letter-spacing: -1px;
+  font-size: 2em;
+}
+h2 {
+  letter-spacing: -1px;
+  font-size: 1.833em;
+}
+h3{
+  display: block;
+  font-size: 1.583em;
+  font-weight: 400;
+}
+h3.sub-title {
+  color: #666666;
+  margin-left: 15px;
+}
+h4{
+  font-size: 1.5em;
+  line-height: normal
+}
+h5{
+  font-size: 1.417em;
+  font-weight: 300;
+  line-height: normal;
+}
+h6{
+  font-size: 1.25em;
+  font-weight: 700;
+  line-height: normal;
+}
+
+/*************************************************************/
+
+/* Statistics */
+.statistics-content {
+  margin: 0 -20px;
+}
+
+/*************************************************************/
+
+/* ApiRecorder */
+.apirecorder {
+  resize: none;
+  width:100%;
+}
+.apirecorder-enabled {
+  color: red;
+}
+
+/* Caret */
+.caret {
+  color: $oa-color-blue;
+}
+
+/* Feedback */
+#feedback .feedback-button {
+  position: fixed;
+  top: 50%;
+  right: 0;
+  padding: 2px 16px;
+  cursor: pointer;
+  color: #ffffff;
+  font-size: 1.2em;
+  font-weight: 700;
+  background-color: $oa-color-blue;
+  border-radius: 5px 5px 0 0;
+  z-index: 99999;
+}
+#feedback .feedback-button:hover {
+  background-color: #2172bf;
+}
+#feedback .feedback-button-transform {
+  -webkit-transform: rotate(-90deg) translate(50%, -100%);
+  -moz-transform: rotate(-90deg) translate(50%, -100%);
+  -ms-transform: rotate(-90deg) translate(50%, -100%);
+  -o-transform: rotate(-90deg) translate(50%, -100%);
+  transform: rotate(-90deg) translate(50%, -100%);
+  -webkit-transform-origin: top right;
+  -moz-transform-origin: top right;
+  -ms-transform-origin: top right;
+  -o-transform-origin: top right;
+  transform-origin: top right;
+}
+#feedback .feedback-button-active {
+  right: 299px;
+}
+#feedback .feedback-button .fa,
+#feedback .feedback-button .glyphicon{
+  padding-right: 6px;
+}
+#feedback .feedback-panel {
+  position: fixed;
+  top: 0;
+  right: -300px;
+  padding: 20px;
+  width: 300px;
+  height: 100%;
+  background-color: #ffffff;
+  border-left: 5px solid $oa-color-blue;
+  z-index: 99999;
+  overflow-y: auto;
+}
+#feedback .feedback-panel-active {
+  right: 0;
+}
+#feedback .feedback-transition {
+  transition: right 150ms cubic-bezier(0.0, 0.0, 0.2, 1);
+}
+
+/* FlexElement */
+/* Container */
+.flex-container {
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+}
+.flex-wrap {
+  -webkit-flex-wrap: wrap;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+}
+.flex-nowrap {
+  -webkit-flex-wrap: nowrap;
+  -ms-flex-wrap: nowrap;
+  flex-wrap: nowrap;
+}
+.flex-row {
+  -webkit-flex-direction: row;
+  -ms-flex-direction: row;
+  flex-direction: row;
+}
+.flex-column {
+  -webkit-flex-direction: column;
+  -ms-flex-direction: column;
+  flex-direction: column;
+}
+/* Items */
+.flex-item {
+  margin-bottom: 10px;
+  padding: 15px;
+}
+.flex-item-1 { -webkit-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; padding: 0 10px; }
+.flex-item-2 { -webkit-flex: 2; -moz-flex: 2; -ms-flex: 2; flex: 2; padding: 0 10px; }
+.flex-item-3 { -webkit-flex: 3; -moz-flex: 3; -ms-flex: 3; flex: 3; padding: 0 10px; }
+.flex-item-4 { -webkit-flex: 4; -moz-flex: 4; -ms-flex: 4; flex: 4; padding: 0 10px; }
+.flex-item-5 { -webkit-flex: 5; -moz-flex: 5; -ms-flex: 5; flex: 5; padding: 0 10px; }
+.flex-item-6 { -webkit-flex: 6; -moz-flex: 6; -ms-flex: 6; flex: 6; padding: 0 10px; }
+.flex-item-7 { -webkit-flex: 7; -moz-flex: 7; -ms-flex: 7; flex: 7; padding: 0 10px; }
+.flex-item-8 { -webkit-flex: 8; -moz-flex: 8; -ms-flex: 8; flex: 8; padding: 0 10px; }
+.flex-item-9 { -webkit-flex: 9; -moz-flex: 9; -ms-flex: 9; flex: 9; padding: 0 10px; }
+
+/* Grafana */
+.grafana-container {
+  margin-top: 20px;
+  height: 64px;
+  background:url(./assets/loading.gif) center center no-repeat;
+}
+.grafana {
+  width: 100%;
+  min-height: 600px;
+}
+
+/* Progressbar */
+.progress-bar {
+  background-image: none !important;
+}
+.progress-bar-info {
+  background-color: $oa-color-blue;
+}
+.progress-bar-freespace {
+  background-color: #ddd;
+}
+.progress-bar-stolenspace {
+  background-color: #aaa;
+}
+.progress-bar-outer{
+  margin-top: 5px !important;
+}
+.progress-bar-outer div {
+  border-radius: 31px;
+  background-color: #ffffff;
+  border: 1px solid #ccc;
+  box-shadow: 0 0 0 0;
+  -webkit-box-shadow: 0 0 0 0;
+  -moz-box-shadow: 0 0 0 0;
+  margin: 0;
+  height: 16px;
+}
+.progress-bar-outer div div {
+  background-color: #0091d9;
+}
+.progress-bar-outer div div span {
+  position: relative;
+  top: -3px;
+}
+.oaprogress {
+  position: relative;
+  margin-bottom: 0;
+}
+.oaprogress div.progress-bar {
+  position: static;
+}
+.oaprogress span {
+  position: absolute;
+  display: block;
+  width: 100%;
+  color: black;
+  font-weight: normal;
+}
+
+tags-input .tags {
+  border-radius: 4px;
+  border: 1px solid #ccc;
+  box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+}
+
+/* TagForm */
+.tag-form label {
+  display: block;
+  margin-bottom: 6px;
+  line-height: 19px;
+  font-weight: 400;
+  font-size: 13px;
+  color: #333;
+  text-align: left;
+  white-space: normal;
+}
+
+/* Trees */
+.tree {
+  min-height: 20px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.tree>ul {
+  padding-left: 0;
+}
+.tree ul ul {
+  padding-left: 34px;
+  padding-top: 10px;
+}
+.tree li {
+  list-style-type: none;
+  margin: 0;
+  padding: 5px;
+  position: relative;
+}
+.tree li span {
+  -moz-border-radius: 5px;
+  -webkit-border-radius: 5px;
+  border: 1px dotted #999;
+  border-radius: 5px;
+  display: inline-block;
+  padding: 3px 8px;
+  text-decoration: none;
+  -webkit-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
+  -moz-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
+  -o-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
+  transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
+}
+.tree>ul>li::after,
+.tree>ul>li:before {
+  border: 0;
+}
+.tree li:after,
+.tree li:before {
+  content: '';
+  left: -20px;
+  position: absolute;
+  right: auto;
+}
+.tree li:before {
+  border-left: 1px solid #999;
+  bottom: 50px;
+  height: 100%;
+  top: -11px;
+  width: 1px;
+  -webkit-transition: "border-color 0.1s ease 0.1s";
+  -moz-transition: "border-color 0.1s ease 0.1s";
+  -o-transition: "border-color 0.1s ease 0.1s";
+  transition: "border-color 0.1s ease 0.1s";
+}
+.tree li:after {
+  border-top: 1px solid #999;
+  height: 20px;
+  top: 18px;
+  width: 25px;
+}
+.tree li:last-child::before {
+  height: 30px;
+}
+
+.scrollable-menu {
+  height: auto;
+  max-height: 200px;
+  overflow-x: hidden;
+}
+
+.toggle, .toggle-on, .toggle-off {
+  border-radius: 20px;
+}
+
+.toggle .toggle-handle {
+  border-radius: 20px;
+}
+
+/* CSS Fix */
+a {
+  cursor: pointer;
+}
+form .input-group-addon {
+  color: #a2a2a2 !important;
+  background-color: transparent;
+}
+uib-accordion .panel-title,
+.panel .accordion-title {
+  font-size: 14px !important;
+}
+.panel-body h2:first-child {
+  margin-top: 0;
+}
+.actions {
+  padding-bottom: 10px;
+}
+.pull-left {
+  float: left;
+}
+.code-clogs {
+  display: block;
+  padding: 9px;
+  margin: 0 0 10px;
+  font-size: 13px;
+  line-height: 1.42857143;
+  color: #333;
+  word-break: break-all;
+  word-wrap: break-word;
+  background-color: #f5f5f5;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
+}
+.degree-sign:after {
+  content: "\00B0 C"!important;
+}
+.formactions.well {
+  overflow: auto;
+  padding: 10px 20px;
+}
+.disabled {
+  pointer-events: none;
+}
+.clickable {
+  cursor: pointer;
+}
+.non-clickable {
+  cursor: initial;
+}
+.locked {
+  cursor: default!important;
+}
+.list-nomargin {
+  margin: 0;
+}
+
+.has-error .has-error-btn {
+  background-color: #f2dede;
+  border-color: #a94442;
+}
+
+.has-error .has-error-btn:disabled:hover {
+  background-color: #f2dede;
+  border-color: #a94442;
+}
+
+/* If javascript is disabled. */
+.noscript {
+  padding-top: 5em;
+}
+.noscript p {
+  color: #777;
+}
+
+/* Notifications */
+
+.notification div.img-circle {
+  width: 50px;
+  height: 50px;
+  position: relative;
+}
+.notification.info div.img-circle {
+  background-color: #5bc0de;
+}
+.notification.error div.img-circle {
+  background-color: #d9534f;
+}
+.notification.success div.img-circle {
+  background-color: #5cb85c;
+}
+.notification.warning div.img-circle {
+  background-color: #f0ad4e;
+}
+
+.notification .icon {
+  background-repeat: no-repeat;
+  background-image: url('./assets/notification-icons.png') !important;
+  height: 36px;
+  width: 36px;
+  position: absolute;
+  margin: 7px;
+}
+.notification.info .icon {
+  background-position: -36px 0;
+}
+.notification.error .icon {
+  background-position: -108px 0;
+}
+.notification.success .icon {
+  background-position: 0 0;
+}
+.notification.warning .icon {
+  background-position: -72px 0;
+}
+
+.required {
+  color: #d04437;
+}
+
+/* oa-helper  */
+oa-helper i {
+  color: $oa-color-blue;
+  cursor: pointer;
+}
+
+.page-footer {
+  font-size: 12px;
+  color: #777;
+  text-align: center;
+  margin-left: 150px;
+  margin-right: 150px;
+  margin-top: 50px;
+  margin-bottom: 50px;
+}
+
+hr.oa-hr-small {
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+
+.table>thead>tr>th.rbd-striping-object{
+  min-width: 60px;
+}
+.table>thead>tr>th.rbd-striping-stripe {
+  min-width: 100px;
+}
+.rbd-striping-column-separator {
+  width: 1px;
+}
+
+.table>tbody>tr>td.rbd-striping-cell-top {
+  border-top: 1px solid #ccc;
+  border-left: 1px solid #ccc;
+  border-right: 1px solid #ccc;
+}
+.table>tbody>tr>td.rbd-striping-cell-center {
+  border-top: 1px dashed #ccc;
+  border-left: 1px solid #ccc;
+  border-right: 1px solid #ccc;
+}
+.table>tbody>tr>td.rbd-striping-cell-bottom {
+  border-bottom: 1px solid #ccc;
+  border-left: 1px solid #ccc;
+  border-right: 1px solid #ccc;
+}
+
+.dropdown-submenu {
+    position: relative;
+}
+
+.dropdown-submenu>.dropdown-menu {
+    top: 0;
+    left: 100%;
+    margin-top: -6px;
+    margin-left: -1px;
+    -webkit-border-radius: 0 6px 6px 6px;
+    -moz-border-radius: 0 6px 6px;
+    border-radius: 0 6px 6px 6px;
+}
+
+.dropdown-submenu:hover>.dropdown-menu {
+    display: block;
+}
+
+.dropdown-submenu>a:after {
+    display: block;
+    content: " ";
+    float: right;
+    width: 0;
+    height: 0;
+    border-color: transparent;
+    border-style: solid;
+    border-width: 5px 0 5px 5px;
+    border-left-color: $oa-color-blue;
+    margin-top: 5px;
+    margin-right: -10px;
+}
+
+.dropdown-submenu:hover>a:after {
+    border-left-color: $oa-color-blue;
+}
+
+.dropdown-submenu.pull-left {
+    float: none;
+}
+
+.dropdown-submenu.pull-left>.dropdown-menu {
+    left: -100%;
+    margin-left: 10px;
+    -webkit-border-radius: 6px 0 6px 6px;
+    -moz-border-radius: 6px 0 6px 6px;
+    border-radius: 6px 0 6px 6px;
+}
+
+/* Forms */
+.form-group>.control-label>span.required {
+  @extend .fa;
+  @extend .fa-asterisk;
+  @extend .required;
+  font-size: 6px;
+  padding-left: 4px;
+  vertical-align: text-top;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/polyfills.ts b/src/pybind/mgr/dashboard/frontend/src/polyfills.ts
new file mode 100644 (file)
index 0000000..caac2e0
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * 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/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+import 'core-js/es6/array';
+import 'core-js/es6/date';
+import 'core-js/es6/function';
+import 'core-js/es6/map';
+import 'core-js/es6/math';
+import 'core-js/es6/number';
+import 'core-js/es6/object';
+import 'core-js/es6/parse-float';
+import 'core-js/es6/parse-int';
+import 'core-js/es6/regexp';
+import 'core-js/es6/set';
+import 'core-js/es6/string';
+import 'core-js/es6/symbol';
+import 'core-js/es6/weak-map';
+import 'core-js/es7/object';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js';  // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following for the Reflect API. */
+// import 'core-js/es6/reflect';
+
+/** Evergreen browsers require these. **/
+// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
+import 'core-js/es7/reflect';
+
+/**
+ * Required to support Web Animations `@angular/platform-browser/animations`.
+ * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
+ **/
+// import 'web-animations-js';  // Run `npm install --save web-animations-js`.
+
+/***************************************************************************************************
+ * 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/styles.scss b/src/pybind/mgr/dashboard/frontend/src/styles.scss
new file mode 100644 (file)
index 0000000..c10c1ee
--- /dev/null
@@ -0,0 +1,2 @@
+/* You can add global styles to this file, and also import other style files */
+@import './openattic-theme.scss';
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 (file)
index 0000000..835bb36
--- /dev/null
@@ -0,0 +1,62 @@
+.chart-container {
+  position: absolute;
+  margin: auto;
+  cursor: pointer;
+  overflow: visible;
+}
+
+canvas {
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+
+.chartjs-tooltip {
+  opacity: 0;
+  position: absolute;
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+  border-radius: 3px;
+  -webkit-transition: all 0.1s ease;
+  transition: all 0.1s ease;
+  pointer-events: none;
+  font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif !important;
+
+  -webkit-transform: translate(-50%, 0);
+  transform: translate(-50%, 0);
+
+  &.transform-left {
+    transform: translate(-10%, 0);
+
+    &::after {
+      left: 10%;
+    }
+  }
+
+  &.transform-right {
+    transform: translate(-90%, 0);
+
+    &::after {
+      left: 90%;
+    }
+  }
+}
+
+.chartjs-tooltip::after {
+  content: ' ';
+  position: absolute;
+  top: 100%; /* At the bottom of the tooltip */
+  left: 50%;
+  margin-left: -5px;
+  border-width: 5px;
+  border-style: solid;
+  border-color: black transparent transparent transparent;
+}
+
+::ng-deep .chartjs-tooltip-key {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  margin-right: 10px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/test.ts b/src/pybind/mgr/dashboard/frontend/src/test.ts
new file mode 100644 (file)
index 0000000..19beece
--- /dev/null
@@ -0,0 +1,33 @@
+/* tslint:disable:ordered-imports */
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/dist/long-stack-trace-zone';
+import 'zone.js/dist/proxy.js';
+import 'zone.js/dist/sync-test';
+import 'zone.js/dist/jasmine-patch';
+import 'zone.js/dist/async-test';
+import 'zone.js/dist/fake-async-test';
+import { getTestBed } from '@angular/core/testing';
+import {
+  BrowserDynamicTestingModule,
+  platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
+declare const __karma__: any;
+declare const require: any;
+
+// Prevent Karma from running prematurely.
+__karma__.loaded = function () {};
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+  BrowserDynamicTestingModule,
+  platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
+// Finally, start Karma to run the tests.
+__karma__.start();
diff --git a/src/pybind/mgr/dashboard/frontend/src/tsconfig.app.json b/src/pybind/mgr/dashboard/frontend/src/tsconfig.app.json
new file mode 100644 (file)
index 0000000..39ba8db
--- /dev/null
@@ -0,0 +1,13 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/app",
+    "baseUrl": "./",
+    "module": "es2015",
+    "types": []
+  },
+  "exclude": [
+    "test.ts",
+    "**/*.spec.ts"
+  ]
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/tsconfig.spec.json b/src/pybind/mgr/dashboard/frontend/src/tsconfig.spec.json
new file mode 100644 (file)
index 0000000..63d89ff
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/spec",
+    "baseUrl": "./",
+    "module": "commonjs",
+    "target": "es5",
+    "types": [
+      "jasmine",
+      "node"
+    ]
+  },
+  "files": [
+    "test.ts"
+  ],
+  "include": [
+    "**/*.spec.ts",
+    "**/*.d.ts"
+  ]
+}
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 (file)
index 0000000..ef5c7bd
--- /dev/null
@@ -0,0 +1,5 @@
+/* SystemJS module definition */
+declare var module: NodeModule;
+interface NodeModule {
+  id: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/tsconfig.json b/src/pybind/mgr/dashboard/frontend/tsconfig.json
new file mode 100644 (file)
index 0000000..a6c016b
--- /dev/null
@@ -0,0 +1,19 @@
+{
+  "compileOnSave": false,
+  "compilerOptions": {
+    "outDir": "./dist/out-tsc",
+    "sourceMap": true,
+    "declaration": false,
+    "moduleResolution": "node",
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "target": "es5",
+    "typeRoots": [
+      "node_modules/@types"
+    ],
+    "lib": [
+      "es2017",
+      "dom"
+    ]
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/tslint.json b/src/pybind/mgr/dashboard/frontend/tslint.json
new file mode 100644 (file)
index 0000000..d2486f1
--- /dev/null
@@ -0,0 +1,180 @@
+{
+  "rulesDirectory": [
+    "node_modules/codelyzer"
+  ],
+  "extends": [
+    "tslint-eslint-rules"
+  ],
+  "rules": {
+    "no-consecutive-blank-lines": true,
+    "arrow-return-shorthand": true,
+    "callable-types": true,
+    "class-name": true,
+    "comment-format": [
+      true,
+      "check-space"
+    ],
+    "curly": true,
+    "eofline": true,
+    "forin": true,
+    "import-blacklist": [
+      true,
+      "rxjs",
+      "rxjs/Rx"
+    ],
+    "import-spacing": true,
+    "indent": [
+      true,
+      "spaces"
+    ],
+    "interface-over-type-literal": true,
+    "label-position": true,
+    "max-line-length": [
+      true,
+      100
+    ],
+    "member-access": false,
+    "member-ordering": [
+      true,
+      {
+        "order": [
+          "static-field",
+          "instance-field",
+          "static-method",
+          "instance-method"
+        ]
+      }
+    ],
+    "no-arg": true,
+    "no-bitwise": true,
+    "no-console": [
+      true,
+      "debug",
+      "info",
+      "time",
+      "timeEnd",
+      "trace"
+    ],
+    "no-construct": true,
+    "no-debugger": true,
+    "no-duplicate-super": true,
+    "no-empty": false,
+    "no-empty-interface": true,
+    "no-eval": true,
+    "no-inferrable-types": [
+      true,
+      "ignore-params"
+    ],
+    "no-misused-new": true,
+    "no-non-null-assertion": true,
+    "no-shadowed-variable": true,
+    "no-string-literal": false,
+    "no-string-throw": true,
+    "no-switch-case-fall-through": true,
+    "no-trailing-whitespace": true,
+    "no-unnecessary-initializer": true,
+    "no-unused-expression": true,
+    "no-use-before-declare": true,
+    "no-var-keyword": true,
+    "object-literal-sort-keys": false,
+    "one-line": [
+      true,
+      "check-open-brace",
+      "check-catch",
+      "check-else",
+      "check-whitespace"
+    ],
+    "prefer-const": true,
+    "quotemark": [
+      true,
+      "single"
+    ],
+    "radix": true,
+    "semicolon": [
+      true,
+      "always"
+    ],
+    "triple-equals": [
+      true,
+      "allow-null-check"
+    ],
+    "typedef-whitespace": [
+      true,
+      {
+        "call-signature": "nospace",
+        "index-signature": "nospace",
+        "parameter": "nospace",
+        "property-declaration": "nospace",
+        "variable-declaration": "nospace"
+      }
+    ],
+    "unified-signatures": true,
+    "variable-name": [
+      true,
+      "check-format",
+      "allow-snake-case"
+    ],
+    "whitespace": [
+      true,
+      "check-branch",
+      "check-decl",
+      "check-operator",
+      "check-separator",
+      "check-type",
+      "check-module"
+    ],
+    "directive-selector": [
+      true,
+      "attribute",
+      "cd",
+      "camelCase"
+    ],
+    "component-selector": [
+      true,
+      "element",
+      "cd",
+      "kebab-case"
+    ],
+    "angular-whitespace": [true, "check-interpolation", "check-semicolon"],
+    "no-output-on-prefix": true,
+    "use-input-property-decorator": true,
+    "use-output-property-decorator": true,
+    "use-host-property-decorator": true,
+    "no-attribute-parameter-decorator": true,
+    "no-input-rename": true,
+    "no-output-rename": true,
+    "use-life-cycle-interface": true,
+    "use-pipe-transform-interface": true,
+    "component-class-suffix": true,
+    "directive-class-suffix": true,
+    "no-forward-ref": true,
+    "no-output-named-after-standard-event": true,
+    "ordered-imports": true,
+    "no-extra-semi": true,
+    "ter-no-irregular-whitespace": true,
+    "no-multi-spaces": true,
+    "brace-style": [
+      true,
+      "1tbs",
+      {
+        "allowSingleLine": false
+      }
+    ],
+    "ter-indent": [
+      true,
+      2,
+      {
+        "SwitchCase": 1,
+        "FunctionDeclaration": {
+          "body": 1,
+          "parameters": "first"
+        },
+        "FunctionExpression": {
+          "body": 1,
+          "parameters": "first"
+        }
+      }
+    ],
+    "space-in-parens": [true, "never"]
+  }
+}
diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py
new file mode 100644 (file)
index 0000000..6877bd8
--- /dev/null
@@ -0,0 +1,292 @@
+# -*- coding: utf-8 -*-
+"""
+openATTIC mgr plugin (based on CherryPy)
+"""
+from __future__ import absolute_import
+
+import errno
+import os
+import socket
+try:
+    from urlparse import urljoin
+except ImportError:
+    from urllib.parse import urljoin
+try:
+    import cherrypy
+except ImportError:
+    # To be picked up and reported by .can_run()
+    cherrypy = None
+
+from mgr_module import MgrModule, MgrStandbyModule
+
+if 'COVERAGE_ENABLED' in os.environ:
+    import coverage
+    _cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)))
+    _cov.start()
+
+# pylint: disable=wrong-import-position
+from . import logger, mgr
+from .controllers.auth import Auth
+from .tools import load_controllers, json_error_page, SessionExpireAtBrowserCloseTool, \
+                   NotificationQueue, RequestLoggingTool
+from .settings import options_command_list, handle_option_command
+
+
+# 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
+
+
+def prepare_url_prefix(url_prefix):
+    """
+    return '' if no prefix, or '/prefix' without slash in the end.
+    """
+    url_prefix = urljoin('/', url_prefix)
+    return url_prefix.rstrip('/')
+
+
+class Module(MgrModule):
+    """
+    dashboard module entrypoint
+    """
+
+    COMMANDS = [
+        {
+            'cmd': 'dashboard set-login-credentials '
+                   'name=username,type=CephString '
+                   'name=password,type=CephString',
+            'desc': 'Set the login credentials',
+            'perm': 'w'
+        },
+        {
+            'cmd': 'dashboard set-session-expire '
+                   'name=seconds,type=CephInt',
+            'desc': 'Set the session expire timeout',
+            'perm': 'w'
+        }
+    ]
+    COMMANDS.extend(options_command_list())
+
+    @property
+    def url_prefix(self):
+        return self._url_prefix
+
+    def __init__(self, *args, **kwargs):
+        super(Module, self).__init__(*args, **kwargs)
+        mgr.init(self)
+        self._url_prefix = ''
+
+    @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: incomplete build?"
+
+        return True, ""
+
+    @classmethod
+    def get_frontend_path(cls):
+        current_dir = os.path.dirname(os.path.abspath(__file__))
+        return os.path.join(current_dir, 'frontend/dist')
+
+    def configure_cherrypy(self):
+        server_addr = self.get_localized_config('server_addr', '::')
+        server_port = self.get_localized_config('server_port', '8080')
+        if server_addr is None:
+            raise RuntimeError(
+                'no server_addr configured; '
+                'try "ceph config-key put mgr/{}/{}/server_addr <ip>"'
+                .format(self.module_name, self.get_mgr_id()))
+        self.log.info('server_addr: %s server_port: %s', server_addr,
+                      server_port)
+
+        self._url_prefix = prepare_url_prefix(self.get_config('url_prefix',
+                                                              default=''))
+
+        # Initialize custom handlers.
+        cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
+        cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
+        cherrypy.tools.request_logging = RequestLoggingTool()
+
+        # 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
+        }
+        cherrypy.config.update(config)
+
+        config = {
+            '/': {
+                'tools.staticdir.on': True,
+                'tools.staticdir.dir': self.get_frontend_path(),
+                'tools.staticdir.index': 'index.html'
+            }
+        }
+
+        # Publish the URI that others may use to access the service we're
+        # about to start serving
+        self.set_uri("http://{0}:{1}{2}/".format(
+            socket.getfqdn() if server_addr == "::" else server_addr,
+            server_port,
+            self.url_prefix
+        ))
+
+        cherrypy.tree.mount(Module.ApiRoot(self), '{}/api'.format(self.url_prefix))
+        cherrypy.tree.mount(Module.StaticRoot(), '{}/'.format(self.url_prefix), config=config)
+
+    def serve(self):
+        if 'COVERAGE_ENABLED' in os.environ:
+            _cov.start()
+        self.configure_cherrypy()
+
+        cherrypy.engine.start()
+        NotificationQueue.start_queue()
+        logger.info('Waiting for engine...')
+        cherrypy.engine.block()
+        if 'COVERAGE_ENABLED' in os.environ:
+            _cov.stop()
+            _cov.save()
+        logger.info('Engine done')
+
+    def shutdown(self):
+        super(Module, self).shutdown()
+        logger.info('Stopping server...')
+        NotificationQueue.stop()
+        cherrypy.engine.exit()
+        logger.info('Stopped server')
+
+    def handle_command(self, cmd):
+        res = handle_option_command(cmd)
+        if res[0] != -errno.ENOSYS:
+            return res
+        if cmd['prefix'] == 'dashboard set-login-credentials':
+            Auth.set_login_credentials(cmd['username'], cmd['password'])
+            return 0, 'Username and password updated', ''
+        elif cmd['prefix'] == 'dashboard set-session-expire':
+            self.set_config('session-expire', str(cmd['seconds']))
+            return 0, 'Session expiration timeout updated', ''
+
+        return (-errno.EINVAL, '', 'Command not found \'{0}\''
+                .format(cmd['prefix']))
+
+    def notify(self, notify_type, notify_id):
+        NotificationQueue.new_notification(notify_type, notify_id)
+
+    class ApiRoot(object):
+
+        _cp_config = {
+            'tools.sessions.on': True,
+            'tools.authenticate.on': True
+        }
+
+        def __init__(self, mgrmod):
+            self.ctrls = load_controllers()
+            logger.debug('Loaded controllers: %s', self.ctrls)
+
+            first_level_ctrls = [ctrl for ctrl in self.ctrls
+                                 if '/' not in ctrl._cp_path_]
+            multi_level_ctrls = set(self.ctrls).difference(first_level_ctrls)
+
+            for ctrl in first_level_ctrls:
+                logger.info('Adding controller: %s -> /api/%s', ctrl.__name__,
+                            ctrl._cp_path_)
+                inst = ctrl()
+                setattr(Module.ApiRoot, ctrl._cp_path_, inst)
+
+            for ctrl in multi_level_ctrls:
+                path_parts = ctrl._cp_path_.split('/')
+                path = '/'.join(path_parts[:-1])
+                key = path_parts[-1]
+                parent_ctrl_classes = [c for c in self.ctrls
+                                       if c._cp_path_ == path]
+                if len(parent_ctrl_classes) != 1:
+                    logger.error('No parent controller found for %s! '
+                                 'Please check your path in the ApiController '
+                                 'decorator!', ctrl)
+                else:
+                    inst = ctrl()
+                    setattr(parent_ctrl_classes[0], key, inst)
+
+        @cherrypy.expose
+        def index(self):
+            tpl = """API Endpoints:<br>
+            <ul>
+            {lis}
+            </ul>
+            """
+            endpoints = ['<li><a href="{}">{}</a></li>'.format(ctrl._cp_path_, ctrl.__name__) for
+                         ctrl in self.ctrls]
+            return tpl.format(lis='\n'.join(endpoints))
+
+    class StaticRoot(object):
+        pass
+
+
+class StandbyModule(MgrStandbyModule):
+    def serve(self):
+        server_addr = self.get_localized_config('server_addr', '::')
+        server_port = self.get_localized_config('server_port', '7000')
+        if server_addr is None:
+            msg = 'no server_addr configured; try "ceph config-key set ' \
+                  'mgr/dashboard/server_addr <ip>"'
+            raise RuntimeError(msg)
+        self.log.info("server_addr: %s server_port: %s",
+                      server_addr, server_port)
+        cherrypy.config.update({
+            'server.socket_host': server_addr,
+            'server.socket_port': int(server_port),
+            'engine.autoreload.on': False
+        })
+
+        module = self
+
+        class Root(object):
+            @cherrypy.expose
+            def index(self):
+                active_uri = module.get_active_uri()
+                if active_uri:
+                    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)
+
+        url_prefix = prepare_url_prefix(self.get_config('url_prefix',
+                                                        default=''))
+        cherrypy.tree.mount(Root(), "{}/".format(url_prefix), {})
+        self.log.info("Starting engine...")
+        cherrypy.engine.start()
+        self.log.info("Waiting for engine...")
+        cherrypy.engine.wait(state=cherrypy.engine.states.STOPPED)
+        self.log.info("Engine done.")
+
+    def shutdown(self):
+        self.log.info("Stopping server...")
+        cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
+        cherrypy.engine.stop()
+        self.log.info("Stopped server")
diff --git a/src/pybind/mgr/dashboard/requirements.txt b/src/pybind/mgr/dashboard/requirements.txt
new file mode 100644 (file)
index 0000000..f6191ea
--- /dev/null
@@ -0,0 +1,32 @@
+astroid==1.6.1
+attrs==17.4.0
+backports.functools-lru-cache==1.4
+cheroot==6.0.0
+CherryPy==13.1.0
+configparser==3.5.0
+coverage==4.4.2
+enum34==1.1.6
+funcsigs==1.0.2
+isort==4.2.15
+lazy-object-proxy==1.3.1
+mccabe==0.6.1
+mock==2.0.0
+more-itertools==4.1.0
+pbr==3.1.1
+pluggy==0.6.0
+portend==2.2
+py==1.5.2
+pycodestyle==2.3.1
+pycparser==2.18
+pylint==1.8.2
+pytest==3.3.2
+pytest-cov==2.5.1
+python-bcrypt==0.3.2
+pytz==2017.3
+requests==2.18.4
+singledispatch==3.4.0.3
+six==1.11.0
+tempora==1.10
+tox==2.9.1
+virtualenv==15.1.0
+wrapt==1.10.11
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 (executable)
index 0000000..8f86401
--- /dev/null
@@ -0,0 +1,106 @@
+#!/usr/bin/env bash
+
+# run from ./
+
+# creating temp directory to store virtualenv and teuthology
+TEMP_DIR=`mktemp -d`
+
+get_cmake_variable() {
+    local variable=$1
+    grep "$variable" CMakeCache.txt | cut -d "=" -f 2
+}
+
+read -r -d '' TEUTHOLOFY_PY_REQS <<EOF
+apache-libcloud==2.2.1 \
+asn1crypto==0.22.0 \
+bcrypt==3.1.4 \
+certifi==2018.1.18 \
+cffi==1.10.0 \
+chardet==3.0.4 \
+configobj==5.0.6 \
+cryptography==2.1.4 \
+enum34==1.1.6 \
+gevent==1.2.2 \
+greenlet==0.4.13 \
+idna==2.5 \
+ipaddress==1.0.18 \
+Jinja2==2.9.6 \
+manhole==1.5.0 \
+MarkupSafe==1.0 \
+netaddr==0.7.19 \
+packaging==16.8 \
+paramiko==2.4.0 \
+pexpect==4.4.0 \
+psutil==5.4.3 \
+ptyprocess==0.5.2 \
+pyasn1==0.2.3 \
+pycparser==2.17 \
+PyNaCl==1.2.1 \
+pyparsing==2.2.0 \
+python-dateutil==2.6.1 \
+PyYAML==3.12 \
+requests==2.18.4 \
+six==1.10.0 \
+urllib3==1.22
+EOF
+
+
+CURR_DIR=`pwd`
+
+cd $TEMP_DIR
+
+virtualenv --python=/usr/bin/python venv
+source venv/bin/activate
+eval pip install $TEUTHOLOFY_PY_REQS
+pip install -r $CURR_DIR/requirements.txt
+deactivate
+
+git clone https://github.com/ceph/teuthology.git
+
+cd $CURR_DIR
+cd ../../../../build
+
+CEPH_MGR_PY_VERSION_MAJOR=$(get_cmake_variable MGR_PYTHON_VERSION | cut -d '.' -f1)
+if [ -n "$CEPH_MGR_PY_VERSION_MAJOR" ]; then
+    CEPH_PY_VERSION_MAJOR=${CEPH_MGR_PY_VERSION_MAJOR}
+else
+    if [ $(get_cmake_variable WITH_PYTHON2) = ON ]; then
+        CEPH_PY_VERSION_MAJOR=2
+    else
+        CEPH_PY_VERSION_MAJOR=3
+    fi
+fi
+
+export COVERAGE_ENABLED=true
+export COVERAGE_FILE=.coverage.mgr.dashboard
+
+MGR=2 RGW=1 ../src/vstart.sh -n -d
+sleep 10
+
+source $TEMP_DIR/venv/bin/activate
+BUILD_DIR=`pwd`
+
+if [ "$#" -gt 0 ]; then
+  TEST_CASES=""
+  for t in "$@"; do
+    TEST_CASES="$TESTS_CASES $t"
+  done
+else
+  TEST_CASES=`for i in \`ls $BUILD_DIR/../qa/tasks/mgr/dashboard/test_*\`; do F=$(basename $i); M="${F%.*}"; echo -n " tasks.mgr.dashboard.$M"; done`
+  TEST_CASES="tasks.mgr.test_dashboard $TEST_CASES"
+fi
+
+export PATH=$BUILD_DIR/bin:$PATH
+export LD_LIBRARY_PATH=$BUILD_DIR/lib/cython_modules/lib.${CEPH_PY_VERSION_MAJOR}/:$BUILD_DIR/lib
+export PYTHONPATH=$TEMP_DIR/teuthology:$BUILD_DIR/../qa:$BUILD_DIR/lib/cython_modules/lib.${CEPH_PY_VERSION_MAJOR}/
+eval python ../qa/tasks/vstart_runner.py $TEST_CASES
+
+deactivate
+killall ceph-mgr
+sleep 10
+../src/stop.sh
+sleep 5
+
+cd $CURR_DIR
+rm -rf $TEMP_DIR
+
diff --git a/src/pybind/mgr/dashboard/run-frontend-unittests.sh b/src/pybind/mgr/dashboard/run-frontend-unittests.sh
new file mode 100755 (executable)
index 0000000..f2a50c4
--- /dev/null
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd $CEPH_ROOT/src/pybind/mgr/dashboard/frontend
+
+npm run build -- --prod
+npm run test -- --browsers PhantomJS --watch=false
+npm run lint
diff --git a/src/pybind/mgr/dashboard/run-tox.sh b/src/pybind/mgr/dashboard/run-tox.sh
new file mode 100755 (executable)
index 0000000..5f24bb6
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+# run from ./ or from ../
+: ${MGR_DASHBOARD_VIRTUALENV:=/tmp/mgr-dashboard-virtualenv}
+: ${WITH_PYTHON3:=ON}
+test -d dashboard && cd dashboard
+
+if [ -e tox.ini ]; then
+    TOX_PATH=`readlink -f tox.ini`
+else
+    TOX_PATH=`readlink -f $(dirname $0)/tox.ini`
+fi
+
+if [ -z $CEPH_BUILD_DIR ]; then
+    export CEPH_BUILD_DIR=$(dirname ${TOX_PATH})
+fi
+
+source ${MGR_DASHBOARD_VIRTUALENV}/bin/activate
+
+if [ "$WITH_PYTHON3" = "ON" ]; then
+  ENV_LIST="cov-init,py27,py3,cov-report,lint"
+else
+  ENV_LIST="cov-init,py27,cov-report,lint"
+fi
+
+tox -c ${TOX_PATH} -e $ENV_LIST
+
diff --git a/src/pybind/mgr/dashboard/services/__init__.py b/src/pybind/mgr/dashboard/services/__init__.py
new file mode 100644 (file)
index 0000000..139759b
--- /dev/null
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
diff --git a/src/pybind/mgr/dashboard/services/ceph_service.py b/src/pybind/mgr/dashboard/services/ceph_service.py
new file mode 100644 (file)
index 0000000..cb27e1e
--- /dev/null
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import time
+import collections
+from collections import defaultdict
+
+from .. import mgr
+
+
+class CephService(object):
+    @classmethod
+    def get_service_map(cls, service_name):
+        service_map = {}
+        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(cls, service_name, service_id):
+        for server in mgr.list_servers():
+            for service in server['services']:
+                if service['type'] == service_name:
+                    inst_id = service['id']
+                    if inst_id == service_id:
+                        metadata = mgr.get_metadata(service_name, inst_id)
+                        status = mgr.get_daemon_status(service_name, inst_id)
+                        return {
+                            'id': inst_id,
+                            'type': service_name,
+                            'hostname': server['hostname'],
+                            'metadata': metadata,
+                            'status': status
+                        }
+        return None
+
+    @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 = defaultdict(lambda: defaultdict(
+            lambda: collections.deque(maxlen=10)))
+
+        df = mgr.get("df")
+        pool_stats_dict = dict([(p['id'], p['stats']) for p in df['pools']])
+        now = time.time()
+        for pool_id, stats in pool_stats_dict.items():
+            for stat_name, stat_val in stats.items():
+                pool_stats[pool_id][stat_name].appendleft((now, stat_val))
+
+        for pool in pools:
+            pool['pg_status'] = pg_summary['by_pool'][pool['pool'].__str__()]
+            stats = pool_stats[pool['pool']]
+            s = {}
+
+            def get_rate(series):
+                if len(series) >= 2:
+                    return (float(series[0][1]) - float(series[1][1])) / \
+                        (float(series[0][0]) - float(series[1][0]))
+                return 0
+
+            for stat_name, stat_series in stats.items():
+                s[stat_name] = {
+                    'latest': stat_series[0][1],
+                    'rate': get_rate(stat_series),
+                    'series': [i for i in stat_series]
+                }
+            pool['stats'] = s
+            pools_w_stats.append(pool)
+        return pools_w_stats
diff --git a/src/pybind/mgr/dashboard/settings.py b/src/pybind/mgr/dashboard/settings.py
new file mode 100644 (file)
index 0000000..4f68fbb
--- /dev/null
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import errno
+import inspect
+from six import add_metaclass
+
+from . import mgr
+
+
+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)
+    """
+    pass
+
+
+class SettingsMeta(type):
+    def __getattr__(cls, attr):
+        default, stype = getattr(Options, attr)
+        return stype(mgr.get_config(attr, default))
+
+    def __setattr__(cls, attr, value):
+        if not attr.startswith('_') and hasattr(Options, attr):
+            mgr.set_config(attr, str(value))
+        else:
+            setattr(SettingsMeta, attr, value)
+
+
+# pylint: disable=no-init
+@add_metaclass(SettingsMeta)
+class Settings(object):
+    pass
+
+
+def _options_command_map():
+    def filter_attr(member):
+        return not inspect.isroutine(member)
+
+    cmd_map = {}
+    for option, value 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('_', '-'))
+        cmd_map[key_get] = {'name': option, 'type': None}
+        cmd_map[key_set] = {'name': option, 'type': value[1]}
+    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 not opt['type']:
+            cmd_list.append({
+                'cmd': '{}'.format(cmd),
+                'desc': 'Get the {} option value'.format(opt['name']),
+                'perm': 'r'
+            })
+        else:
+            cmd_list.append({
+                'cmd': '{} name=value,type={}'
+                       .format(cmd, py2ceph(opt['type'])),
+                'desc': 'Set the {} option value'.format(opt['name']),
+                'perm': 'w'
+            })
+
+    return cmd_list
+
+
+def handle_option_command(cmd):
+    if cmd['prefix'] not in _OPTIONS_COMMAND_MAP:
+        return (-errno.ENOSYS, '', "Command not found '{}'".format(cmd['prefix']))
+
+    opt = _OPTIONS_COMMAND_MAP[cmd['prefix']]
+    if not opt['type']:
+        # get option
+        return 0, str(getattr(Settings, opt['name'])), ''
+
+    # set option
+    setattr(Settings, opt['name'], opt['type'](cmd['value']))
+    return 0, 'Option {} updated'.format(opt['name']), ''
diff --git a/src/pybind/mgr/dashboard/tests/__init__.py b/src/pybind/mgr/dashboard/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/tests/helper.py b/src/pybind/mgr/dashboard/tests/helper.py
new file mode 100644 (file)
index 0000000..effe21d
--- /dev/null
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=W0212
+from __future__ import absolute_import
+
+import json
+
+import cherrypy
+from cherrypy.test import helper
+
+from ..controllers.auth import Auth
+from ..tools import json_error_page, SessionExpireAtBrowserCloseTool
+
+
+class ControllerTestCase(helper.CPWebCase):
+    def __init__(self, *args, **kwargs):
+        cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
+        cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
+        cherrypy.config.update({'error_page.default': json_error_page})
+        super(ControllerTestCase, self).__init__(*args, **kwargs)
+
+    def _request(self, url, method, data=None):
+        if not data:
+            b = None
+            h = None
+        else:
+            b = json.dumps(data)
+            h = [('Content-Type', 'application/json'),
+                 ('Content-Length', str(len(b)))]
+        self.getPage(url, method=method, body=b, headers=h)
+
+    def _get(self, url):
+        self._request(url, 'GET')
+
+    def _post(self, url, data=None):
+        self._request(url, 'POST', data)
+
+    def _delete(self, url, data=None):
+        self._request(url, 'DELETE', data)
+
+    def _put(self, url, data=None):
+        self._request(url, 'PUT', data)
+
+    def jsonBody(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):
+        """Fail if value != self.body."""
+        json_body = self.jsonBody()
+        if data != json_body:
+            if msg is None:
+                msg = 'expected body:\n%r\n\nactual body:\n%r' % (
+                    data, json_body)
+            self._handlewebError(msg)
diff --git a/src/pybind/mgr/dashboard/tests/test_notification.py b/src/pybind/mgr/dashboard/tests/test_notification.py
new file mode 100644 (file)
index 0000000..bca27f9
--- /dev/null
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import random
+import time
+import unittest
+
+
+from ..tools import NotificationQueue
+
+
+class Listener(object):
+    def __init__(self):
+        NotificationQueue.register(self.log_type1, 'type1')
+        NotificationQueue.register(self.log_type2, 'type2')
+        NotificationQueue.register(self.log_type1_3, ['type1', 'type3'])
+        NotificationQueue.register(self.log_all)
+        self.type1 = []
+        self.type2 = []
+        self.type1_3 = []
+        self.all = []
+
+        # 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.append(val)
+
+    def log_type2(self, val):
+        self.type2.append(val)
+
+    def log_type1_3(self, val):
+        self.type1_3.append(val)
+
+    def log_all(self, val):
+        self.all.append(val)
+
+    def clear(self):
+        self.type1 = []
+        self.type2 = []
+        self.type1_3 = []
+        self.all = []
+
+
+class NotificationQueueTest(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.listener = Listener()
+
+    def setUp(self):
+        self.listener.clear()
+
+    def test_invalid_register(self):
+        with self.assertRaises(Exception) as ctx:
+            NotificationQueue.register(None, 1)
+        self.assertEqual(str(ctx.exception),
+                         "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])
+
+    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, 500):
+            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)
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 (file)
index 0000000..c283c94
--- /dev/null
@@ -0,0 +1,88 @@
+from __future__ import absolute_import
+
+import json
+import mock
+
+import cherrypy
+
+from .. import mgr
+from ..controllers.summary import Summary
+from ..controllers.rbd_mirroring import RbdMirror
+from .helper 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_local_count': 5,
+        'image_remote_count': 6,
+        'image_error_count': 7,
+        'image_warning_count': 8,
+        'name': 'pool_name'
+    }
+}
+
+mock_get_daemon_status = {
+    'json': json.dumps(_status)
+}
+
+mock_osd_map = {
+    'pools': [{
+        'pool_name': 'rbd',
+        'application_metadata': {'rbd'}
+    }]
+}
+
+
+class RbdMirroringControllerTest(ControllerTestCase):
+
+    @classmethod
+    def setup_server(cls):
+        mgr.list_servers.return_value = mock_list_servers
+        mgr.get_metadata.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': []},
+
+        }[key]
+        mgr.url_prefix = ''
+        mgr.get_mgr_id.return_value = 0
+        mgr.have_mon_connection.return_value = True
+
+        RbdMirror._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
+
+        Summary._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
+
+        cherrypy.tree.mount(RbdMirror(), '/api/test/rbdmirror')
+        cherrypy.tree.mount(Summary(), '/api/test/summary')
+
+    @mock.patch('dashboard.controllers.rbd_mirroring.rbd')
+    def test_default(self, rbd_mock):  # pylint: disable=W0613
+        self._get('/api/test/rbdmirror')
+        result = self.jsonBody()
+        self.assertStatus(200)
+        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.rbd_mirroring.rbd')
+    def test_summary(self, rbd_mock):  # pylint: disable=W0613
+        """We're also testing `summary`, as it also uses code from `rbd_mirroring.py`"""
+        self._get('/api/test/summary')
+        self.assertStatus(200)
+
+        summary = self.jsonBody()['rbd_mirroring']
+        self.assertEqual(summary, {'errors': 0, 'warnings': 1})
diff --git a/src/pybind/mgr/dashboard/tests/test_settings.py b/src/pybind/mgr/dashboard/tests/test_settings.py
new file mode 100644 (file)
index 0000000..92fcf7f
--- /dev/null
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import errno
+import unittest
+
+from .. import mgr
+from .. import settings
+from ..settings import Settings, handle_option_command
+
+
+class SettingsTest(unittest.TestCase):
+    CONFIG_KEY_DICT = {}
+
+    @classmethod
+    def setUpClass(cls):
+        # pylint: disable=protected-access
+        settings.Options.GRAFANA_API_HOST = ('localhost', str)
+        settings.Options.GRAFANA_API_PORT = (3000, int)
+        settings._OPTIONS_COMMAND_MAP = settings._options_command_map()
+
+    @classmethod
+    def mock_set_config(cls, attr, val):
+        cls.CONFIG_KEY_DICT[attr] = val
+
+    @classmethod
+    def mock_get_config(cls, attr, default):
+        return cls.CONFIG_KEY_DICT.get(attr, default)
+
+    def setUp(self):
+        self.CONFIG_KEY_DICT.clear()
+        mgr.set_config.side_effect = self.mock_set_config
+        mgr.get_config.side_effect = self.mock_get_config
+        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')
+
+    def test_set_setting(self):
+        Settings.GRAFANA_API_HOST = 'grafanahost'
+        self.assertEqual(Settings.GRAFANA_API_HOST, 'grafanahost')
+
+    def test_get_cmd(self):
+        r, out, err = handle_option_command(
+            {'prefix': 'dashboard get-grafana-api-port'})
+        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'})
+        self.assertEqual(r, 0)
+        self.assertEqual(out, 'Option GRAFANA_API_PORT updated')
+        self.assertEqual(err, '')
+
+    def test_inv_cmd(self):
+        r, out, err = handle_option_command(
+            {'prefix': 'dashboard get-non-existent-option'})
+        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'})
+        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'})
+        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'")
diff --git a/src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py b/src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py
new file mode 100644 (file)
index 0000000..88077cb
--- /dev/null
@@ -0,0 +1,71 @@
+from __future__ import absolute_import
+
+import cherrypy
+
+from .. import mgr
+from ..controllers.tcmu_iscsi import TcmuIscsi
+from .helper import ControllerTestCase
+
+mocked_servers = [{
+    'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)',
+    'hostname': 'ceph-dev',
+    'services': [{'id': 'a:b', 'type': 'tcmu-runner'}]
+}]
+
+mocked_metadata = {
+    'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)',
+    'pool_name': 'pool1',
+    'image_name': 'image1',
+    'image_id': '42',
+    'optimized_since': 100.0,
+}
+
+mocked_get_daemon_status = {
+    'lock_owner': 'true',
+}
+
+mocked_get_counter = {
+    'librbd-42-pool1-image1.lock_acquired_time': [[10000.0, 10000.0]],
+    'librbd-42-pool1-image1.rd': 43,
+    'librbd-42-pool1-image1.wr': 44,
+    'librbd-42-pool1-image1.rd_bytes': 45,
+    'librbd-42-pool1-image1.wr_bytes': 46,
+}
+
+mocked_get_rate = 47
+
+
+class TcmuIscsiControllerTest(ControllerTestCase):
+
+    @classmethod
+    def setup_server(cls):
+        mgr.list_servers.return_value = mocked_servers
+        mgr.get_metadata.return_value = mocked_metadata
+        mgr.get_daemon_status.return_value = mocked_get_daemon_status
+        mgr.get_counter.return_value = mocked_get_counter
+        mgr.get_rate.return_value = mocked_get_rate
+        mgr.url_prefix = ''
+        TcmuIscsi._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
+
+        cherrypy.tree.mount(TcmuIscsi(), "/api/test/tcmu")
+
+    def test_list(self):
+        self._get('/api/test/tcmu')
+        self.assertStatus(200)
+        self.assertJsonBody({
+            'daemons': [{
+                'server_hostname': 'ceph-dev',
+                'version': 'ceph version 13.0.0-5083- () mimic (dev)',
+                'optimized_paths': 1, 'non_optimized_paths': 0}],
+            'images': [{
+                'device_id': 'b',
+                'pool_name': 'pool1',
+                'name': 'image1',
+                'id': '42', 'optimized_paths': ['ceph-dev'],
+                'non_optimized_paths': [],
+                'optimized_since': 1e-05,
+                'stats': {'rd': 47, 'rd_bytes': 47, 'wr': 47, 'wr_bytes': 47},
+                'stats_history': {
+                    'rd': 43, 'wr': 44, 'rd_bytes': 45, 'wr_bytes': 46}
+            }]
+        })
diff --git a/src/pybind/mgr/dashboard/tests/test_tools.py b/src/pybind/mgr/dashboard/tests/test_tools.py
new file mode 100644 (file)
index 0000000..ca4d904
--- /dev/null
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import cherrypy
+from cherrypy.lib.sessions import RamSession
+from mock import patch
+
+from .helper import ControllerTestCase
+from ..tools import RESTController
+
+
+# pylint: disable=W0613
+class FooResource(RESTController):
+    elems = []
+
+    def list(self, *vpath, **params):
+        return FooResource.elems
+
+    def create(self, data, *args, **kwargs):
+        FooResource.elems.append(data)
+        return data
+
+    def get(self, key, *args, **kwargs):
+        if args:
+            return {'detail': (key, args)}
+        return FooResource.elems[int(key)]
+
+    def delete(self, key):
+        del FooResource.elems[int(key)]
+
+    def bulk_delete(self):
+        FooResource.elems = []
+
+    def set(self, data, key):
+        FooResource.elems[int(key)] = data
+        return dict(key=key, **data)
+
+
+class FooArgs(RESTController):
+    @RESTController.args_from_json
+    def set(self, code, name):
+        return {'code': code, 'name': name}
+
+
+# pylint: disable=C0102
+class Root(object):
+    foo = FooResource()
+    fooargs = FooArgs()
+
+
+class RESTControllerTest(ControllerTestCase):
+
+    @classmethod
+    def setup_server(cls):
+        cherrypy.tree.mount(Root())
+
+    def test_empty(self):
+        self._delete("/foo")
+        self.assertStatus(204)
+        self._get("/foo")
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'application/json')
+        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', 'application/json')
+
+            self._get("/foo")
+            self.assertStatus('200 OK')
+            self.assertHeader('Content-Type', 'application/json')
+            self.assertJsonBody([data] * 5)
+
+            self._put('/foo/0', {'newdata': 'newdata'})
+            self.assertStatus('200 OK')
+            self.assertHeader('Content-Type', 'application/json')
+            self.assertJsonBody({'newdata': 'newdata', 'key': '0'})
+
+    def test_not_implemented(self):
+        self._put("/foo")
+        self.assertStatus(405)
+        body = self.jsonBody()
+        self.assertIsInstance(body, dict)
+        assert body['detail'] == 'Method not implemented.'
+        assert '405' in body['status']
+        assert 'traceback' in body
+
+    def test_args_from_json(self):
+        self._put("/fooargs/hello", {'name': 'world'})
+        self.assertJsonBody({'code': 'hello', 'name': 'world'})
+
+    def test_detail_route(self):
+        self._get('/foo/1/detail')
+        self.assertJsonBody({'detail': ['1', ['detail']]})
+
+        self._post('/foo/1/detail', 'post-data')
+        self.assertStatus(405)
diff --git a/src/pybind/mgr/dashboard/tools.py b/src/pybind/mgr/dashboard/tools.py
new file mode 100644 (file)
index 0000000..8775a70
--- /dev/null
@@ -0,0 +1,531 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=W0212
+from __future__ import absolute_import
+
+import collections
+import datetime
+import importlib
+import inspect
+import json
+import os
+import pkgutil
+import sys
+import time
+import threading
+
+import cherrypy
+
+from . import logger
+
+
+def ApiController(path):
+    def decorate(cls):
+        cls._cp_controller_ = True
+        cls._cp_path_ = path
+        config = {
+            'tools.sessions.on': True,
+            'tools.sessions.name': Session.NAME,
+            'tools.session_expire_at_browser_close.on': True
+        }
+        if not hasattr(cls, '_cp_config'):
+            cls._cp_config = {}
+        if 'tools.authenticate.on' not in cls._cp_config:
+            config['tools.authenticate.on'] = False
+        cls._cp_config.update(config)
+        return cls
+    return decorate
+
+
+def AuthRequired(enabled=True):
+    def decorate(cls):
+        if not hasattr(cls, '_cp_config'):
+            cls._cp_config = {
+                'tools.authenticate.on': enabled
+            }
+        else:
+            cls._cp_config['tools.authenticate.on'] = enabled
+        return cls
+    return decorate
+
+
+def load_controllers():
+    # setting sys.path properly when not running under the mgr
+    dashboard_dir = os.path.dirname(os.path.realpath(__file__))
+    mgr_dir = os.path.dirname(dashboard_dir)
+    if mgr_dir not in sys.path:
+        sys.path.append(mgr_dir)
+
+    controllers = []
+    ctrls_path = '{}/controllers'.format(dashboard_dir)
+    mods = [mod for _, mod, _ in pkgutil.iter_modules([ctrls_path])]
+    for mod_name in mods:
+        mod = importlib.import_module('.controllers.{}'.format(mod_name),
+                                      package='dashboard')
+        for _, cls in mod.__dict__.items():
+            # Controllers MUST be derived from the class BaseController.
+            if inspect.isclass(cls) and issubclass(cls, BaseController) and \
+                    hasattr(cls, '_cp_controller_'):
+                controllers.append(cls)
+
+    return controllers
+
+
+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))
+
+
+class BaseController(object):
+    """
+    Base class for all controllers providing API endpoints.
+    """
+
+
+class RequestLoggingTool(cherrypy.Tool):
+    def __init__(self):
+        cherrypy.Tool.__init__(self, 'before_handler', self.request_begin,
+                               priority=95)
+
+    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 _get_user(self):
+        if hasattr(cherrypy.serving, 'session'):
+            return cherrypy.session.get(Session.USERNAME)
+        return None
+
+    def request_begin(self):
+        req = cherrypy.request
+        user = self._get_user()
+        if user:
+            logger.debug("[%s:%s] [%s] [%s] %s", req.remote.ip,
+                         req.remote.port, req.method, user, req.path_info)
+        else:
+            logger.debug("[%s:%s] [%s] %s", req.remote.ip,
+                         req.remote.port, req.method, req.path_info)
+
+    def request_error(self):
+        self._request_log(logger.error)
+        logger.error(cherrypy.response.body)
+
+    def request_end(self):
+        status = cherrypy.response.status[:3]
+        if status in ["401"]:
+            # log unauthorized accesses
+            self._request_log(logger.warning)
+        else:
+            self._request_log(logger.info)
+
+    def _format_bytes(self, num):
+        units = ['B', 'K', 'M', 'G']
+
+        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 = self._get_user()
+        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", req.remote.ip,
+                      req.remote.port, req.method, status,
+                      "{0:.3f}s".format(lat), length, req.path_info)
+
+
+# pylint: disable=too-many-instance-attributes
+class ViewCache(object):
+    VALUE_OK = 0
+    VALUE_STALE = 1
+    VALUE_NONE = 2
+    VALUE_EXCEPTION = 3
+
+    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):
+            try:
+                t0 = time.time()
+                val = self.fn(*self.args, **self.kwargs)
+                t1 = time.time()
+            except Exception as ex:
+                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.datetime.now()
+                    self._view.getter_thread = None
+                    self._view.exception = None
+
+            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()
+
+        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.datetime.now()
+                if self.value_when and now - self.value_when < datetime.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()
+
+                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
+                        return ViewCache.VALUE_EXCEPTION, self.exception
+                    return ViewCache.VALUE_OK, self.value
+                elif 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
+                return ViewCache.VALUE_NONE, None
+
+    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)
+        return wrapper
+
+
+class RESTController(BaseController):
+    """
+    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)
+    * delete(key)
+
+    Test with curl:
+
+    curl -H "Content-Type: application/json" -X POST \
+         -d '{"username":"xyz","password":"xyz"}'  http://127.0.0.1:8080/foo
+    curl http://127.0.0.1:8080/foo
+    curl http://127.0.0.1:8080/foo/0
+
+    """
+
+    def _not_implemented(self, is_sub_path):
+        methods = [method
+                   for ((method, _is_element), (meth, _))
+                   in self._method_mapping.items()
+                   if _is_element == is_sub_path is not None and hasattr(self, meth)]
+        cherrypy.response.headers['Allow'] = ','.join(methods)
+        raise cherrypy.HTTPError(405, 'Method not implemented.')
+
+    _method_mapping = {
+        ('GET', False): ('list', 200),
+        ('PUT', False): ('bulk_set', 200),
+        ('PATCH', False): ('bulk_set', 200),
+        ('POST', False): ('create', 201),
+        ('DELETE', False): ('bulk_delete', 204),
+        ('GET', True): ('get', 200),
+        ('PUT', True): ('set', 200),
+        ('PATCH', True): ('set', 200),
+        ('DELETE', True): ('delete', 204),
+    }
+
+    def _get_method(self, vpath):
+        is_sub_path = bool(len(vpath))
+        try:
+            method_name, status_code = self._method_mapping[
+                (cherrypy.request.method, is_sub_path)]
+        except KeyError:
+            self._not_implemented(is_sub_path)
+        method = getattr(self, method_name, None)
+        if not method:
+            self._not_implemented(is_sub_path)
+        return method, status_code
+
+    @cherrypy.expose
+    def default(self, *vpath, **params):
+        method, status_code = self._get_method(vpath)
+
+        if cherrypy.request.method not in ['GET', 'DELETE']:
+            method = RESTController._takes_json(method)
+
+        if cherrypy.request.method != 'DELETE':
+            method = RESTController._returns_json(method)
+
+        cherrypy.response.status = status_code
+
+        return method(*vpath, **params)
+
+    @staticmethod
+    def args_from_json(func):
+        func._args_from_json_ = True
+        return func
+
+    # pylint: disable=W1505
+    @staticmethod
+    def _takes_json(func):
+        def inner(*args, **kwargs):
+            content_length = int(cherrypy.request.headers['Content-Length'])
+            body = cherrypy.request.body.read(content_length)
+            if not body:
+                raise cherrypy.HTTPError(400, 'Empty body. Content-Length={}'
+                                         .format(content_length))
+            try:
+                data = json.loads(body.decode('utf-8'))
+            except Exception as e:
+                raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
+                                         .format(str(e)))
+            if hasattr(func, '_args_from_json_'):
+                if sys.version_info > (3, 0):
+                    f_args = list(inspect.signature(func).parameters.keys())
+                else:
+                    f_args = inspect.getargspec(func).args[1:]
+                n_args = []
+                for arg in args:
+                    n_args.append(arg)
+                for arg in f_args:
+                    if arg in data:
+                        n_args.append(data[arg])
+                        data.pop(arg)
+                kwargs.update(data)
+                return func(*n_args, **kwargs)
+
+            return func(data, *args, **kwargs)
+        return inner
+
+    @staticmethod
+    def _returns_json(func):
+        def inner(*args, **kwargs):
+            cherrypy.response.headers['Content-Type'] = 'application/json'
+            ret = func(*args, **kwargs)
+            return json.dumps(ret).encode('utf8')
+        return inner
+
+    @staticmethod
+    def split_vpath(vpath):
+        if not vpath:
+            return None, None
+        if len(vpath) == 1:
+            return vpath[0], None
+        return vpath[0], vpath[1]
+
+
+class Session(object):
+    """
+    This class contains all relevant settings related to cherrypy.session.
+    """
+    NAME = 'session_id'
+
+    # The keys used to store the information in the cherrypy.session.
+    USERNAME = '_username'
+    TS = '_ts'
+    EXPIRE_AT_BROWSER_CLOSE = '_expire_at_browser_close'
+
+    # The default values.
+    DEFAULT_EXPIRE = 1200.0
+
+
+class SessionExpireAtBrowserCloseTool(cherrypy.Tool):
+    """
+    A CherryPi Tool which takes care that the cookie does not expire
+    at browser close if the 'Keep me logged in' checkbox was selected
+    on the login page.
+    """
+    def __init__(self):
+        cherrypy.Tool.__init__(self, 'before_finalize', self._callback)
+
+    def _callback(self):
+        # Shall the cookie expire at browser close?
+        expire_at_browser_close = cherrypy.session.get(
+            Session.EXPIRE_AT_BROWSER_CLOSE, True)
+        logger.debug("expire at browser close: %s", expire_at_browser_close)
+        if expire_at_browser_close:
+            # Get the cookie and its name.
+            cookie = cherrypy.response.cookie
+            name = cherrypy.request.config.get(
+                'tools.sessions.name', Session.NAME)
+            # Make the cookie a session cookie by purging the
+            # fields 'expires' and 'max-age'.
+            logger.debug("expire at browser close: removing 'expires' and 'max-age'")
+            if name in cookie:
+                del cookie[name]['expires']
+                del cookie[name]['max-age']
+
+
+class NotificationQueue(threading.Thread):
+    _ALL_TYPES_ = '__ALL__'
+    _listeners = collections.defaultdict(set)
+    _lock = threading.Lock()
+    _cond = threading.Condition()
+    _queue = collections.deque()
+    _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()
+        logger.debug("starting notification queue")
+        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()
+        logger.debug("waiting for notification queue to finish")
+        instance.join()
+        logger.debug("notification queue stopped")
+
+    @classmethod
+    def register(cls, func, types=None):
+        """Registers function to listen for notifications
+
+        If the second parameter `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)
+            types (str|list): the single type to listen, or a list of types
+        """
+        with cls._lock:
+            if not types:
+                cls._listeners[cls._ALL_TYPES_].add(func)
+                return
+            if isinstance(types, str):
+                cls._listeners[types].add(func)
+            elif isinstance(types, list):
+                for typ in types:
+                    cls._listeners[typ].add(func)
+            else:
+                raise Exception("types param is neither a string nor a list")
+
+    @classmethod
+    def new_notification(cls, notify_type, notify_value):
+        cls._queue.append((notify_type, notify_value))
+        with cls._cond:
+            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_])
+            for listener in listeners:
+                listener(notify_value)
+
+    def run(self):
+        logger.debug("notification queue started")
+        while self._running:
+            private_buffer = []
+            logger.debug("NQ: processing queue: %s", len(self._queue))
+            try:
+                while True:
+                    private_buffer.append(self._queue.popleft())
+            except IndexError:
+                pass
+            self.notify_listeners(private_buffer)
+            with self._cond:
+                self._cond.wait(1.0)
+        # flush remaining events
+        logger.debug("NQ: flush remaining events: %s", len(self._queue))
+        self.notify_listeners(self._queue)
+        self._queue.clear()
+        logger.debug("notification queue finished")
diff --git a/src/pybind/mgr/dashboard/tox.ini b/src/pybind/mgr/dashboard/tox.ini
new file mode 100644 (file)
index 0000000..743a8a6
--- /dev/null
@@ -0,0 +1,40 @@
+[tox]
+envlist = cov-init,py27,py3,cov-report,lint
+skipsdist = true
+
+[testenv]
+deps=-r{toxinidir}/requirements.txt
+setenv=
+    UNITTEST=true
+    WEBTEST_INTERACTIVE=false
+    COVERAGE_FILE= .coverage.{envname}
+    PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3:{toxinidir}/../../../../build/lib/cython_modules/lib.2
+    LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
+    PATH = {toxinidir}/../../../../build/bin:$PATH
+commands=
+    {envbindir}/py.test --cov=. --cov-report= --junitxml=junit.{envname}.xml --doctest-modules controllers/rbd.py tests/
+
+[testenv:cov-init]
+setenv =
+    COVERAGE_FILE = .coverage
+deps = coverage
+commands =
+    coverage erase
+
+[testenv:cov-report]
+setenv =
+    COVERAGE_FILE = .coverage
+deps = coverage
+commands =
+    coverage combine
+    coverage report
+    coverage xml
+
+[testenv:lint]
+setenv =
+    PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3:{toxinidir}/../../../../build/lib/cython_modules/lib.2
+    LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
+deps=-r{toxinidir}/requirements.txt
+commands=
+    pylint --rcfile=.pylintrc --jobs=5 . module.py tools.py controllers tests services
+    pycodestyle --max-line-length=100 --exclude=python2.7,.tox,venv,frontend --ignore=E402,E121,E123,E126,E226,E24,E704,W503 .
diff --git a/src/pybind/mgr/dashboard_v2/.coveragerc b/src/pybind/mgr/dashboard_v2/.coveragerc
deleted file mode 100644 (file)
index 29a6319..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-[run]
-omit = tests/*
-       */python*/*
-       ceph_module_mock.py
-       __init__.py
-       */mgr_module.py
-
diff --git a/src/pybind/mgr/dashboard_v2/.editorconfig b/src/pybind/mgr/dashboard_v2/.editorconfig
deleted file mode 100644 (file)
index a831e3d..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-# 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_v2/.gitignore b/src/pybind/mgr/dashboard_v2/.gitignore
deleted file mode 100644 (file)
index b636948..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-.coverage*
-htmlcov
-.tox
-coverage.xml
-junit*xml
-__pycache__
-.cache
-ceph.conf
-wheelhouse*
-
-# IDE
-.vscode
-.idea
-*.egg
-
-# virtualenv
-venv
diff --git a/src/pybind/mgr/dashboard_v2/.pylintrc b/src/pybind/mgr/dashboard_v2/.pylintrc
deleted file mode 100644 (file)
index ab5d1f8..0000000
+++ /dev/null
@@ -1,548 +0,0 @@
-[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=rados,rbd
-
-# 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.
-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=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,
-        locally-enabled,
-        file-ignored,
-        suppressed-message,
-        useless-suppression,
-        deprecated-pragma,
-        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,
-        missing-docstring,
-        invalid-name,
-        no-self-use,
-        too-few-public-methods,
-        no-member,
-        fixme
-
-# 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=
-
-# 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=no
-
-# 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=7
-
-# 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_v2/CMakeLists.txt b/src/pybind/mgr/dashboard_v2/CMakeLists.txt
deleted file mode 100644 (file)
index 15e2f27..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-set(MGR_DASHBOARD_V2_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/mgr-dashboard_v2-virtualenv)
-
-add_custom_target(mgr-dashboard_v2-test-venv
-  COMMAND
-  ${CMAKE_SOURCE_DIR}/src/tools/setup-virtualenv.sh ${MGR_DASHBOARD_V2_VIRTUALENV} &&
-  ${MGR_DASHBOARD_V2_VIRTUALENV}/bin/pip install --no-index --use-wheel --find-links=file:${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/wheelhouse -r requirements.txt
-  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2
-  COMMENT "dashboard_v2 tests virtualenv is being created")
-add_dependencies(tests mgr-dashboard_v2-test-venv)
-
-if(WITH_MGR_DASHBOARD_V2_FRONTEND AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
-  find_program(NPM_BIN
-    NAMES npm
-    HINTS $ENV{NPM_ROOT}/bin)
-  if(NOT NPM_BIN)
-    message(FATAL_ERROR "WITH_MGR_DASHBOARD_V2_FRONTEND set, but npm not found")
-  endif()
-
-add_custom_command(
-  OUTPUT "${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend/node_modules"
-  COMMAND ${NPM_BIN} install
-  DEPENDS frontend/package.json
-  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend
-  COMMENT "dashboard_v2 frontend dependencies are being installed"
-)
-
-add_custom_target(mgr-dashboard_v2-frontend-deps
-  DEPENDS frontend/node_modules
-  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend
-)
-
-# Glob some frontend files. With CMake 3.6, this can be simplified
-# to *.ts *.html. Just add:
-# list(FILTER frontend_src INCLUDE REGEX "frontend/src")
-file(
-  GLOB_RECURSE frontend_src
-  frontend/src/*.ts
-  frontend/src/*.html
-  frontend/src/*/*.ts
-  frontend/src/*/*.html
-  frontend/src/*/*/*.ts
-  frontend/src/*/*/*.html
-  frontend/src/*/*/*/*.ts
-  frontend/src/*/*/*/*.html
-  frontend/src/*/*/*/*/*.ts
-  frontend/src/*/*/*/*/*.html
-  frontend/src/*/*/*/*/*/*.ts
-  frontend/src/*/*/*/*/*/*.html)
-
-if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
-  set(npm_command ${NPM_BIN} run build -- --prod)
-else()
-  set(npm_command ${NPM_BIN} run build)
-endif()
-
-add_custom_command(
-  OUTPUT "${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend/dist"
-  COMMAND ${npm_command}
-  DEPENDS ${frontend_src} frontend/node_modules
-  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend
-  COMMENT "dashboard_v2 frontend is being created"
-)
-add_custom_target(mgr-dashboard_v2-frontend-build
-  DEPENDS frontend/dist mgr-dashboard_v2-frontend-deps
-  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend
-)
-add_dependencies(ceph-mgr mgr-dashboard_v2-frontend-build)
-endif(WITH_MGR_DASHBOARD_V2_FRONTEND AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
diff --git a/src/pybind/mgr/dashboard_v2/HACKING.rst b/src/pybind/mgr/dashboard_v2/HACKING.rst
deleted file mode 100644 (file)
index 610e117..0000000
+++ /dev/null
@@ -1,510 +0,0 @@
-Dashboard v2 Developer Documentation
-====================================
-
-Frontend Development
---------------------
-
-Before you can start the dashboard from within a development environment,  you
-will need to generate the frontend code and either use a compiled and running
-Ceph cluster (e.g. started by ``vstart.sh``) or the standalone development web
-server.
-
-The build process is based on `Node.js <https://nodejs.org/>`_ and requires the
-`Node Package Manager <https://www.npmjs.com/>`_ ``npm`` to be installed.
-
-Prerequisites
-~~~~~~~~~~~~~
-
-Run ``npm install`` in directory ``src/pybind/mgr/dashboard_v2/frontend`` to
-install the required packages locally.
-
-.. note::
-
-  If you do not have the `Angular CLI <https://github.com/angular/angular-cli>`_
-  installed globally, then you need to execute ``ng`` commands with an
-  additional ``npm run`` before it.
-
-Setting up a Development Server
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Create the ``proxy.conf.json`` file based on ``proxy.conf.json.sample``.
-
-Run ``npm start -- --proxy-config proxy.conf.json`` for a dev server.
-Navigate to ``http://localhost:4200/``. The app will automatically
-reload if you change any of the source files.
-
-Code Scaffolding
-~~~~~~~~~~~~~~~~
-
-Run ``ng generate component component-name`` to generate a new
-component. You can also use
-``ng generate directive|pipe|service|class|guard|interface|enum|module``.
-
-Build the Project
-~~~~~~~~~~~~~~~~~
-
-Run ``npm run build`` to build the project. The build artifacts will be
-stored in the ``dist/`` directory. Use the ``-prod`` flag for a
-production build. Navigate to ``http://localhost:8080``.
-
-Running Unit Tests
-~~~~~~~~~~~~~~~~~~
-
-Run ``npm run test`` to execute the unit tests via `Karma
-<https://karma-runner.github.io>`_.
-
-Running End-to-End Tests
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Run ``npm run e2e`` to execute the end-to-end tests via
-`Protractor <http://www.protractortest.org/>`__.
-
-Further Help
-~~~~~~~~~~~~
-
-To get more help on the Angular CLI use ``ng help`` or go check out the
-`Angular CLI
-README <https://github.com/angular/angular-cli/blob/master/README.md>`__.
-
-Example of a Generator
-~~~~~~~~~~~~~~~~~~~~~~
-
-::
-
-    # Create module 'Core'
-    src/app> ng generate module core -m=app --routing
-
-    # Create module 'Auth' under module 'Core'
-    src/app/core> ng generate module auth -m=core --routing
-    or, alternatively:
-    src/app> ng generate module core/auth -m=core --routing
-
-    # Create component 'Login' under module 'Auth'
-    src/app/core/auth> ng generate component login -m=core/auth
-    or, alternatively:
-    src/app> ng generate component core/auth/login -m=core/auth
-
-Frontend Typescript Code Style Guide Recommendations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Group the imports based on its source and separate them with a blank
-line.
-
-The source groups can be either from Angular, external or internal.
-
-Example:
-
-.. code:: javascript
-
-    import { Component } from '@angular/core';
-    import { Router } from '@angular/router';
-
-    import { ToastsManager } from 'ng2-toastr';
-
-    import { Credentials } from '../../../shared/models/credentials.model';
-    import { HostService } from './services/host.service';
-
-
-Backend Development
--------------------
-
-The Python backend code of this module requires a number of Python modules to be
-installed. They are listed in file ``requirements.txt``. Using `pip
-<https://pypi.python.org/pypi/pip>`_ you may install all required dependencies
-by issuing ``pip install -r requirements.txt`` in directory
-``src/pybind/mgr/dashboard_v2``.
-
-If you're using the `ceph-dev-docker development environment
-<https://github.com/ricardoasmarques/ceph-dev-docker/>`_, simply run
-``./install_deps.sh`` from the toplevel directory to install them.
-
-Unit Testing and Linting
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-We included a ``tox`` configuration file that will run the unit tests under
-Python 2 or 3, as well as linting tools to guarantee the uniformity of code.
-
-You need to install ``tox`` and ``coverage`` before running it. To install the
-packages in your system, either install it via your operating system's package
-management tools, e.g. by running ``dnf install python-tox python-coverage`` on
-Fedora Linux.
-
-Alternatively, you can use Python's native package installation method::
-
-  $ pip install tox
-  $ pip install coverage
-
-The unit tests must run against a real Ceph cluster (no mocks are used). This
-has the advantage of catching bugs originated from changes in the internal Ceph
-code.
-
-Our ``tox.ini`` script will start a ``vstart`` Ceph cluster before running the
-python unit tests, and then it stops the cluster after the tests are run. Of
-course this implies that you have built/compiled Ceph previously.
-
-To run tox, run the following command in the root directory (where ``tox.ini``
-is located)::
-
-  $ PATH=../../../../build/bin:$PATH tox
-
-We also collect coverage information from the backend code. You can check the
-coverage information provided by the tox output, or by running the following
-command after tox has finished successfully::
-
-  $ coverage html
-
-This command will create a directory ``htmlcov`` with an HTML representation of
-the code coverage of the backend.
-
-You can also run a single step of the tox script (aka tox environment), for
-instance if you only want to run the linting tools, do::
-
-  $ PATH=../../../../build/bin:$PATH tox -e lint
-
-How to run a single unit test without using ``tox``?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-When developing the code of a controller and respective test code, it's useful
-to be able to run that single test file without going through the whole ``tox``
-workflow.
-
-Since the tests must run against a real Ceph cluster, the first thing is to have
-a Ceph cluster running. For that we can leverage the tox environment that starts
-a Ceph cluster::
-
-  $ PATH=../../../../build/bin:$PATH tox -e ceph-cluster-start
-
-The command above uses ``vstart.sh`` script to start a Ceph cluster and
-automatically enables the ``dashboard_v2`` module, and configures its cherrypy
-web server to listen in port ``9865``.
-
-After starting the Ceph cluster we can run our test file using ``py.test`` like
-this::
-
-  DASHBOARD_V2_PORT=9865 UNITTEST=true py.test -s tests/test_mycontroller.py
-
-You can run tests multiple times without having to start and stop the Ceph
-cluster.
-
-After you finish your tests, you can stop the Ceph cluster using another tox
-environment::
-
-  $ tox -e ceph-cluster-stop
-
-How to add a new controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you want to add a new endpoint to the backend, you just need to add a
-class derived from ``BaseController`` decorated with ``ApiController`` in a
-Python file located under the ``controllers`` directory. The Dashboard module
-will automatically load your new controller upon start.
-
-For example create a file ``ping2.py`` under ``controllers`` directory with the
-following code::
-
-  import cherrypy
-  from ..tools import ApiController, BaseController
-
-  @ApiController('ping2')
-  class Ping2(BaseController):
-    @cherrypy.expose
-    def default(self, *args):
-      return "Hello"
-
-Every path given in the ``ApiController`` decorator will automatically be
-prefixed with ``api``. After reloading the Dashboard module you can access the
-above mentioned controller by pointing your browser to
-http://mgr_hostname:8080/api/ping2.
-
-It is also possible to have nested controllers. The ``RgwController`` uses
-this technique to make the daemons available through the URL
-http://mgr_hostname:8080/api/rgw/daemon::
-
-  @ApiController('rgw')
-  @AuthRequired()
-  class Rgw(RESTController):
-    pass
-
-
-  @ApiController('rgw/daemon')
-  @AuthRequired()
-  class RgwDaemon(RESTController):
-
-    def list(self):
-      pass
-
-
-Note that paths must be unique and that a path like ``rgw/daemon`` has to have
-a parent ``rgw``. Otherwise it won't work.
-
-How does the RESTController work?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We also provide a simple mechanism to create REST based controllers using the
-``RESTController`` class. Any class which inherits from ``RESTController`` will,
-by default, return JSON.
-
-The ``RESTController`` is basically an additional abstraction layer which eases
-and unifies the work with collections. A collection is just an array of objects
-with a specific type. ``RESTController`` enables some default mappings of
-request types and given parameters to specific method names. This may sound
-complicated at first, but it's fairly easy. Lets have look at the following
-example::
-
-  import cherrypy
-  from ..tools import ApiController, RESTController
-
-  @ApiController('ping2')
-  class Ping2(RESTController):
-    def list(self):
-      return {"msg": "Hello"}
-
-    def get(self, id):
-      return self.objects[id]
-
-In this case, the ``list`` method is automatically used for all requests to
-``api/ping2`` where no additional argument is given and where the request type
-is ``GET``. If the request is given an additional argument, the ID in our
-case, it won't map to ``list`` anymore but to ``get`` and return the element
-with the given ID (assuming that ``self.objects`` has been filled before). The
-same applies to other request types:
-
-+--------------+------------+----------------+-------------+
-| Request type | Arguments  | Method         | Status Code |
-+==============+============+================+=============+
-| GET          | No         | list           | 200         |
-+--------------+------------+----------------+-------------+
-| PUT          | No         | bulk_set       | 200         |
-+--------------+------------+----------------+-------------+
-| PATCH        | No         | bulk_set       | 200         |
-+--------------+------------+----------------+-------------+
-| POST         | No         | create         | 201         |
-+--------------+------------+----------------+-------------+
-| DELETE       | No         | bulk_delete    | 204         |
-+--------------+------------+----------------+-------------+
-| GET          | Yes        | get            | 200         |
-+--------------+------------+----------------+-------------+
-| PUT          | Yes        | set            | 200         |
-+--------------+------------+----------------+-------------+
-| PATCH        | Yes        | set            | 200         |
-+--------------+------------+----------------+-------------+
-| DELETE       | Yes        | delete         | 204         |
-+--------------+------------+----------------+-------------+
-
-How to restrict access to a controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you require that only authenticated users can access you controller, just
-add the ``AuthRequired`` decorator to your controller class.
-
-Example::
-
-  import cherrypy
-  from ..tools import ApiController, AuthRequired, RESTController
-
-
-  @ApiController('ping2')
-  @AuthRequired()
-  class Ping2(RESTController):
-    def list(self):
-      return {"msg": "Hello"}
-
-Now only authenticated users will be able to "ping" your controller.
-
-
-How to access the manager module instance from a controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We provide the manager module instance as a global variable that can be
-imported in any module. We also provide a logger instance in the same way.
-
-Example::
-
-  import cherrypy
-  from .. import logger, mgr
-  from ..tools import ApiController, RESTController
-
-
-  @ApiController('servers')
-  class Servers(RESTController):
-    def list(self):
-      logger.debug('Listing available servers')
-      return {'servers': mgr.list_servers()}
-
-
-How to write a unit test for a controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We provide a test helper class called ``ControllerTestCase`` to easily create
-unit tests for your controller.
-
-If we want to write a unit test for the above ``Ping2`` controller, create a
-``test_ping2.py`` file under the ``tests`` directory with the following code::
-
-  from .helper import ControllerTestCase
-  from .controllers.ping2 import Ping2
-
-
-  class Ping2Test(ControllerTestCase):
-      @classmethod
-      def setup_test(cls):
-          Ping2._cp_config['tools.authentica.on'] = False
-
-      def test_ping2(self):
-          self._get("/api/ping2")
-          self.assertStatus(200)
-          self.assertJsonBody({'msg': 'Hello'})
-
-The ``ControllerTestCase`` class will call the dashboard module code that loads
-the controllers and initializes the CherryPy webserver. Then it will call the
-``setup_test()`` class method to execute additional instructions that each test
-case needs to add to the test.
-In the example above we use the ``setup_test()`` method to disable the
-authentication handler for the ``Ping2`` controller.
-
-
-How to listen for manager notifications in a controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The manager notifies the modules of several types of cluster events, such
-as cluster logging event, etc...
-
-Each module has a "global" handler function called ``notify`` that the manager
-calls to notify the module. But this handler function must not block or spend
-too much time processing the event notification.
-For this reason we provide a notification queue that controllers can register
-themselves with to receive cluster notifications.
-
-The example below represents a controller that implements a very simple live
-log viewer page::
-
-  from __future__ import absolute_import
-
-  import collections
-
-  import cherrypy
-
-  from ..tools import ApiController, BaseController, NotificationQueue
-
-
-  @ApiController('livelog')
-  class LiveLog(BaseController):
-      log_buffer = collections.deque(maxlen=1000)
-
-      def __init__(self):
-          super(LiveLog, self).__init__()
-          NotificationQueue.register(self.log, 'clog')
-
-      def log(self, log_struct):
-          self.log_buffer.appendleft(log_struct)
-
-      @cherrypy.expose
-      def default(self):
-          ret = '<html><meta http-equiv="refresh" content="2" /><body>'
-          for l in self.log_buffer:
-              ret += "{}<br>".format(l)
-          ret += "</body></html>"
-          return ret
-
-As you can see above, the ``NotificationQueue`` class provides a register
-method that receives the function as its first argument, and receives the
-"notification type" as the second argument.
-You can omit the second argument of the ``register`` method, and in that case
-you are registering to listen all notifications of any type.
-
-Here is an list of notification types (these might change in the future) that
-can be used:
-
-* ``clog``: cluster log notifications
-* ``command``: notification when a command issued by ``MgrModule.send_command``
-  completes
-* ``perf_schema_update``: perf counters schema update
-* ``mon_map``: monitor map update
-* ``fs_map``: cephfs map update
-* ``osd_map``: OSD map update
-* ``service_map``: services (RGW, RBD-Mirror, etc.) map update
-* ``mon_status``: monitor status regular update
-* ``health``: health status regular update
-* ``pg_summary``: regular update of PG status information
-
-
-How to write a unit test when a controller accesses a Ceph module?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Consider the following example that implements a controller that retrieves the
-list of RBD images of the ``rbd`` pool::
-
-  import rbd
-  from .. import mgr
-  from ..tools import ApiController, RESTController
-
-
-  @ApiController('rbdimages')
-  class RbdImages(RESTController):
-      def __init__(self):
-          self.ioctx = mgr.rados.open_ioctx('rbd')
-          self.rbd = rbd.RBD()
-
-      def list(self):
-          return [{'name': n} for n in self.rbd.list(self.ioctx)]
-
-In the example above, we want to mock the return value of the ``rbd.list``
-function, so that we can test the JSON response of the controller.
-
-The unit test code will look like the following::
-
-  import mock
-  from .helper import ControllerTestCase
-
-
-  class RbdImagesTest(ControllerTestCase):
-      @mock.patch('rbd.RBD.list')
-      def test_list(self, rbd_list_mock):
-          rbd_list_mock.return_value = ['img1', 'img2']
-          self._get('/api/rbdimages')
-          self.assertJsonBody([{'name': 'img1'}, {'name': 'img2'}])
-
-
-
-How to add a new configuration setting?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you need to store some configuration setting for a new feature, we already
-provide an easy mechanism for you to specify/use the new config setting.
-
-For instance, if you want to add a new configuration setting to hold the
-email address of the dashboard admin, just add a setting name as a class
-attribute to the ``Options`` class in the ``settings.py`` file::
-
-  # ...
-  class Options(object):
-    # ...
-
-    ADMIN_EMAIL_ADDRESS = ('admin@admin.com', str)
-
-The value of the class attribute is a pair composed by the default value for that
-setting, and the python type of the value.
-
-By declaring the ``ADMIN_EMAIL_ADDRESS`` class attribute, when you restart the
-dashboard plugin, you will atomatically gain two additional CLI commands to
-get and set that setting::
-
-  $ ceph dashboard get-admin-email-address
-  $ ceph dashboard set-admin-email-address <value>
-
-To access, or modify the config setting value from your Python code, either
-inside a controller or anywhere else, you just need to import the ``Settings``
-class and access it like this::
-
-  from settings import Settings
-
-  # ...
-  tmp_var = Settings.ADMIN_EMAIL_ADDRESS
-
-  # ....
-  Settings.ADMIN_EMAIL_ADDRESS = 'myemail@admin.com'
-
-The settings management implementation will make sure that if you change a
-setting value from the Python code you will see that change when accessing
-that setting from the CLI and vice-versa.
-
diff --git a/src/pybind/mgr/dashboard_v2/README.rst b/src/pybind/mgr/dashboard_v2/README.rst
deleted file mode 100644 (file)
index 6ee5984..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-Dashboard and Administration Module for Ceph Manager (aka "Dashboard v2")
-=========================================================================
-
-Overview
---------
-
-The original Ceph Manager Dashboard that was shipped with Ceph "Luminous"
-started out as a simple read-only view into various run-time information and
-performance data of a Ceph cluster.
-
-However, there is a `growing demand <http://pad.ceph.com/p/mimic-dashboard>`_
-for adding more web-based management capabilities, to make it easier for
-administrators that prefer a WebUI over the command line.
-
-This module is an ongoing project to add a native web based monitoring and
-administration application to Ceph Manager. It aims at becoming a successor of
-the existing dashboard, which provides read-only functionality and uses a
-simpler architecture to achieve the original goal.
-
-The code and architecture of this module is derived from and inspired by the
-`openATTIC Ceph management and monitoring tool <https://openattic.org/>`_ (both
-the backend and WebUI). The development is actively driven by the team behind
-openATTIC.
-
-The intention is to reuse as much of the existing openATTIC code as possible,
-while adapting it to the different environment. The current openATTIC backend
-implementation is based on Django and the Django REST framework, the Manager
-module's backend code will use the CherryPy framework and a custom REST API
-implementation instead.
-
-The WebUI implementation will be developed using Angular/TypeScript, merging
-both functionality from the existing dashboard as well as adding new
-functionality originally developed for the standalone version of openATTIC.
-
-The porting and migration of the existing openATTIC and dashboard functionality
-will be done in stages. The tasks are currently tracked in the `openATTIC team's
-JIRA instance <https://tracker.openattic.org/browse/OP-3039>`_.
-
-Enabling and Starting the Dashboard
------------------------------------
-
-If you have installed Ceph from distribution packages, the package management
-system should have taken care of installing all the required dependencies.
-
-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 <http://docs.ceph.com/docs/master/dev/>`_ for details on how to
-accomplish this.
-
-Finally, you need to build the dashboard frontend code. See the file
-``HACKING.rst`` in this directory for instructions on setting up the necessary
-development environment.
-
-From within a running Ceph cluster, you can start the Dashboard module by
-running the following command::
-
-  $ ceph mgr module enable dashboard_v2
-
-You can see currently enabled Manager modules with::
-
-  $ ceph mgr module ls
-
-In order to be able to log in, you need to define a username and password, which
-will be stored in the MON's configuration database::
-
-  $ ceph dashboard set-login-credentials <username> <password>
-
-The password will be stored as a hash using ``bcrypt``.
-
-The Dashboard's WebUI should then be reachable on TCP port 8080.
-
-Working on the Dashboard Code
------------------------------
-
-If you're interested in helping with the development of the dashboard, please
-see the file ``HACKING.rst`` for details on how to set up a development
-environment and some other development-related topics.
diff --git a/src/pybind/mgr/dashboard_v2/__init__.py b/src/pybind/mgr/dashboard_v2/__init__.py
deleted file mode 100644 (file)
index f09ef24..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-# -*- coding: utf-8 -*-
-# pylint: disable=wrong-import-position,global-statement,protected-access
-"""
-openATTIC module
-"""
-from __future__ import absolute_import
-
-import os
-
-
-if 'UNITTEST' not in os.environ:
-    class _LoggerProxy(object):
-        def __init__(self):
-            self._logger = None
-
-        def __getattr__(self, item):
-            if self._logger is None:
-                raise AttributeError("logger not initialized")
-            return getattr(self._logger, item)
-
-    class _ModuleProxy(object):
-        def __init__(self):
-            self._mgr = None
-
-        def init(self, module_inst):
-            global logger
-            self._mgr = module_inst
-            logger._logger = self._mgr._logger
-
-        def __getattr__(self, item):
-            if self._mgr is None:
-                raise AttributeError("global manager module instance not initialized")
-            return getattr(self._mgr, item)
-
-    mgr = _ModuleProxy()
-    logger = _LoggerProxy()
-
-    from .module import Module, StandbyModule
-else:
-    import logging
-    logging.basicConfig(level=logging.DEBUG)
-    logger = logging.getLogger(__name__)
-    logging.root.handlers[0].setLevel(logging.DEBUG)
-    os.environ['PATH'] = '{}:{}'.format(os.path.abspath('../../../../build/bin'),
-                                        os.environ['PATH'])
-
-    # Mock ceph module otherwise every module that is involved in a testcase and imports it will
-    # raise an ImportError
-    import sys
-    import mock
-    sys.modules['ceph_module'] = mock.Mock()
-
-    mgr = mock.Mock()
diff --git a/src/pybind/mgr/dashboard_v2/controllers/__init__.py b/src/pybind/mgr/dashboard_v2/controllers/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/controllers/auth.py b/src/pybind/mgr/dashboard_v2/controllers/auth.py
deleted file mode 100644 (file)
index 28a2f28..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import time
-import sys
-
-import bcrypt
-import cherrypy
-
-from ..tools import ApiController, RESTController, Session
-from .. import logger, mgr
-
-
-@ApiController('auth')
-class Auth(RESTController):
-    """
-    Provide login and logout actions.
-
-    Supported config-keys:
-
-      | KEY             | DEFAULT | DESCR                                     |
-      ------------------------------------------------------------------------|
-      | username        | None    | Username                                  |
-      | password        | None    | Password encrypted using bcrypt           |
-      | session-expire  | 1200    | Session will expire after <expires>       |
-      |                           | seconds without activity                  |
-    """
-
-    @RESTController.args_from_json
-    def create(self, username, password, stay_signed_in=False):
-        now = time.time()
-        config_username = mgr.get_config('username', None)
-        config_password = mgr.get_config('password', None)
-        hash_password = Auth.password_hash(password,
-                                           config_password)
-        if username == config_username and hash_password == config_password:
-            cherrypy.session.regenerate()
-            cherrypy.session[Session.USERNAME] = username
-            cherrypy.session[Session.TS] = now
-            cherrypy.session[Session.EXPIRE_AT_BROWSER_CLOSE] = not stay_signed_in
-            logger.debug('Login successful')
-            return {'username': username}
-
-        cherrypy.response.status = 403
-        if config_username is None:
-            logger.warning('No Credentials configured. Need to call `ceph dashboard '
-                           'set-login-credentials <username> <password>` first.')
-        else:
-            logger.debug('Login failed')
-        return {'detail': 'Invalid credentials'}
-
-    def bulk_delete(self):
-        logger.debug('Logout successful')
-        cherrypy.session[Session.USERNAME] = None
-        cherrypy.session[Session.TS] = None
-
-    @staticmethod
-    def password_hash(password, salt_password=None):
-        if not salt_password:
-            salt_password = bcrypt.gensalt()
-        if sys.version_info > (3, 0):
-            return bcrypt.hashpw(password, salt_password)
-        return bcrypt.hashpw(password.encode('utf8'), salt_password)
-
-    @staticmethod
-    def check_auth():
-        username = cherrypy.session.get(Session.USERNAME)
-        if not username:
-            logger.debug('Unauthorized access to %s',
-                         cherrypy.url(relative='server'))
-            raise cherrypy.HTTPError(401, 'You are not authorized to access '
-                                          'that resource')
-        now = time.time()
-        expires = float(mgr.get_config(
-            'session-expire', Session.DEFAULT_EXPIRE))
-        if expires > 0:
-            username_ts = cherrypy.session.get(Session.TS, None)
-            if username_ts and float(username_ts) < (now - expires):
-                cherrypy.session[Session.USERNAME] = None
-                cherrypy.session[Session.TS] = None
-                logger.debug('Session expired')
-                raise cherrypy.HTTPError(401,
-                                         'Session expired. You are not '
-                                         'authorized to access that resource')
-        cherrypy.session[Session.TS] = now
-
-    @staticmethod
-    def set_login_credentials(username, password):
-        mgr.set_config('username', username)
-        hashed_passwd = Auth.password_hash(password)
-        mgr.set_config('password', hashed_passwd)
diff --git a/src/pybind/mgr/dashboard_v2/controllers/cephfs.py b/src/pybind/mgr/dashboard_v2/controllers/cephfs.py
deleted file mode 100644 (file)
index c4786ce..0000000
+++ /dev/null
@@ -1,318 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from collections import defaultdict
-import json
-
-import cherrypy
-from mgr_module import CommandResult
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, BaseController, ViewCache
-
-
-@ApiController('cephfs')
-@AuthRequired()
-class CephFS(BaseController):
-    def __init__(self):
-        super(CephFS, self).__init__()
-
-        # Stateful instances of CephFSClients, hold cached results.  Key to
-        # dict is FSCID
-        self.cephfs_clients = {}
-
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
-    def clients(self, fs_id):
-        fs_id = self.fs_id_to_int(fs_id)
-
-        return self._clients(fs_id)
-
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
-    def data(self, fs_id):
-        fs_id = self.fs_id_to_int(fs_id)
-
-        return self.fs_status(fs_id)
-
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
-    def mds_counters(self, fs_id):
-        """
-        Result format: map of daemon name to map of counter to list of datapoints
-        rtype: dict[str, dict[str, list]]
-        """
-
-        # Opinionated list of interesting performance counters for the GUI --
-        # if you need something else just add it.  See how simple life is
-        # when you don't have to write general purpose APIs?
-        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"
-        ]
-
-        fs_id = self.fs_id_to_int(fs_id)
-
-        result = {}
-        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 cherrypy.HTTPError(400, "Invalid cephfs id {}".format(fs_id))
-
-    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 get_rate(self, daemon_type, daemon_name, stat):
-        data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
-
-        if data and len(data) > 1:
-            return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
-
-        return 0
-
-    # pylint: disable=too-many-locals,too-many-statements,too-many-branches
-    def fs_status(self, fs_id):
-        mds_versions = 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 = self.get_latest("mds", info['name'], "mds.inodes")
-                inos = self.get_latest("mds", info['name'], "mds_mem.ino")
-
-                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 = self.colorize(state, self.GREEN)
-                # else:
-                #     c_state = self.colorize(state, self.YELLOW)
-
-                # Populate based on context of state, e.g. client
-                # ops for an active daemon, replay progress, reconnect
-                # progress
-                activity = ""
-
-                if state == "active":
-                    activity = self.get_rate("mds",
-                                             info['name'],
-                                             "mds_server.handle_client_request")
-
-                metadata = mgr.get_metadata('mds', info['name'])
-                mds_versions[metadata.get('ceph_version', 'unknown')].append(
-                    info['name'])
-                rank_table.append(
-                    {
-                        "rank": rank,
-                        "state": state,
-                        "mds": info['name'],
-                        "activity": activity,
-                        "dns": dns,
-                        "inos": inos
-                    }
-                )
-
-            else:
-                rank_table.append(
-                    {
-                        "rank": rank,
-                        "state": "failed",
-                        "mds": "",
-                        "activity": "",
-                        "dns": 0,
-                        "inos": 0
-                    }
-                )
-
-        # Find the standby replays
-        # pylint: disable=unused-variable
-        for gid_str, daemon_info in mdsmap['info'].iteritems():
-            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.inodes")
-
-            activity = self.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
-                }
-            )
-
-        df = mgr.get("df")
-        pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
-        osdmap = mgr.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 = []
-        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['bytes_used'],
-                "avail": stats['max_avail']
-            })
-
-        standby_table = []
-        for standby in fsmap['standbys']:
-            metadata = mgr.get_metadata('mds', standby['name'])
-            mds_versions[metadata.get('ceph_version', 'unknown')].append(
-                standby['name'])
-
-            standby_table.append({
-                'name': standby['name']
-            })
-
-        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']:
-                client['type'] = "userspace"
-                client['version'] = client['client_metadata']['ceph_version']
-                client['hostname'] = client['client_metadata']['hostname']
-            elif "kernel_version" in client['client_metadata']:
-                client['type'] = "kernel"
-                client['version'] = client['client_metadata']['kernel_version']
-                client['hostname'] = client['client_metadata']['hostname']
-            else:
-                client['type'] = "unknown"
-                client['version'] = ""
-                client['hostname'] = ""
-
-        return {
-            'status': status,
-            'data': clients
-        }
-
-    def get_latest(self, daemon_type, daemon_name, stat):
-        data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
-        if data:
-            return data[-1][1]
-        return 0
-
-
-class CephFSClients(object):
-    def __init__(self, module_inst, fscid):
-        self._module = module_inst
-        self.fscid = fscid
-
-    # pylint: disable=unused-variable
-    @ViewCache()
-    def get(self):
-        mds_spec = "{0}:0".format(self.fscid)
-        result = CommandResult("")
-        self._module.send_command(result, "mds", mds_spec,
-                                  json.dumps({
-                                      "prefix": "session ls",
-                                  }),
-                                  "")
-        r, outb, outs = result.wait()
-        # TODO handle nonzero returns, e.g. when rank isn't active
-        assert r == 0
-        return json.loads(outb)
diff --git a/src/pybind/mgr/dashboard_v2/controllers/cluster_configuration.py b/src/pybind/mgr/dashboard_v2/controllers/cluster_configuration.py
deleted file mode 100644 (file)
index d02027b..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import cherrypy
-
-from .. import mgr
-from ..tools import ApiController, RESTController, AuthRequired
-
-
-@ApiController('cluster_conf')
-@AuthRequired()
-class ClusterConfiguration(RESTController):
-    def list(self):
-        options = mgr.get("config_options")['options']
-        return options
-
-    def get(self, name):
-        for option in mgr.get('config_options')['options']:
-            if option['name'] == name:
-                return option
-
-        raise cherrypy.HTTPError(404)
diff --git a/src/pybind/mgr/dashboard_v2/controllers/dashboard.py b/src/pybind/mgr/dashboard_v2/controllers/dashboard.py
deleted file mode 100644 (file)
index 3457c2f..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import collections
-import json
-
-import cherrypy
-from mgr_module import CommandResult
-
-from .. import mgr
-from ..services.ceph_service import CephService
-from ..tools import ApiController, AuthRequired, BaseController, NotificationQueue
-
-
-LOG_BUFFER_SIZE = 30
-
-
-@ApiController('dashboard')
-@AuthRequired()
-class Dashboard(BaseController):
-    def __init__(self):
-        super(Dashboard, self).__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):
-        result = CommandResult("")
-        mgr.send_command(result, "mon", "", json.dumps({
-            "prefix": "log last",
-            "format": "json",
-            "channel": channel_name,
-            "num": LOG_BUFFER_SIZE
-        }), "")
-        r, outb, outs = result.wait()
-        if r != 0:
-            # Oh well. We won't let this stop us though.
-            self.log.error("Error fetching log history (r={0}, \"{1}\")".format(
-                r, outs))
-        else:
-            try:
-                lines = json.loads(outb)
-            except ValueError:
-                self.log.error("Error decoding log history")
-            else:
-                for l in lines:
-                    buf.appendleft(l)
-
-    # pylint: disable=R0914
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
-    def health(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')
-
-        # Fuse osdmap with pg_summary to get description of pools
-        # including their PG states
-
-        osd_map = self.osd_map()
-
-        pools = CephService.get_pool_list_with_stats()
-
-        # Not needed, skip the effort of transmitting this
-        # to UI
-        del osd_map['pg_temp']
-
-        df = mgr.get("df")
-        df['stats']['total_objects'] = sum(
-            [p['stats']['objects'] for p in df['pools']])
-
-        return {
-            "health": self.health_data(),
-            "mon_status": self.mon_status(),
-            "fs_map": mgr.get('fs_map'),
-            "osd_map": osd_map,
-            "clog": list(self.log_buffer),
-            "audit_log": list(self.audit_buffer),
-            "pools": pools,
-            "mgr_map": mgr.get("mgr_map"),
-            "df": df
-        }
-
-    def mon_status(self):
-        mon_status_data = mgr.get("mon_status")
-        return json.loads(mon_status_data['json'])
-
-    def osd_map(self):
-        osd_map = mgr.get("osd_map")
-
-        assert osd_map is not None
-
-        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 health_data(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
diff --git a/src/pybind/mgr/dashboard_v2/controllers/host.py b/src/pybind/mgr/dashboard_v2/controllers/host.py
deleted file mode 100644 (file)
index 8bef071..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, RESTController
-
-
-@ApiController('host')
-@AuthRequired()
-class Host(RESTController):
-    def list(self):
-        return mgr.list_servers()
diff --git a/src/pybind/mgr/dashboard_v2/controllers/monitor.py b/src/pybind/mgr/dashboard_v2/controllers/monitor.py
deleted file mode 100644 (file)
index ac3bfe4..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-
-import cherrypy
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, BaseController
-
-
-@ApiController('monitor')
-@AuthRequired()
-class Monitor(BaseController):
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
-    def default(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_v2/controllers/osd.py b/src/pybind/mgr/dashboard_v2/controllers/osd.py
deleted file mode 100644 (file)
index 24fca6d..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-
-from mgr_module import CommandResult
-
-from .. import logger, mgr
-from ..tools import ApiController, AuthRequired, RESTController
-
-
-@ApiController('osd')
-@AuthRequired()
-class Osd(RESTController):
-    def get_counter(self, daemon_name, stat):
-        return mgr.get_counter('osd', daemon_name, stat)[stat]
-
-    def get_rate(self, daemon_name, stat):
-        data = self.get_counter(daemon_name, stat)
-        rate = 0
-        if data and len(data) > 1:
-            rate = (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
-        return rate
-
-    def get_latest(self, daemon_name, stat):
-        data = self.get_counter(daemon_name, stat)
-        latest = 0
-        if data and data[-1] and len(data[-1]) == 2:
-            latest = data[-1][1]
-        return latest
-
-    def list(self):
-        osds = self.get_osd_map()
-        # Extending by osd stats information
-        for s in mgr.get('osd_stats')['osd_stats']:
-            osds[str(s['osd'])].update({'osd_stats': s})
-        # Extending by osd node information
-        nodes = mgr.get('osd_map_tree')['nodes']
-        osd_tree = [(str(o['id']), o) for o in nodes if o['id'] >= 0]
-        for o in osd_tree:
-            osds[o[0]].update({'tree': o[1]})
-        # Extending by osd parent node information
-        hosts = [(h['name'], h) for h in nodes if h['id'] < 0]
-        for h in hosts:
-            for o_id in h[1]['children']:
-                if o_id >= 0:
-                    osds[str(o_id)]['host'] = h[1]
-        # Extending by osd histogram data
-        for o_id in osds:
-            o = osds[o_id]
-            o['stats'] = {}
-            o['stats_history'] = {}
-            osd_spec = str(o['osd'])
-            for s in ['osd.op_w', 'osd.op_in_bytes', 'osd.op_r', 'osd.op_out_bytes']:
-                prop = s.split('.')[1]
-                o['stats'][prop] = self.get_rate(osd_spec, s)
-                o['stats_history'][prop] = self.get_counter(osd_spec, s)
-            # Gauge stats
-            for s in ['osd.numpg', 'osd.stat_bytes', 'osd.stat_bytes_used']:
-                o['stats'][s.split('.')[1]] = self.get_latest(osd_spec, s)
-        return osds.values()
-
-    def get_osd_map(self):
-        osds = {}
-        for osd in mgr.get('osd_map')['osds']:
-            osd['id'] = osd['osd']
-            osds[str(osd['id'])] = osd
-        return osds
-
-    def get(self, svc_id):
-        result = CommandResult('')
-        mgr.send_command(result, 'osd', svc_id,
-                         json.dumps({
-                             'prefix': 'perf histogram dump',
-                         }),
-                         '')
-        r, outb, outs = result.wait()
-        if r != 0:
-            histogram = None
-            logger.warning('Failed to load histogram for OSD %s', svc_id)
-            logger.debug(outs)
-            histogram = outs
-        else:
-            histogram = json.loads(outb)
-        return {
-            'osd_map': self.get_osd_map()[svc_id],
-            'osd_metadata': mgr.get_metadata('osd', svc_id),
-            'histogram': histogram,
-        }
diff --git a/src/pybind/mgr/dashboard_v2/controllers/perf_counters.py b/src/pybind/mgr/dashboard_v2/controllers/perf_counters.py
deleted file mode 100644 (file)
index 59692d3..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, RESTController
-
-
-class PerfCounter(RESTController):
-    def __init__(self, service_type):
-        self._service_type = service_type
-
-    def _get_rate(self, daemon_type, daemon_name, stat):
-        data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
-        if data and len(data) > 1:
-            return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
-        return 0
-
-    def _get_latest(self, daemon_type, daemon_name, stat):
-        data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
-        if data:
-            return data[-1][1]
-        return 0
-
-    def get(self, service_id):
-        schema = mgr.get_perf_schema(
-            self._service_type, str(service_id)).values()[0]
-        counters = []
-
-        for key, value in sorted(schema.items()):
-            counter = dict()
-            counter['name'] = str(key)
-            counter['description'] = value['description']
-            # pylint: disable=W0212
-            if mgr._stattype_to_str(value['type']) == 'counter':
-                counter['value'] = self._get_rate(
-                    self._service_type, service_id, key)
-                counter['unit'] = mgr._unit_to_str(value['units'])
-            else:
-                counter['value'] = self._get_latest(
-                    self._service_type, service_id, key)
-                counter['unit'] = ''
-            counters.append(counter)
-
-        return {
-            'service': {
-                'type': self._service_type,
-                'id': service_id
-            },
-            'counters': counters
-        }
-
-
-@ApiController('perf_counters')
-@AuthRequired()
-class PerfCounters(RESTController):
-    def __init__(self):
-        self.mds = PerfCounter('mds')
-        self.mon = PerfCounter('mon')
-        self.osd = PerfCounter('osd')
-        self.rgw = PerfCounter('rgw')
-        self.rbd_mirror = PerfCounter('rbd-mirror')
-        self.mgr = PerfCounter('mgr')
-
-    def list(self):
-        counters = mgr.get_all_perf_counters()
-        return counters
diff --git a/src/pybind/mgr/dashboard_v2/controllers/pool.py b/src/pybind/mgr/dashboard_v2/controllers/pool.py
deleted file mode 100644 (file)
index 2eac9f5..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from ..services.ceph_service import CephService
-from ..tools import ApiController, RESTController, AuthRequired
-
-
-@ApiController('pool')
-@AuthRequired()
-class Pool(RESTController):
-
-    @classmethod
-    def _serialize_pool(cls, pool, attrs):
-        if not attrs or not isinstance(attrs, list):
-            return pool
-
-        res = {}
-        for attr in attrs:
-            if attr not in pool:
-                continue
-            if attr == 'type':
-                res[attr] = {1: 'replicated', 3: 'erasure'}[pool[attr]]
-            else:
-                res[attr] = pool[attr]
-
-        # pool_name is mandatory
-        res['pool_name'] = pool['pool_name']
-        return res
-
-    @staticmethod
-    def _str_to_bool(var):
-        if isinstance(var, bool):
-            return var
-        return var.lower() in ("true", "yes", "1", 1)
-
-    def list(self, attrs=None, stats=False):
-        if attrs:
-            attrs = attrs.split(',')
-
-        if self._str_to_bool(stats):
-            pools = CephService.get_pool_list_with_stats()
-        else:
-            pools = CephService.get_pool_list()
-
-        return [self._serialize_pool(pool, attrs) for pool in pools]
-
-    def get(self, pool_name, attrs=None, stats=False):
-        pools = self.list(attrs, stats)
-        return [pool for pool in pools if pool['pool_name'] == pool_name][0]
diff --git a/src/pybind/mgr/dashboard_v2/controllers/rbd.py b/src/pybind/mgr/dashboard_v2/controllers/rbd.py
deleted file mode 100644 (file)
index b73697b..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import math
-import cherrypy
-import rbd
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, RESTController, ViewCache
-
-
-@ApiController('rbd')
-@AuthRequired()
-class Rbd(RESTController):
-
-    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",
-    }
-
-    def __init__(self):
-        self.rbd = None
-
-    @staticmethod
-    def _format_bitmask(features):
-        """
-        Formats the bitmask:
-
-        >>> Rbd._format_bitmask(45)
-        'deep-flatten, exclusive-lock, layering, object-map'
-        """
-        names = [val for key, val in Rbd.RBD_FEATURES_NAME_MAPPING.items()
-                 if key & features == key]
-        return ', '.join(sorted(names))
-
-    @staticmethod
-    def _format_features(features):
-        """
-        Converts the features list to bitmask:
-
-        >>> Rbd._format_features(['deep-flatten', 'exclusive-lock', 'layering', 'object-map'])
-        45
-
-        >>> Rbd._format_features(None) is None
-        True
-
-        >>> Rbd._format_features('not a list') is None
-        True
-        """
-        if not features or not isinstance(features, list):
-            return None
-
-        res = 0
-        for key, value in Rbd.RBD_FEATURES_NAME_MAPPING.items():
-            if value in features:
-                res = key | res
-        return res
-
-    @ViewCache()
-    def _rbd_list(self, pool_name):
-        ioctx = mgr.rados.open_ioctx(pool_name)
-        self.rbd = rbd.RBD()
-        names = self.rbd.list(ioctx)
-        result = []
-        for name in names:
-            i = rbd.Image(ioctx, name)
-            stat = i.stat()
-            stat['name'] = name
-            features = i.features()
-            stat['features'] = features
-            stat['features_name'] = self._format_bitmask(features)
-
-            try:
-                parent_info = i.parent_info()
-                parent = "{}@{}".format(parent_info[0], parent_info[1])
-                if parent_info[0] != pool_name:
-                    parent = "{}/{}".format(parent_info[0], parent)
-                stat['parent'] = parent
-            except rbd.ImageNotFound:
-                pass
-            result.append(stat)
-        return result
-
-    def get(self, pool_name):
-        # pylint: disable=unbalanced-tuple-unpacking
-        status, value = self._rbd_list(pool_name)
-        if status == ViewCache.VALUE_EXCEPTION:
-            raise value
-        return {'status': status, 'value': value}
-
-    def create(self, data):
-        if not self.rbd:
-            self.rbd = rbd.RBD()
-
-        # Get input values
-        name = data.get('name')
-        pool_name = data.get('pool_name')
-        size = data.get('size')
-        obj_size = data.get('obj_size')
-        features = data.get('features')
-        stripe_unit = data.get('stripe_unit')
-        stripe_count = data.get('stripe_count')
-        data_pool = data.get('data_pool')
-
-        # Set order
-        order = None
-        if obj_size and obj_size > 0:
-            order = int(round(math.log(float(obj_size), 2)))
-
-        # Set features
-        feature_bitmask = self._format_features(features)
-
-        ioctx = mgr.rados.open_ioctx(pool_name)
-
-        try:
-            self.rbd.create(ioctx, name, size, order=order, old_format=False,
-                            features=feature_bitmask, stripe_unit=stripe_unit,
-                            stripe_count=stripe_count, data_pool=data_pool)
-        except rbd.OSError as e:
-            cherrypy.response.status = 400
-            return {'success': False, 'detail': str(e), 'errno': e.errno}
-        return {'success': True}
diff --git a/src/pybind/mgr/dashboard_v2/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard_v2/controllers/rbd_mirroring.py
deleted file mode 100644 (file)
index 62164ff..0000000
+++ /dev/null
@@ -1,305 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-import re
-
-from functools import partial
-
-import cherrypy
-import rbd
-
-from .. import logger, mgr
-from ..services.ceph_service import CephService
-from ..tools import ApiController, AuthRequired, BaseController, ViewCache
-
-
-@ViewCache()
-def get_daemons_and_pools():  # pylint: disable=R0915
-    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']
-
-                try:
-                    status = json.loads(status['json'])
-                except (ValueError, KeyError) as _:
-                    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_color': 'info',
-            'health': 'Unknown'
-        }
-        for _, pool_data in daemon['status'].items():  # TODO: simplify
-            if (health['health'] != 'error' and
-                    [k for k, v in pool_data.get('callouts', {}).items()
-                     if v['level'] == 'error']):
-                health = {
-                    'health_color': 'error',
-                    'health': 'Error'
-                }
-            elif (health['health'] != 'error' and
-                  [k for k, v in pool_data.get('callouts', {}).items()
-                   if v['level'] == 'warning']):
-                health = {
-                    'health_color': 'warning',
-                    'health': 'Warning'
-                }
-            elif health['health_color'] == 'info':
-                health = {
-                    'health_color': 'success',
-                    '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')]
-        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)
-            except:  # noqa pylint: disable=W0702
-                logger.exception("Failed to query mirror mode %s", pool_name)
-
-            stats = {}
-            if mirror_mode == rbd.RBD_MIRROR_MODE_DISABLED:
-                continue
-            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"
-                stats['health_color'] = "warning"
-                stats['health'] = "Warning"
-
-            pool_stats[pool_name] = dict(stats, **{
-                'mirror_mode': mirror_mode
-            })
-
-        for daemon in daemons:
-            for _, pool_data in daemon['status'].items():
-                stats = pool_stats.get(pool_data['name'], None)
-                if stats is None:
-                    continue
-
-                if pool_data.get('leader', False):
-                    # leader instance stores image counts
-                    stats['leader_id'] = daemon['metadata']['instance_id']
-                    stats['image_local_count'] = pool_data.get('image_local_count', 0)
-                    stats['image_remote_count'] = pool_data.get('image_remote_count', 0)
-
-                if (stats.get('health_color', '') != 'error' and
-                        pool_data.get('image_error_count', 0) > 0):
-                    stats['health_color'] = 'error'
-                    stats['health'] = 'Error'
-                elif (stats.get('health_color', '') != 'error' and
-                      pool_data.get('image_warning_count', 0) > 0):
-                    stats['health_color'] = 'warning'
-                    stats['health'] = 'Warning'
-                elif stats.get('health', None) is None:
-                    stats['health_color'] = 'success'
-                    stats['health'] = 'OK'
-
-        for _, stats in pool_stats.items():
-            if stats.get('health', None) is None:
-                # daemon doesn't know about pool
-                stats['health_color'] = 'error'
-                stats['health'] = 'Error'
-            elif stats.get('leader_id', None) is None:
-                # no daemons are managing the pool as leader instance
-                stats['health_color'] = 'warning'
-                stats['health'] = 'Warning'
-        return pool_stats
-
-    daemons = get_daemons()
-    return {
-        'daemons': daemons,
-        'pools': get_pools(daemons)
-    }
-
-
-@ApiController('rbdmirror')
-@AuthRequired()
-class RbdMirror(BaseController):
-
-    def __init__(self):
-        self.pool_data = {}
-
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
-    def default(self):
-        status, content_data = self._get_content_data()
-        return {'status': status, 'content_data': content_data}
-
-    @ViewCache()
-    def _get_pool_datum(self, 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'
-            },
-            rbd.MIRROR_IMAGE_STATUS_STATE_STARTING_REPLAY: {
-                'health': 'ok',
-                'state_color': 'success',
-                'state': 'Starting'
-            },
-            rbd.MIRROR_IMAGE_STATUS_STATE_REPLAYING: {
-                'health': 'ok',
-                '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': 'Primary'
-            }
-        }
-
-        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_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)
-
-        return data
-
-    @ViewCache()
-    def _get_content_data(self):  # pylint: disable=R0914
-
-        def get_pool_datum(pool_name):
-            pool_datum = self.pool_data.get(pool_name, None)
-            if pool_datum is None:
-                pool_datum = partial(self._get_pool_datum, pool_name)
-                self.pool_data[pool_name] = pool_datum
-
-            _, value = pool_datum()
-            return value
-
-        pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
-        _, data = get_daemons_and_pools()
-        if isinstance(data, Exception):
-            logger.exception("Failed to get rbd-mirror daemons list")
-            raise type(data)(str(data))
-        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) or {}
-            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']
-                }
-
-                if mirror_image['health'] == 'ok':
-                    image.update({
-                        'state_color': mirror_image['state_color'],
-                        'state': mirror_image['state'],
-                        'description': mirror_image['description']
-                    })
-                    image_ready.append(image)
-                elif mirror_image['health'] == 'syncing':
-                    p = re.compile("bootstrapping, IMAGE_COPY/COPY_OBJECT (.*)%")
-                    image.update({
-                        'progress': (p.findall(mirror_image['description']) or [0])[0]
-                    })
-                    image_syncing.append(image)
-                else:
-                    image.update({
-                        'state_color': mirror_image['state_color'],
-                        'state': mirror_image['state'],
-                        '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
-        }
diff --git a/src/pybind/mgr/dashboard_v2/controllers/rgw.py b/src/pybind/mgr/dashboard_v2/controllers/rgw.py
deleted file mode 100644 (file)
index 4f8e169..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-
-from .. import logger
-from ..services.ceph_service import CephService
-from ..tools import ApiController, RESTController, AuthRequired
-
-
-@ApiController('rgw')
-@AuthRequired()
-class Rgw(RESTController):
-    pass
-
-
-@ApiController('rgw/daemon')
-@AuthRequired()
-class RgwDaemon(RESTController):
-
-    def list(self):
-        daemons = []
-        for hostname, server in CephService.get_service_map('rgw').items():
-            for service in server['services']:
-                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'])
-
-                # extract per-daemon service data and health
-                daemon = {
-                    'id': service['id'],
-                    'version': metadata['ceph_version'],
-                    'server_hostname': hostname
-                }
-
-                daemons.append(daemon)
-
-        return sorted(daemons, key=lambda k: k['id'])
-
-    def get(self, svc_id):
-        daemon = {
-            'rgw_metadata': [],
-            'rgw_id': svc_id,
-            'rgw_status': []
-        }
-        service = CephService.get_service('rgw', svc_id)
-        if not service:
-            return daemon
-
-        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
diff --git a/src/pybind/mgr/dashboard_v2/controllers/summary.py b/src/pybind/mgr/dashboard_v2/controllers/summary.py
deleted file mode 100644 (file)
index 93631bb..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-
-import cherrypy
-
-from .. import logger, mgr
-from ..controllers.rbd_mirroring import get_daemons_and_pools
-from ..tools import AuthRequired, ApiController, BaseController
-from ..services.ceph_service import CephService
-
-
-@ApiController('summary')
-@AuthRequired()
-class Summary(BaseController):
-    def _rbd_pool_data(self):
-        pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
-        return sorted(pool_names)
-
-    def _health_status(self):
-        health_data = mgr.get("health")
-        return json.loads(health_data["json"])['status']
-
-    def _filesystems(self):
-        fsmap = mgr.get("fs_map")
-        return [
-            {
-                "id": f['id'],
-                "name": f['mdsmap']['fs_name']
-            }
-            for f in fsmap['filesystems']
-        ]
-
-    def _rbd_mirroring(self):
-        _, data = get_daemons_and_pools()
-
-        if isinstance(data, Exception):
-            logger.exception("Failed to get rbd-mirror daemons and pools")
-            raise type(data)(str(data))
-        else:
-            daemons = data.get('daemons', [])
-            pools = data.get('pools', {})
-
-        warnings = 0
-        errors = 0
-        for daemon in daemons:
-            if daemon['health_color'] == 'error':
-                errors += 1
-            elif daemon['health_color'] == 'warning':
-                warnings += 1
-        for _, pool in pools.items():
-            if pool['health_color'] == 'error':
-                errors += 1
-            elif pool['health_color'] == 'warning':
-                warnings += 1
-        return {'warnings': warnings, 'errors': errors}
-
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
-    def default(self):
-        return {
-            'rbd_pools': self._rbd_pool_data(),
-            'health_status': self._health_status(),
-            'filesystems': self._filesystems(),
-            'rbd_mirroring': self._rbd_mirroring(),
-            'mgr_id': mgr.get_mgr_id(),
-            'have_mon_connection': mgr.have_mon_connection()
-        }
diff --git a/src/pybind/mgr/dashboard_v2/controllers/tcmu_iscsi.py b/src/pybind/mgr/dashboard_v2/controllers/tcmu_iscsi.py
deleted file mode 100644 (file)
index f4849b7..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .. import mgr
-from ..services.ceph_service import CephService
-from ..tools import ApiController, AuthRequired, RESTController
-
-SERVICE_TYPE = 'tcmu-runner'
-
-
-@ApiController('tcmuiscsi')
-@AuthRequired()
-class TcmuIscsi(RESTController):
-    # pylint: disable=too-many-locals,too-many-nested-blocks
-    def list(self):  # pylint: disable=unused-argument
-        daemons = {}
-        images = {}
-        for service in CephService.get_service_list(SERVICE_TYPE):
-            metadata = service['metadata']
-            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)
-                lock_acquired_time = (mgr.get_counter(
-                    'tcmu-runner', service_id, perf_key)[perf_key] or
-                                      [[0, 0]])[-1][1] / 1000000000
-                if lock_acquired_time > image.get('optimized_since', 0):
-                    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)
-                        image['stats'][s] = mgr.get_rate(
-                            'tcmu-runner', service_id, perf_key)
-                        image['stats_history'][s] = mgr.get_counter(
-                            'tcmu-runner', service_id, perf_key)[perf_key]
-            else:
-                daemon['non_optimized_paths'] += 1
-                image['non_optimized_paths'].append(hostname)
-
-        return {
-            'daemons': sorted(daemons.values(), key=lambda d: d['server_hostname']),
-            'images': sorted(images.values(), key=lambda i: ['id']),
-        }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json b/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json
deleted file mode 100644 (file)
index 6ffd7b7..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-{
-  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
-  "project": {
-    "name": "ceph-dashboard"
-  },
-  "apps": [
-    {
-      "root": "src",
-      "outDir": "dist",
-      "assets": [
-        "assets",
-        "favicon.ico"
-      ],
-      "index": "index.html",
-      "main": "main.ts",
-      "polyfills": "polyfills.ts",
-      "test": "test.ts",
-      "tsconfig": "tsconfig.app.json",
-      "testTsconfig": "tsconfig.spec.json",
-      "prefix": "cd",
-      "styles": [
-        "../node_modules/bootstrap/dist/css/bootstrap.css",
-        "../node_modules/ng2-toastr/bundles/ng2-toastr.min.css",
-        "../node_modules/font-awesome/css/font-awesome.css",
-        "../node_modules/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css",
-        "styles.scss"
-      ],
-      "scripts": [
-        "../node_modules/chart.js/dist/Chart.bundle.js"
-      ],
-      "environmentSource": "environments/environment.ts",
-      "environments": {
-        "dev": "environments/environment.ts",
-        "prod": "environments/environment.prod.ts"
-      }
-    }
-  ],
-  "e2e": {
-    "protractor": {
-      "config": "./protractor.conf.js"
-    }
-  },
-  "lint": [
-    {
-      "project": "src/tsconfig.app.json",
-      "exclude": "**/node_modules/**"
-    },
-    {
-      "project": "src/tsconfig.spec.json",
-      "exclude": "**/node_modules/**"
-    },
-    {
-      "project": "e2e/tsconfig.e2e.json",
-      "exclude": "**/node_modules/**"
-    }
-  ],
-  "test": {
-    "karma": {
-      "config": "./karma.conf.js"
-    }
-  },
-  "defaults": {
-    "styleExt": "scss",
-    "component": {}
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/.editorconfig b/src/pybind/mgr/dashboard_v2/frontend/.editorconfig
deleted file mode 100644 (file)
index 6e87a00..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-# 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_v2/frontend/.gitignore b/src/pybind/mgr/dashboard_v2/frontend/.gitignore
deleted file mode 100644 (file)
index 2e55dc6..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-# See http://help.github.com/ignore-files/ for more about ignoring files.
-
-# compiled output
-/dist
-/tmp
-/out-tsc
-
-# dependencies
-/node_modules
-
-# IDEs and editors
-/.idea
-.project
-.classpath
-.c9/
-*.launch
-.settings/
-*.sublime-workspace
-
-# IDE - VSCode
-.vscode/*
-!.vscode/settings.json
-!.vscode/tasks.json
-!.vscode/launch.json
-!.vscode/extensions.json
-
-# misc
-/.sass-cache
-/connect.lock
-/coverage
-/libpeerconnection.log
-npm-debug.log
-testem.log
-/typings
-
-# e2e
-/e2e/*.js
-/e2e/*.map
-
-# System Files
-.DS_Store
-Thumbs.db
-
-# Package lock files
-yarn.lock
-package-lock.json
-
-# Ceph
-!core
-!*.core
diff --git a/src/pybind/mgr/dashboard_v2/frontend/e2e/app.e2e-spec.ts b/src/pybind/mgr/dashboard_v2/frontend/e2e/app.e2e-spec.ts
deleted file mode 100644 (file)
index 3e98370..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-import { AppPage } from './app.po';
-
-describe('ceph-dashboard App', () => {
-  let page: AppPage;
-
-  beforeEach(() => {
-    page = new AppPage();
-  });
-
-  it('should display welcome message', () => {
-    page.navigateTo();
-    expect(page.getParagraphText()).toEqual('Welcome to oa!');
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/e2e/app.po.ts b/src/pybind/mgr/dashboard_v2/frontend/e2e/app.po.ts
deleted file mode 100644 (file)
index d9761bb..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-import { browser, by, element } from 'protractor';
-
-export class AppPage {
-  navigateTo() {
-    return browser.get('/');
-  }
-
-  getParagraphText() {
-    return element(by.css('oa-root h1')).getText();
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/e2e/tsconfig.e2e.json b/src/pybind/mgr/dashboard_v2/frontend/e2e/tsconfig.e2e.json
deleted file mode 100644 (file)
index 1d9e5ed..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "extends": "../tsconfig.json",
-  "compilerOptions": {
-    "outDir": "../out-tsc/e2e",
-    "baseUrl": "./",
-    "module": "commonjs",
-    "target": "es5",
-    "types": [
-      "jasmine",
-      "jasminewd2",
-      "node"
-    ]
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/karma.conf.js b/src/pybind/mgr/dashboard_v2/frontend/karma.conf.js
deleted file mode 100644 (file)
index 95969e0..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-// Karma configuration file, see link for more information
-// https://karma-runner.github.io/1.0/config/configuration-file.html
-
-module.exports = function (config) {
-  config.set({
-    basePath: '',
-    frameworks: ['jasmine', '@angular/cli'],
-    plugins: [
-      require('karma-jasmine'),
-      require('karma-chrome-launcher'),
-      require('karma-jasmine-html-reporter'),
-      require('karma-coverage-istanbul-reporter'),
-      require('@angular/cli/plugins/karma'),
-      require('karma-phantomjs-launcher'),
-      require('karma-junit-reporter')
-    ],
-    client:{
-      clearContext: false // leave Jasmine Spec Runner output visible in browser
-    },
-    coverageIstanbulReporter: {
-      reports: [ 'html', 'lcovonly', 'cobertura' ],
-      fixWebpackSourcePaths: true
-    },
-    angularCli: {
-      environment: 'dev'
-    },
-    reporters: ['progress', 'kjhtml', 'junit'],
-    junitReporter: {
-      'outputFile': 'junit.frontend.xml',
-      'suite': 'dashboard_v2',
-      'useBrowserName': false
-    },
-    port: 9876,
-    colors: true,
-    logLevel: config.LOG_INFO,
-    autoWatch: true,
-    browsers: ['Chrome'],
-    singleRun: false
-  });
-};
diff --git a/src/pybind/mgr/dashboard_v2/frontend/package.json b/src/pybind/mgr/dashboard_v2/frontend/package.json
deleted file mode 100644 (file)
index e173870..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-{
-  "name": "ceph-dashboard",
-  "version": "0.0.0",
-  "license": "MIT",
-  "scripts": {
-    "ng": "ng",
-    "start": "ng serve",
-    "build": "ng build",
-    "test": "ng test",
-    "lint": "ng lint",
-    "e2e": "ng e2e"
-  },
-  "private": true,
-  "dependencies": {
-    "@angular/animations": "^5.0.0",
-    "@angular/common": "^5.0.0",
-    "@angular/compiler": "^5.0.0",
-    "@angular/core": "^5.0.0",
-    "@angular/forms": "^5.0.0",
-    "@angular/http": "^5.0.0",
-    "@angular/platform-browser": "^5.0.0",
-    "@angular/platform-browser-dynamic": "^5.0.0",
-    "@angular/router": "^5.0.0",
-    "@swimlane/ngx-datatable": "^11.1.7",
-    "@types/lodash": "^4.14.95",
-    "awesome-bootstrap-checkbox": "0.3.7",
-    "bootstrap": "^3.3.7",
-    "chart.js": "^2.7.1",
-    "core-js": "^2.4.1",
-    "font-awesome": "4.7.0",
-    "lodash": "^4.17.4",
-    "moment": "2.20.1",
-    "ng2-charts": "^1.6.0",
-    "ng2-toastr": "4.1.2",
-    "ngx-bootstrap": "^2.0.1",
-    "rxjs": "^5.5.2",
-    "zone.js": "^0.8.14"
-  },
-  "devDependencies": {
-    "@angular/cli": "^1.6.5",
-    "@angular/compiler-cli": "^5.0.0",
-    "@angular/language-service": "^5.0.0",
-    "@types/jasmine": "~2.5.53",
-    "@types/jasminewd2": "~2.0.2",
-    "@types/node": "~6.0.60",
-    "codelyzer": "^4.0.1",
-    "copy-webpack-plugin": "4.3.0",
-    "jasmine-core": "~2.6.2",
-    "jasmine-spec-reporter": "~4.1.0",
-    "karma": "~1.7.0",
-    "karma-chrome-launcher": "~2.1.1",
-    "karma-cli": "~1.0.1",
-    "karma-coverage-istanbul-reporter": "^1.2.1",
-    "karma-jasmine": "~1.1.0",
-    "karma-jasmine-html-reporter": "^0.2.2",
-    "karma-junit-reporter": "^1.2.0",
-    "karma-phantomjs-launcher": "^1.0.4",
-    "node": "^8.9.4",
-    "protractor": "~5.1.2",
-    "ts-node": "~3.2.0",
-    "tslint": "~5.9.1",
-    "tslint-eslint-rules": "^4.1.1",
-    "typescript": "~2.4.2"
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/protractor.conf.js b/src/pybind/mgr/dashboard_v2/frontend/protractor.conf.js
deleted file mode 100644 (file)
index 7ee3b5e..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-// Protractor configuration file, see link for more information
-// https://github.com/angular/protractor/blob/master/lib/config.ts
-
-const { SpecReporter } = require('jasmine-spec-reporter');
-
-exports.config = {
-  allScriptsTimeout: 11000,
-  specs: [
-    './e2e/**/*.e2e-spec.ts'
-  ],
-  capabilities: {
-    'browserName': 'chrome'
-  },
-  directConnect: true,
-  baseUrl: 'http://localhost:4200/',
-  framework: 'jasmine',
-  jasmineNodeOpts: {
-    showColors: true,
-    defaultTimeoutInterval: 30000,
-    print: function() {}
-  },
-  onPrepare() {
-    require('ts-node').register({
-      project: 'e2e/tsconfig.e2e.json'
-    });
-    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
-  }
-};
diff --git a/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample b/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample
deleted file mode 100644 (file)
index e654419..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "/api/": {
-    "target": "http://localhost:8080",
-    "secure": false,
-    "logLevel": "debug"
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts
deleted file mode 100644 (file)
index 8883796..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-
-import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
-import { MirroringComponent } from './ceph/block/mirroring/mirroring.component';
-import { PoolDetailComponent } from './ceph/block/pool-detail/pool-detail.component';
-import { CephfsComponent } from './ceph/cephfs/cephfs/cephfs.component';
-import { ClientsComponent } from './ceph/cephfs/clients/clients.component';
-import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
-import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
-import { MonitorComponent } from './ceph/cluster/monitor/monitor.component';
-import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component';
-import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
-import {
-  PerformanceCounterComponent
-} from './ceph/performance-counter/performance-counter/performance-counter.component';
-import { RgwDaemonListComponent } from './ceph/rgw/rgw-daemon-list/rgw-daemon-list.component';
-import { LoginComponent } from './core/auth/login/login.component';
-import { NotFoundComponent } from './core/not-found/not-found.component';
-import { AuthGuardService } from './shared/services/auth-guard.service';
-
-const routes: Routes = [
-  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
-  { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] },
-  { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
-  { path: 'login', component: LoginComponent },
-  { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
-  {
-    path: 'rgw',
-    component: RgwDaemonListComponent,
-    canActivate: [AuthGuardService]
-  },
-  { path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] },
-  { path: 'block/pool/:name', component: PoolDetailComponent, canActivate: [AuthGuardService] },
-  {
-    path: 'perf_counters/:type/:id',
-    component: PerformanceCounterComponent,
-    canActivate: [AuthGuardService]
-  },
-  { path: 'monitor', component: MonitorComponent, canActivate: [AuthGuardService] },
-  { path: 'cephfs/:id/clients', component: ClientsComponent, canActivate: [AuthGuardService] },
-  { path: 'cephfs/:id', component: CephfsComponent, canActivate: [AuthGuardService] },
-  { path: 'configuration', component: ConfigurationComponent, canActivate: [AuthGuardService] },
-  { path: 'mirroring', component: MirroringComponent, canActivate: [AuthGuardService] },
-  { path: '404', component: NotFoundComponent },
-  { path: 'osd', component: OsdListComponent, canActivate: [AuthGuardService] },
-  { path: '**', redirectTo: '/404'}
-];
-
-@NgModule({
-  imports: [RouterModule.forRoot(routes, { useHash: true })],
-  exports: [RouterModule]
-})
-export class AppRoutingModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html
deleted file mode 100644 (file)
index 638edaa..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<cd-navigation *ngIf="!isLoginActive()"></cd-navigation>
-<div class="container-fluid"
-     [ngClass]="{'full-height':isLoginActive()}">
-  <router-outlet></router-outlet>
-</div>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts
deleted file mode 100644 (file)
index 3cca10d..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-import { async, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { ToastModule } from 'ng2-toastr';
-
-import { AppComponent } from './app.component';
-import { BlockModule } from './ceph/block/block.module';
-import { ClusterModule } from './ceph/cluster/cluster.module';
-import { CoreModule } from './core/core.module';
-import { SharedModule } from './shared/shared.module';
-
-describe('AppComponent', () => {
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        imports: [
-          RouterTestingModule,
-          CoreModule,
-          SharedModule,
-          ToastModule.forRoot(),
-          ClusterModule,
-          BlockModule
-        ],
-        declarations: [AppComponent]
-      }).compileComponents();
-    })
-  );
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts
deleted file mode 100644 (file)
index c9e0e7e..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Component, ViewContainerRef } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { ToastsManager } from 'ng2-toastr';
-
-import { AuthStorageService } from './shared/services/auth-storage.service';
-
-@Component({
-  selector: 'cd-root',
-  templateUrl: './app.component.html',
-  styleUrls: ['./app.component.scss']
-})
-export class AppComponent {
-  title = 'cd';
-
-  constructor(private authStorageService: AuthStorageService,
-              private router: Router,
-              public toastr: ToastsManager,
-              private vcr: ViewContainerRef) {
-    this.toastr.setRootViewContainerRef(vcr);
-  }
-
-  isLoginActive() {
-    return this.router.url === '/login' || !this.authStorageService.isLoggedIn();
-  }
-
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts
deleted file mode 100644 (file)
index 525e947..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
-import { NgModule } from '@angular/core';
-import { BrowserModule } from '@angular/platform-browser';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-
-import { ToastModule, ToastOptions } from 'ng2-toastr/ng2-toastr';
-
-import { AccordionModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap';
-import { AppRoutingModule } from './app-routing.module';
-import { AppComponent } from './app.component';
-import { CephModule } from './ceph/ceph.module';
-import { CoreModule } from './core/core.module';
-import { AuthInterceptorService } from './shared/services/auth-interceptor.service';
-import { SharedModule } from './shared/shared.module';
-
-export class CustomOption extends ToastOptions {
-  animate = 'flyRight';
-  newestOnTop = true;
-  showCloseButton = true;
-  enableHTML = true;
-}
-
-@NgModule({
-  declarations: [
-    AppComponent
-  ],
-  imports: [
-    HttpClientModule,
-    BrowserModule,
-    BrowserAnimationsModule,
-    ToastModule.forRoot(),
-    AppRoutingModule,
-    HttpClientModule,
-    CoreModule,
-    SharedModule,
-    CephModule,
-    AccordionModule.forRoot(),
-    BsDropdownModule.forRoot(),
-    TabsModule.forRoot(),
-    HttpClientModule,
-    BrowserAnimationsModule
-  ],
-  exports: [SharedModule],
-  providers: [
-    {
-      provide: HTTP_INTERCEPTORS,
-      useClass: AuthInterceptorService,
-      multi: true
-    },
-    {
-      provide: ToastOptions,
-      useClass: CustomOption
-    },
-  ],
-  bootstrap: [AppComponent]
-})
-export class AppModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/block.module.ts
deleted file mode 100644 (file)
index 6e094fa..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-
-import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { ComponentsModule } from '../../shared/components/components.module';
-import { PipesModule } from '../../shared/pipes/pipes.module';
-import { ServicesModule } from '../../shared/services/services.module';
-import { SharedModule } from '../../shared/shared.module';
-import { IscsiComponent } from './iscsi/iscsi.component';
-import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
-import { MirroringComponent } from './mirroring/mirroring.component';
-import { PoolDetailComponent } from './pool-detail/pool-detail.component';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    FormsModule,
-    TabsModule.forRoot(),
-    ProgressbarModule.forRoot(),
-    SharedModule,
-    ComponentsModule,
-    PipesModule,
-    ServicesModule
-  ],
-  declarations: [
-    PoolDetailComponent,
-    IscsiComponent,
-    MirroringComponent,
-    MirrorHealthColorPipe
-  ]
-})
-export class BlockModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.html
deleted file mode 100644 (file)
index 68f9326..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li i18n
-        class="breadcrumb-item">Block</li>
-    <li i18n
-        class="breadcrumb-item active"
-        aria-current="page">iSCSI</li>
-  </ol>
-</nav>
-
-<legend i18n>Daemons</legend>
-<cd-table [data]="daemons"
-          (fetchData)="refresh()"
-          [columns]="daemonsColumns">
-</cd-table>
-
-<legend i18n>Images</legend>
-<cd-table [data]="images"
-          [columns]="imagesColumns">
-</cd-table>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts
deleted file mode 100644 (file)
index 78c19b0..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AppModule } from '../../../app.module';
-import { TcmuIscsiService } from '../../../shared/services/tcmu-iscsi.service';
-import { IscsiComponent } from './iscsi.component';
-
-describe('IscsiComponent', () => {
-  let component: IscsiComponent;
-  let fixture: ComponentFixture<IscsiComponent>;
-
-  const fakeService = {
-    tcmuiscsi: () => {
-      return new Promise(function(resolve, reject) {
-        return;
-      });
-    },
-  };
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        imports: [AppModule],
-        providers: [{ provide: TcmuIscsiService, useValue: fakeService }]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(IscsiComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.ts
deleted file mode 100644 (file)
index 9d700f4..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-import { Component } from '@angular/core';
-
-import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
-import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
-import { ListPipe } from '../../../shared/pipes/list.pipe';
-import { RelativeDatePipe } from '../../../shared/pipes/relative-date.pipe';
-import { TcmuIscsiService } from '../../../shared/services/tcmu-iscsi.service';
-
-@Component({
-  selector: 'cd-iscsi',
-  templateUrl: './iscsi.component.html',
-  styleUrls: ['./iscsi.component.scss']
-})
-export class IscsiComponent {
-
-  daemons = [];
-  daemonsColumns: any;
-  images = [];
-  imagesColumns: any;
-
-  constructor(private tcmuIscsiService: TcmuIscsiService,
-              cephShortVersionPipe: CephShortVersionPipe,
-              dimlessBinaryPipe: DimlessBinaryPipe,
-              dimlessPipe: DimlessPipe,
-              relativeDatePipe: RelativeDatePipe,
-              listPipe: ListPipe) {
-    this.daemonsColumns = [
-      {
-        name: 'Hostname',
-        prop: 'server_hostname'
-      },
-      {
-        name: '# Active/Optimized',
-        prop: 'optimized_paths',
-      },
-      {
-        name: '# Active/Non-Optimized',
-        prop: 'non_optimized_paths'
-      },
-      {
-        name: 'Version',
-        prop: 'version',
-        pipe: cephShortVersionPipe
-      }
-    ];
-    this.imagesColumns = [
-      {
-        name: 'Pool',
-        prop: 'pool_name'
-      },
-      {
-        name: 'Image',
-        prop: 'name'
-      },
-      {
-        name: 'Active/Optimized',
-        prop: 'optimized_paths',
-        pipe: listPipe
-      },
-      {
-        name: 'Active/Non-Optimized',
-        prop: 'non_optimized_paths',
-        pipe: listPipe
-      },
-      {
-        name: 'Read Bytes',
-        prop: 'stats.rd_bytes',
-        pipe: dimlessBinaryPipe
-      },
-      {
-        name: 'Write Bytes',
-        prop: 'stats.wr_bytes',
-        pipe: dimlessBinaryPipe
-      },
-      {
-        name: 'Read Ops',
-        prop: 'stats.rd',
-        pipe: dimlessPipe
-      },
-      {
-        name: 'Write Ops',
-        prop: 'stats.wr',
-        pipe: dimlessPipe
-      },
-      {
-        name: 'A/O Since',
-        prop: 'optimized_since',
-        pipe: relativeDatePipe
-      },
-    ];
-
-  }
-
-  refresh() {
-    this.tcmuIscsiService.tcmuiscsi().then((resp) => {
-      this.daemons = resp.daemons;
-      this.images = resp.images;
-    });
-  }
-
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts
deleted file mode 100644 (file)
index f22bcf2..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
-
-describe('MirrorHealthColorPipe', () => {
-  it('create an instance', () => {
-    const pipe = new MirrorHealthColorPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.ts
deleted file mode 100644 (file)
index 43d880f..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
-  name: 'mirrorHealthColor'
-})
-export class MirrorHealthColorPipe implements PipeTransform {
-  transform(value: any, args?: any): any {
-    if (value === 'warning') {
-      return 'label label-warning';
-    } else if (value === 'error') {
-      return 'label label-danger';
-    } else if (value === 'success') {
-      return 'label label-success';
-    }
-    return 'label label-info';
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.html
deleted file mode 100644 (file)
index a76047d..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li class="breadcrumb-item" i18n>Block</li>
-    <li class="breadcrumb-item active"
-        aria-current="page" i18n>Mirroring</li>
-  </ol>
-</nav>
-
-<cd-view-cache [status]="status"></cd-view-cache>
-
-<div class="row">
-  <div class="col-sm-6">
-    <fieldset>
-      <legend i18n>Daemons</legend>
-
-      <cd-table [data]="daemons.data"
-                columnMode="flex"
-                [columns]="daemons.columns"
-                [autoReload]="30000"
-                (fetchData)="refresh()">
-      </cd-table>
-    </fieldset>
-  </div>
-
-  <div class="col-sm-6">
-    <fieldset>
-      <legend i18n>Pools</legend>
-
-      <cd-table [data]="pools.data"
-                columnMode="flex"
-                [autoReload]="0"
-                (fetchData)="refresh()"
-                [columns]="pools.columns">
-      </cd-table>
-    </fieldset>
-  </div>
-</div>
-
-<div class="row">
-  <div class="col-md-12">
-    <fieldset>
-      <legend i18n>Images</legend>
-      <tabset>
-        <tab heading="Issues" i18n-heading>
-          <cd-table [data]="image_error.data"
-                    columnMode="flex"
-                    [autoReload]="0"
-                    (fetchData)="refresh()"
-                    [columns]="image_error.columns">
-          </cd-table>
-        </tab>
-        <tab heading="Syncing" i18n-heading>
-          <cd-table [data]="image_syncing.data"
-                    columnMode="flex"
-                    [autoReload]="0"
-                    (fetchData)="refresh()"
-                    [columns]="image_syncing.columns">
-          </cd-table>
-        </tab>
-        <tab heading="Ready" i18n-heading>
-          <cd-table [data]="image_ready.data"
-                    columnMode="flex"
-                    [autoReload]="0"
-                    (fetchData)="refresh()"
-                    [columns]="image_ready.columns">
-          </cd-table>
-        </tab>
-      </tabset>
-    </fieldset>
-  </div>
-</div>
-
-<ng-template #healthTmpl
-             let-row="row"
-             let-value="value">
-  <span [ngClass]="row.health_color | mirrorHealthColor">{{ value }}</span>
-</ng-template>
-
-<ng-template #stateTmpl
-             let-row="row"
-             let-value="value">
-  <span [ngClass]="row.state_color | mirrorHealthColor">{{ value }}</span>
-</ng-template>
-
-<ng-template #syncTmpl>
-  <span class="label label-info">Syncing</span>
-</ng-template>
-
-<ng-template #progressTmpl
-             let-value="value">
-  <progressbar type="info"
-               [value]="value">
-  </progressbar>
-</ng-template>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts
deleted file mode 100644 (file)
index f20d048..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { BsDropdownModule, TabsModule } from 'ngx-bootstrap';
-import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
-import { Observable } from 'rxjs/Observable';
-
-import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
-import { SharedModule } from '../../../shared/shared.module';
-import { MirrorHealthColorPipe } from '../mirror-health-color.pipe';
-import { MirroringComponent } from './mirroring.component';
-
-describe('MirroringComponent', () => {
-  let component: MirroringComponent;
-  let fixture: ComponentFixture<MirroringComponent>;
-
-  const fakeService = {
-    get: (service_type: string, service_id: string) => {
-      return Observable.create(observer => {
-        return () => console.log('disposed');
-      });
-    }
-  };
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        declarations: [MirroringComponent, MirrorHealthColorPipe],
-        imports: [
-          SharedModule,
-          BsDropdownModule.forRoot(),
-          TabsModule.forRoot(),
-          ProgressbarModule.forRoot(),
-          HttpClientTestingModule
-        ],
-        providers: [{ provide: RbdMirroringService, useValue: fakeService }]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(MirroringComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.ts
deleted file mode 100644 (file)
index 63e960e..0000000
+++ /dev/null
@@ -1,137 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
-import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
-
-@Component({
-  selector: 'cd-mirroring',
-  templateUrl: './mirroring.component.html',
-  styleUrls: ['./mirroring.component.scss']
-})
-export class MirroringComponent implements OnInit {
-  @ViewChild('healthTmpl') healthTmpl: TemplateRef<any>;
-  @ViewChild('stateTmpl') stateTmpl: TemplateRef<any>;
-  @ViewChild('syncTmpl') syncTmpl: TemplateRef<any>;
-  @ViewChild('progressTmpl') progressTmpl: TemplateRef<any>;
-
-  contentData: any;
-
-  status: ViewCacheStatus;
-  daemons = {
-    data: [],
-    columns: []
-  };
-  pools = {
-    data: [],
-    columns: {}
-  };
-  image_error = {
-    data: [],
-    columns: {}
-  };
-  image_syncing = {
-    data: [],
-    columns: {}
-  };
-  image_ready = {
-    data: [],
-    columns: {}
-  };
-
-  constructor(
-    private http: HttpClient,
-    private rbdMirroringService: RbdMirroringService,
-    private cephShortVersionPipe: CephShortVersionPipe
-  ) { }
-
-  ngOnInit() {
-    this.daemons.columns = [
-      { prop: 'instance_id', name: 'Instance', flexGrow: 2 },
-      { prop: 'id', name: 'ID', flexGrow: 2 },
-      { prop: 'server_hostname', name: 'Hostname', flexGrow: 2 },
-      {
-        prop: 'server_hostname',
-        name: 'Version',
-        pipe: this.cephShortVersionPipe,
-        flexGrow: 2
-      },
-      {
-        prop: 'health',
-        name: 'Health',
-        cellTemplate: this.healthTmpl,
-        flexGrow: 1
-      }
-    ];
-
-    this.pools.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', flexGrow: 2 },
-      { prop: 'image_remote_count', name: '# Remote', flexGrow: 2 },
-      {
-        prop: 'health',
-        name: 'Health',
-        cellTemplate: this.healthTmpl,
-        flexGrow: 1
-      }
-    ];
-
-    this.image_error.columns = [
-      { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
-      { prop: 'name', name: 'Image', flexGrow: 2 },
-      { prop: 'description', name: 'Issue', flexGrow: 4 },
-      {
-        prop: 'state',
-        name: 'State',
-        cellTemplate: this.stateTmpl,
-        flexGrow: 1
-      }
-    ];
-
-    this.image_syncing.columns = [
-      { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
-      { prop: 'name', name: 'Image', flexGrow: 2 },
-      {
-        prop: 'progress',
-        name: 'Progress',
-        cellTemplate: this.progressTmpl,
-        flexGrow: 2
-      },
-      {
-        prop: 'state',
-        name: 'State',
-        cellTemplate: this.syncTmpl,
-        flexGrow: 1
-      }
-    ];
-
-    this.image_ready.columns = [
-      { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
-      { prop: 'name', name: 'Image', flexGrow: 2 },
-      { prop: 'description', name: 'Description', flexGrow: 4 },
-      {
-        prop: 'state',
-        name: 'State',
-        cellTemplate: this.stateTmpl,
-        flexGrow: 1
-      }
-    ];
-  }
-
-  refresh() {
-    this.rbdMirroringService.get().subscribe((data: any) => {
-      this.daemons.data = data.content_data.daemons;
-      this.pools.data = data.content_data.pools;
-      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.status = data.status;
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html
deleted file mode 100644 (file)
index 1bdd5a2..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li i18n
-        class="breadcrumb-item">Block</li>
-    <li i18n
-        class="breadcrumb-item">Pools</li>
-    <li class="breadcrumb-item active"
-        aria-current="page">{{ name }}</li>
-  </ol>
-</nav>
-
-<cd-view-cache [status]="viewCacheStatus"></cd-view-cache>
-
-<cd-table [data]="images"
-          columnMode="flex"
-          [columns]="columns"
-          (fetchData)="loadImages()">
-</cd-table>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts
deleted file mode 100644 (file)
index aea790c..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { AlertModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap';
-
-import { ComponentsModule } from '../../../shared/components/components.module';
-import { SharedModule } from '../../../shared/shared.module';
-import { PoolDetailComponent } from './pool-detail.component';
-
-describe('PoolDetailComponent', () => {
-  let component: PoolDetailComponent;
-  let fixture: ComponentFixture<PoolDetailComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      imports: [
-        SharedModule,
-        BsDropdownModule.forRoot(),
-        TabsModule.forRoot(),
-        AlertModule.forRoot(),
-        ComponentsModule,
-        RouterTestingModule,
-        HttpClientTestingModule
-      ],
-      declarations: [ PoolDetailComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(PoolDetailComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts
deleted file mode 100644 (file)
index 98ac59c..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
-import { PoolService } from '../../../shared/services/pool.service';
-
-@Component({
-  selector: 'cd-pool-detail',
-  templateUrl: './pool-detail.component.html',
-  styleUrls: ['./pool-detail.component.scss']
-})
-export class PoolDetailComponent implements OnInit, OnDestroy {
-  name: string;
-  images: any;
-  columns: CdTableColumn[];
-  retries: number;
-  routeParamsSubscribe: any;
-  viewCacheStatus: ViewCacheStatus;
-
-  constructor(
-    private route: ActivatedRoute,
-    private poolService: PoolService,
-    dimlessBinaryPipe: DimlessBinaryPipe,
-    dimlessPipe: DimlessPipe
-  ) {
-    this.columns = [
-      {
-        name: 'Name',
-        prop: 'name',
-        flexGrow: 2
-      },
-      {
-        name: 'Size',
-        prop: 'size',
-        flexGrow: 1,
-        cellClass: 'text-right',
-        pipe: dimlessBinaryPipe
-      },
-      {
-        name: 'Objects',
-        prop: 'num_objs',
-        flexGrow: 1,
-        cellClass: 'text-right',
-        pipe: dimlessPipe
-      },
-      {
-        name: 'Object size',
-        prop: 'obj_size',
-        flexGrow: 1,
-        cellClass: 'text-right',
-        pipe: dimlessBinaryPipe
-      },
-      {
-        name: 'Features',
-        prop: 'features_name',
-        flexGrow: 3
-      },
-      {
-        name: 'Parent',
-        prop: 'parent',
-        flexGrow: 2
-      }
-    ];
-  }
-
-  ngOnInit() {
-    this.routeParamsSubscribe = this.route.params.subscribe((params: { name: string }) => {
-      this.name = params.name;
-      this.images = [];
-      this.retries = 0;
-    });
-  }
-
-  ngOnDestroy() {
-    this.routeParamsSubscribe.unsubscribe();
-  }
-
-  loadImages() {
-    this.poolService.rbdPoolImages(this.name).then(
-      resp => {
-        this.viewCacheStatus = resp.status;
-        this.images = resp.value;
-      },
-      () => {
-        this.viewCacheStatus = ViewCacheStatus.ValueException;
-      }
-    );
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts
deleted file mode 100644 (file)
index 0f74b82..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { SharedModule } from '../shared/shared.module';
-import { BlockModule } from './block/block.module';
-import { CephfsModule } from './cephfs/cephfs.module';
-import { ClusterModule } from './cluster/cluster.module';
-import { DashboardModule } from './dashboard/dashboard.module';
-import { PerformanceCounterModule } from './performance-counter/performance-counter.module';
-import { RgwModule } from './rgw/rgw.module';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    ClusterModule,
-    DashboardModule,
-    RgwModule,
-    PerformanceCounterModule,
-    BlockModule,
-    CephfsModule,
-    SharedModule
-  ],
-  declarations: []
-})
-export class CephModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html
deleted file mode 100644 (file)
index b98d708..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<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_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss
deleted file mode 100644 (file)
index 62a023b..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-@import '../../../../styles/chart-tooltip.scss';
-
-.chart-container {
-  height: 500px;
-  width: 100%;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts
deleted file mode 100644 (file)
index 6d55204..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-
-import { CephfsChartComponent } from './cephfs-chart.component';
-
-describe('CephfsChartComponent', () => {
-  let component: CephfsChartComponent;
-  let fixture: ComponentFixture<CephfsChartComponent>;
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        imports: [ChartsModule],
-        declarations: [CephfsChartComponent]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(CephfsChartComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts
deleted file mode 100644 (file)
index cca1ae2..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-import { Component, ElementRef, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
-
-import * as _ from 'lodash';
-import * as moment from 'moment';
-
-import { ChartTooltip } from '../../../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') chartCanvas: ElementRef;
-  @ViewChild('chartTooltip') chartTooltip: ElementRef;
-
-  @Input() mdsCounter: any;
-
-  lhsCounter = 'mds.inodes';
-  rhsCounter = 'mds_server.handle_client_request';
-
-  chart: any;
-
-  constructor() {}
-
-  ngOnInit() {
-    if (_.isUndefined(this.mdsCounter)) {
-      return;
-    }
-
-    const getTitle = title => {
-      return moment(title).format('LTS');
-    };
-
-    const getStyleTop = tooltip => {
-      return tooltip.caretY - tooltip.height - 15 + 'px';
-    };
-
-    const getStyleLeft = tooltip => {
-      return tooltip.caretX + 'px';
-    };
-
-    const chartTooltip = new ChartTooltip(
-      this.chartCanvas,
-      this.chartTooltip,
-      getStyleLeft,
-      getStyleTop
-    );
-    chartTooltip.getTitle = getTitle;
-    chartTooltip.checkOffset = true;
-
-    const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]);
-    const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]);
-
-    this.chart = {
-      datasets: [
-        {
-          label: this.lhsCounter,
-          yAxisID: 'LHS',
-          data: lhsData,
-          tension: 0.1
-        },
-        {
-          label: this.rhsCounter,
-          yAxisID: 'RHS',
-          data: rhsData,
-          tension: 0.1
-        }
-      ],
-      options: {
-        responsive: true,
-        maintainAspectRatio: false,
-        legend: {
-          position: 'top'
-        },
-        scales: {
-          xAxes: [
-            {
-              position: 'top',
-              type: 'time',
-              time: {
-                displayFormats: {
-                  quarter: 'MMM YYYY'
-                }
-              }
-            }
-          ],
-          yAxes: [
-            {
-              id: 'LHS',
-              type: 'linear',
-              position: 'left',
-              min: 0
-            },
-            {
-              id: 'RHS',
-              type: 'linear',
-              position: 'right',
-              min: 0
-            }
-          ]
-        },
-        tooltips: {
-          enabled: false,
-          mode: 'index',
-          intersect: false,
-          position: 'nearest',
-          custom: tooltip => {
-            chartTooltip.customTooltips(tooltip);
-          }
-        }
-      },
-      chartType: 'line'
-    };
-  }
-
-  ngOnChanges() {
-    if (!this.chart) {
-      return;
-    }
-
-    const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]);
-    const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]);
-
-    this.chart.datasets[0].data = lhsData;
-    this.chart.datasets[1].data = rhsData;
-  }
-
-  // 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)
-  convert_timeseries(sourceSeries) {
-    const data = [];
-    _.each(sourceSeries, dp => {
-      data.push({
-        x: dp[0] * 1000,
-        y: dp[1]
-      });
-    });
-
-    return data;
-  }
-
-  delta_timeseries(sourceSeries) {
-    let i;
-    let prev = sourceSeries[0];
-    const result = [];
-    for (i = 1; i < sourceSeries.length; i++) {
-      const cur = sourceSeries[i];
-      const tdelta = cur[0] - prev[0];
-      const vdelta = cur[1] - prev[1];
-      const rate = vdelta / tdelta;
-
-      result.push({
-        x: cur[0] * 1000,
-        y: rate
-      });
-
-      prev = cur;
-    }
-    return result;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts
deleted file mode 100644 (file)
index c47051c..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
-
-import { AppRoutingModule } from '../../app-routing.module';
-import { SharedModule } from '../../shared/shared.module';
-import { CephfsChartComponent } from './cephfs-chart/cephfs-chart.component';
-import { CephfsService } from './cephfs.service';
-import { CephfsComponent } from './cephfs/cephfs.component';
-import { ClientsComponent } from './clients/clients.component';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    SharedModule,
-    AppRoutingModule,
-    ChartsModule,
-    ProgressbarModule.forRoot()
-  ],
-  declarations: [CephfsComponent, ClientsComponent, CephfsChartComponent],
-  providers: [CephfsService]
-})
-export class CephfsModule {}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts
deleted file mode 100644 (file)
index a9e59a0..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { CephfsService } from './cephfs.service';
-
-describe('CephfsService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      imports: [HttpClientModule],
-      providers: [CephfsService]
-    });
-  });
-
-  it(
-    'should be created',
-    inject([CephfsService], (service: CephfsService) => {
-      expect(service).toBeTruthy();
-    })
-  );
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.ts
deleted file mode 100644 (file)
index a5c4994..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class CephfsService {
-  baseURL = 'api/cephfs';
-
-  constructor(private http: HttpClient) {}
-
-  getCephfs(id) {
-    return this.http.get(`${this.baseURL}/data/${id}`);
-  }
-
-  getClients(id) {
-    return this.http.get(`${this.baseURL}/clients/${id}`);
-  }
-
-  getMdsCounters(id) {
-    return this.http.get(`${this.baseURL}/mds_counters/${id}`);
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html
deleted file mode 100644 (file)
index ef62292..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li i18n
-        class="breadcrumb-item">Filesystem</li>
-    <li class="breadcrumb-item active"
-        aria-current="page">{{ name }}</li>
-  </ol>
-</nav>
-
-<div class="row">
-  <div class="col-md-12">
-    <i class="fa fa-desktop"></i>
-    <a i18n
-       [routerLink]="['/cephfs/' + id + '/clients']">
-      <span style="font-weight:bold;">{{ clientCount }}</span>
-      Clients
-    </a>
-  </div>
-</div>
-
-<div class="row">
-  <div class="col-sm-6">
-    <fieldset>
-      <legend i18n>Ranks</legend>
-
-      <cd-table [data]="ranks.data"
-                [columns]="ranks.columns"
-                (fetchData)="refresh()"
-                [toolHeader]="false">
-      </cd-table>
-    </fieldset>
-
-    <cd-table-key-value [data]="standbys">
-    </cd-table-key-value>
-  </div>
-
-  <div class="col-sm-6">
-    <fieldset>
-      <legend i18n>Pools</legend>
-
-      <cd-table [data]="pools.data"
-                [columns]="pools.columns"
-                [toolHeader]="false">
-      </cd-table>
-
-    </fieldset>
-  </div>
-</div>
-
-<div class="row"
-     *ngFor="let mdsCounter of objectValues(mdsCounters); trackBy: trackByFn">
-  <div class="cold-md-12">
-    <cd-cephfs-chart [mdsCounter]="mdsCounter"></cd-cephfs-chart>
-  </div>
-</div>
-
-<!-- templates -->
-<ng-template #poolProgressTmpl
-             let-row="row">
-  <progressbar type="danger"
-               [value]="row.used * 100.0 / row.avail">
-  </progressbar>
-</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_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss
deleted file mode 100644 (file)
index d82829a..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-.progress {
-  margin-bottom: 0px;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts
deleted file mode 100644 (file)
index 3df655d..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-import { BsDropdownModule, ProgressbarModule } from 'ngx-bootstrap';
-import { Observable } from 'rxjs/Observable';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { CephfsChartComponent } from '../cephfs-chart/cephfs-chart.component';
-import { CephfsService } from '../cephfs.service';
-import { CephfsComponent } from './cephfs.component';
-
-describe('CephfsComponent', () => {
-  let component: CephfsComponent;
-  let fixture: ComponentFixture<CephfsComponent>;
-
-  const fakeFilesystemService = {
-    getCephfs: id => {
-      return Observable.create(observer => {
-        return () => console.log('disposed');
-      });
-    },
-    getMdsCounters: id => {
-      return Observable.create(observer => {
-        return () => console.log('disposed');
-      });
-    }
-  };
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        imports: [
-          SharedModule,
-          ChartsModule,
-          RouterTestingModule,
-          BsDropdownModule.forRoot(),
-          ProgressbarModule.forRoot()
-        ],
-        declarations: [CephfsComponent, CephfsChartComponent],
-        providers: [
-          { provide: CephfsService, useValue: fakeFilesystemService }
-        ]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(CephfsComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts
deleted file mode 100644 (file)
index d8fe382..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-import * as _ from 'lodash';
-import { Subscription } from 'rxjs/Subscription';
-
-import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
-import { CephfsService } from '../cephfs.service';
-
-@Component({
-  selector: 'cd-cephfs',
-  templateUrl: './cephfs.component.html',
-  styleUrls: ['./cephfs.component.scss']
-})
-export class CephfsComponent implements OnInit, OnDestroy {
-  @ViewChild('poolProgressTmpl') poolProgressTmpl: TemplateRef<any>;
-  @ViewChild('activityTmpl') activityTmpl: TemplateRef<any>;
-
-  routeParamsSubscribe: Subscription;
-
-  objectValues = Object.values;
-
-  id: number;
-  name: string;
-  ranks: any;
-  pools: any;
-  standbys = [];
-  clientCount: number;
-
-  mdsCounters = {};
-
-  constructor(
-    private route: ActivatedRoute,
-    private cephfsService: CephfsService,
-    private dimlessBinary: DimlessBinaryPipe,
-    private dimless: DimlessPipe
-  ) {}
-
-  ngOnInit() {
-    this.ranks = {
-      columns: [
-        { prop: 'rank' },
-        { prop: 'state' },
-        { prop: 'mds', name: 'Daemon' },
-        { prop: 'activity', cellTemplate: this.activityTmpl },
-        { prop: 'dns', name: 'Dentries', pipe: this.dimless },
-        { prop: 'inos', name: 'Inodes', pipe: this.dimless }
-      ],
-      data: []
-    };
-
-    this.pools = {
-      columns: [
-        { prop: 'pool' },
-        { prop: 'type' },
-        { prop: 'used', pipe: this.dimlessBinary },
-        { prop: 'avail', pipe: this.dimlessBinary },
-        {
-          name: 'Usage',
-          cellTemplate: this.poolProgressTmpl,
-          comparator: (valueA, valueB, rowA, rowB, sortDirection) => {
-            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;
-            }
-          }
-        }
-      ],
-      data: []
-    };
-
-    this.routeParamsSubscribe = this.route.params.subscribe((params: { id: number }) => {
-      this.id = params.id;
-
-      this.ranks.data = [];
-      this.pools.data = [];
-      this.standbys = [];
-      this.mdsCounters = {};
-    });
-  }
-
-  ngOnDestroy() {
-    this.routeParamsSubscribe.unsubscribe();
-  }
-
-  refresh() {
-    this.cephfsService.getCephfs(this.id).subscribe((data: any) => {
-      this.ranks.data = data.cephfs.ranks;
-      this.pools.data = data.cephfs.pools;
-      this.standbys = [
-        {
-          key: 'Standby daemons',
-          value: data.standbys.map(value => value.name).join(', ')
-        }
-      ];
-      this.name = data.cephfs.name;
-      this.clientCount = data.cephfs.client_count;
-    });
-
-    this.cephfsService.getMdsCounters(this.id).subscribe(data => {
-      _.each(this.mdsCounters, (value, key) => {
-        if (data[key] === undefined) {
-          delete this.mdsCounters[key];
-        }
-      });
-
-      _.each(data, (mdsData: any, mdsName) => {
-        mdsData.name = mdsName;
-        this.mdsCounters[mdsName] = mdsData;
-      });
-    });
-  }
-
-  trackByFn(index, item) {
-    return item.name;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.html
deleted file mode 100644 (file)
index 7832a38..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li i18n
-        class="breadcrumb-item">Filesystem</li>
-    <li class="breadcrumb-item">
-      <a [routerLink]="['/cephfs/' + id]">{{ name }}</a>
-    </li>
-    <li i18n
-        class="breadcrumb-item active"
-        aria-current="page">Clients</li>
-  </ol>
-</nav>
-
-<fieldset>
-  <cd-view-cache [status]="viewCacheStatus"></cd-view-cache>
-
-  <cd-table [data]="clients.data"
-            [columns]="clients.columns"
-            (fetchData)="refresh()"
-            [header]="false">
-  </cd-table>
-</fieldset>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts
deleted file mode 100644 (file)
index d3506a9..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-import { Observable } from 'rxjs/Observable';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { CephfsService } from '../cephfs.service';
-import { ClientsComponent } from './clients.component';
-
-describe('ClientsComponent', () => {
-  let component: ClientsComponent;
-  let fixture: ComponentFixture<ClientsComponent>;
-
-  const fakeFilesystemService = {
-    getCephfs: id => {
-      return Observable.create(observer => {
-        return () => console.log('disposed');
-      });
-    },
-    getClients: id => {
-      return Observable.create(observer => {
-        return () => console.log('disposed');
-      });
-    }
-  };
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        imports: [
-          RouterTestingModule,
-          BsDropdownModule.forRoot(),
-          SharedModule
-        ],
-        declarations: [ClientsComponent],
-        providers: [{ provide: CephfsService, useValue: fakeFilesystemService }]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(ClientsComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.ts
deleted file mode 100644 (file)
index fc2cbde..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-import { CephfsService } from '../cephfs.service';
-
-@Component({
-  selector: 'cd-clients',
-  templateUrl: './clients.component.html',
-  styleUrls: ['./clients.component.scss']
-})
-export class ClientsComponent implements OnInit, OnDestroy {
-  routeParamsSubscribe: any;
-
-  id: number;
-  name: string;
-  clients: any;
-  viewCacheStatus: ViewCacheStatus;
-
-  constructor(private route: ActivatedRoute, private cephfsService: CephfsService) {}
-
-  ngOnInit() {
-    this.clients = {
-      columns: [
-        { prop: 'id' },
-        { prop: 'type' },
-        { prop: 'state' },
-        { prop: 'version' },
-        { prop: 'hostname', name: 'Host' },
-        { prop: 'root' }
-      ],
-      data: []
-    };
-
-    this.routeParamsSubscribe = this.route.params.subscribe((params: { id: number }) => {
-      this.id = params.id;
-      this.clients.data = [];
-      this.viewCacheStatus = ViewCacheStatus.ValueNone;
-
-      this.cephfsService.getCephfs(this.id).subscribe((data: any) => {
-        this.name = data.cephfs.name;
-      });
-    });
-  }
-
-  ngOnDestroy() {
-    this.routeParamsSubscribe.unsubscribe();
-  }
-
-  refresh() {
-    this.cephfsService.getClients(this.id).subscribe((data: any) => {
-      this.viewCacheStatus = data.status;
-      this.clients.data = data.data;
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts
deleted file mode 100644 (file)
index d661f51..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { RouterModule } from '@angular/router';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { ComponentsModule } from '../../shared/components/components.module';
-import { SharedModule } from '../../shared/shared.module';
-import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
-import { ConfigurationComponent } from './configuration/configuration.component';
-import { HostsComponent } from './hosts/hosts.component';
-import { MonitorService } from './monitor.service';
-import { MonitorComponent } from './monitor/monitor.component';
-import { OsdDetailsComponent } from './osd/osd-details/osd-details.component';
-import { OsdListComponent } from './osd/osd-list/osd-list.component';
-import {
-  OsdPerformanceHistogramComponent
-} from './osd/osd-performance-histogram/osd-performance-histogram.component';
-import { OsdService } from './osd/osd.service';
-
-@NgModule({
-  entryComponents: [
-    OsdDetailsComponent
-  ],
-  imports: [
-    CommonModule,
-    PerformanceCounterModule,
-    ComponentsModule,
-    TabsModule.forRoot(),
-    SharedModule,
-    RouterModule,
-    FormsModule
-  ],
-  declarations: [
-    HostsComponent,
-    MonitorComponent,
-    ConfigurationComponent,
-    OsdListComponent,
-    OsdDetailsComponent,
-    OsdPerformanceHistogramComponent
-  ],
-  providers: [
-    MonitorService,
-    OsdService
-  ]
-})
-export class ClusterModule {}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.html
deleted file mode 100644 (file)
index efe071a..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li class="breadcrumb-item">Cluster</li>
-    <li class="breadcrumb-item active"
-        aria-current="page">Configuration Documentation</li>
-  </ol>
-</nav>
-
-<div class="dataTables_wrapper">
-  <div class="dataTables_header clearfix form-inline">
-    <!-- filters -->
-    <div class="form-group pull-right filter"
-         *ngFor="let filter of filters">
-      <label>{{ filter.label }}: </label>
-      <select class="form-control input-sm"
-              [(ngModel)]="filter.value"
-              (ngModelChange)="updateFilter()">
-        <option *ngFor="let opt of filter.options">{{ opt }}</option>
-      </select>
-    </div>
-    <!-- end filters -->
-  </div>
-
-  <table class="oadatatable table table-striped table-condensed table-bordered table-hover">
-    <thead class="datatable-header">
-      <tr>
-        <th >Name</th>
-        <th style="width:400px;">Description</th>
-        <th>Type</th>
-        <th>Level</th>
-        <th style="width: 200px">Default</th>
-        <th>Tags</th>
-        <th>Services</th>
-        <th>See_also</th>
-        <th>Max</th>
-        <th>Min</th>
-      </tr>
-    </thead>
-    <tbody>
-      <tr *ngFor="let row of data | filter:filters">
-        <td >{{ row.name }}</td>
-        <td>
-          <p>
-            {{ row.desc }}</p>
-          <p *ngIf="row.long_desc"
-             class=text-muted>{{ row.long_desc }}</p>
-        </td>
-        <td>{{ row.type }}</td>
-        <td>{{ row.level }}</td>
-        <td class="wrap">
-          {{ row.default }} {{ row.daemon_default }}
-        </td>
-        <td>
-          <p *ngFor="let item of row.tags">{{ item }}</p>
-        </td>
-        <td>
-          <p *ngFor="let item of row.services">{{ item }}</p>
-        </td>
-        <td class="wrap">
-          <p *ngFor="let item of row.see_also">{{ item }}</p>
-        </td>
-        <td>{{ row.max }}</td>
-        <td>{{ row.min }}</td>
-      </tr>
-    </tbody>
-  </table>
-</div>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.scss
deleted file mode 100644 (file)
index e968d6d..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-@import '../../../shared/datatable/table/table.component.scss';
-
-td.wrap {
-  word-break: break-all;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts
deleted file mode 100644 (file)
index 0d98766..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
-
-import { Observable } from 'rxjs/Observable';
-
-import { ConfigurationService } from '../../../shared/services/configuration.service';
-import { SharedModule } from '../../../shared/shared.module';
-import { ConfigurationComponent } from './configuration.component';
-
-describe('ConfigurationComponent', () => {
-  let component: ConfigurationComponent;
-  let fixture: ComponentFixture<ConfigurationComponent>;
-
-  const fakeService = {
-    getConfigData: () => {
-      return Observable.create(observer => {
-        return () => console.log('disposed');
-      });
-    }
-  };
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        declarations: [ConfigurationComponent],
-        providers: [{ provide: ConfigurationService, useValue: fakeService }],
-        imports: [SharedModule, FormsModule]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(ConfigurationComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.ts
deleted file mode 100644 (file)
index 7c6ed68..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import { ConfigurationService } from '../../../shared/services/configuration.service';
-
-@Component({
-  selector: 'cd-configuration',
-  templateUrl: './configuration.component.html',
-  styleUrls: ['./configuration.component.scss']
-})
-export class ConfigurationComponent implements OnInit {
-  @ViewChild('arrayTmpl') arrayTmpl: TemplateRef<any>;
-
-  data = [];
-  columns: any;
-
-  filters = [
-    {
-      label: 'Level',
-      prop: 'level',
-      value: 'basic',
-      options: ['basic', 'advanced', 'dev'],
-      applyFilter: (row, value) => {
-        enum Level {
-          basic = 0,
-          advanced = 1,
-          dev = 2
-        }
-
-        const levelVal = Level[value];
-
-        return Level[row.level] <= levelVal;
-      }
-    },
-    {
-      label: 'Service',
-      prop: 'services',
-      value: 'any',
-      options: ['mon', 'mgr', 'osd', 'mds', 'common', 'mds_client', 'rgw', 'any'],
-      applyFilter: (row, value) => {
-        if (value === 'any') {
-          return true;
-        }
-
-        return row.services.includes(value);
-      }
-    }
-  ];
-
-  constructor(private configurationService: ConfigurationService) {}
-
-  ngOnInit() {
-    this.columns = [
-      { flexGrow: 2, canAutoResize: true, prop: 'name' },
-      { flexGrow: 2, prop: 'desc', name: 'Description' },
-      { flexGrow: 2, prop: 'long_desc', name: 'Long description' },
-      { flexGrow: 1, prop: 'type' },
-      { flexGrow: 1, prop: 'level' },
-      { flexGrow: 1, prop: 'default' },
-      { flexGrow: 2, prop: 'daemon_default', name: 'Daemon default' },
-      { flexGrow: 1, prop: 'tags', name: 'Tags', cellTemplate: this.arrayTmpl },
-      { flexGrow: 1, prop: 'services', name: 'Services', cellTemplate: this.arrayTmpl },
-      { flexGrow: 1, prop: 'see_also', name: 'See_also', cellTemplate: this.arrayTmpl },
-      { flexGrow: 1, prop: 'max', name: 'Max' },
-      { flexGrow: 1, prop: 'min', name: 'Min' }
-    ];
-
-    this.fetchData();
-  }
-
-  fetchData() {
-    this.configurationService.getConfigData().subscribe((data: any) => {
-      this.data = data;
-    });
-  }
-
-  updateFilter() {
-    this.data = [...this.data];
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html
deleted file mode 100644 (file)
index f2935c3..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li i18n
-        class="breadcrumb-item">Cluster</li>
-    <li i18n
-        class="breadcrumb-item active"
-        aria-current="page">Hosts</li>
-  </ol>
-</nav>
-<cd-table [data]="hosts"
-          [columns]="columns"
-          columnMode="flex"
-          (fetchData)="getHosts()">
-  <ng-template #servicesTpl let-value="value">
-    <span *ngFor="let service of value; last as isLast">
-      <a [routerLink]="[service.cdLink]">{{ service.type }}.{{ service.id }}</a>{{ !isLast ? ", " : "" }}
-    </span>
-  </ng-template>
-</cd-table>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts
deleted file mode 100644 (file)
index 90eb5e6..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-
-import { ComponentsModule } from '../../../shared/components/components.module';
-import { SharedModule } from '../../../shared/shared.module';
-import { HostsComponent } from './hosts.component';
-
-describe('HostsComponent', () => {
-  let component: HostsComponent;
-  let fixture: ComponentFixture<HostsComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      imports: [
-        SharedModule,
-        HttpClientTestingModule,
-        ComponentsModule,
-        BsDropdownModule.forRoot(),
-        RouterTestingModule
-      ],
-      declarations: [
-        HostsComponent
-      ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(HostsComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
deleted file mode 100644 (file)
index 28a193f..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
-import { HostService } from '../../../shared/services/host.service';
-
-@Component({
-  selector: 'cd-hosts',
-  templateUrl: './hosts.component.html',
-  styleUrls: ['./hosts.component.scss']
-})
-export class HostsComponent implements OnInit {
-
-  columns: Array<CdTableColumn> = [];
-  hosts: Array<object> = [];
-  isLoadingHosts = false;
-
-  @ViewChild('servicesTpl') public servicesTpl: TemplateRef<any>;
-
-  constructor(private hostService: HostService,
-              private cephShortVersionPipe: CephShortVersionPipe) { }
-
-  ngOnInit() {
-    this.columns = [
-      {
-        name: 'Hostname',
-        prop: 'hostname',
-        flexGrow: 1
-      },
-      {
-        name: 'Services',
-        prop: 'services',
-        flexGrow: 3,
-        cellTemplate: this.servicesTpl
-      },
-      {
-        name: 'Version',
-        prop: 'ceph_version',
-        flexGrow: 1,
-        pipe: this.cephShortVersionPipe
-      }
-    ];
-  }
-
-  getHosts() {
-    if (this.isLoadingHosts) {
-      return;
-    }
-    this.isLoadingHosts = true;
-    this.hostService.list().then((resp) => {
-      resp.map((host) => {
-        host.services.map((service) => {
-          service.cdLink = `/perf_counters/${service.type}/${service.id}`;
-          return service;
-        });
-        return host;
-      });
-      this.hosts = resp;
-      this.isLoadingHosts = false;
-    }).catch(() => {
-      this.isLoadingHosts = false;
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts
deleted file mode 100644 (file)
index 1d5f7de..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import {
-  HttpClientTestingModule,
-  HttpTestingController
-} from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { MonitorService } from './monitor.service';
-
-describe('MonitorService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [MonitorService],
-      imports: [HttpClientTestingModule, HttpClientModule]
-    });
-  });
-
-  it('should be created', inject([MonitorService], (service: MonitorService) => {
-    expect(service).toBeTruthy();
-  }));
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts
deleted file mode 100644 (file)
index 32057f3..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class MonitorService {
-  constructor(private http: HttpClient) {}
-
-  getMonitor() {
-    return this.http.get('api/monitor');
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html
deleted file mode 100644 (file)
index d59de84..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li i18n
-        class="breadcrumb-item">Cluster</li>
-    <li i18n
-        class="breadcrumb-item active"
-        aria-current="page">Monitors</li>
-  </ol>
-</nav>
-
-<div class="row">
-  <div class="col-md-4">
-    <fieldset>
-      <legend i18n>Status</legend>
-      <table class="table table-striped"
-             *ngIf="mon_status">
-        <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 }}</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>
-      </table>
-    </fieldset>
-  </div>
-
-  <div class="col-md-8">
-    <fieldset>
-      <legend i18n
-              class="in-quorum">In Quorum</legend>
-      <cd-table [data]="inQuorum.data"
-                [columns]="inQuorum.columns">
-      </cd-table>
-
-      <legend i18n
-              class="in-quorum">Not In Quorum</legend>
-      <cd-table [data]="notInQuorum.data"
-                (fetchData)="refresh()"
-                [columns]="notInQuorum.columns">
-      </cd-table>
-    </fieldset>
-  </div>
-</div>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts
deleted file mode 100644 (file)
index 906581e..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AppModule } from '../../../app.module';
-import { MonitorComponent } from './monitor.component';
-
-describe('MonitorComponent', () => {
-  let component: MonitorComponent;
-  let fixture: ComponentFixture<MonitorComponent>;
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        imports: [AppModule]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(MonitorComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.ts
deleted file mode 100644 (file)
index 0a23129..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-import { Component } from '@angular/core';
-
-import { CellTemplate } from '../../../shared/enum/cell-template.enum';
-import { MonitorService } from '../monitor.service';
-
-@Component({
-  selector: 'cd-monitor',
-  templateUrl: './monitor.component.html',
-  styleUrls: ['./monitor.component.scss']
-})
-export class MonitorComponent {
-
-  mon_status: any;
-  inQuorum: any;
-  notInQuorum: any;
-
-  interval: any;
-  sparklineStyle = {
-    height: '30px',
-    width: '50%'
-  };
-
-  constructor(private monitorService: MonitorService) {
-    this.inQuorum = {
-      columns: [
-        { prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
-        { prop: 'rank', name: 'Rank' },
-        { prop: 'public_addr', name: 'Public Address' },
-        {
-          prop: 'cdOpenSessions',
-          name: 'Open Sessions',
-          cellTransformation: CellTemplate.sparkline
-        }
-      ],
-      data: []
-    };
-
-    this.notInQuorum = {
-      columns: [
-        { prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
-        { prop: 'rank', name: 'Rank' },
-        { prop: 'public_addr', name: 'Public Address' }
-      ],
-      data: []
-    };
-  }
-
-  refresh() {
-    this.monitorService.getMonitor().subscribe((data: any) => {
-      data.in_quorum.map((row) => {
-        row.cdOpenSessions = row.stats.num_sessions.map(i => i[1]);
-        row.cdLink = '/perf_counters/mon/' + row.name;
-        return row;
-      });
-
-      data.out_quorum.map((row) => {
-        row.cdLink = '/perf_counters/mon/' + row.name;
-        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_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html
deleted file mode 100644 (file)
index c511d54..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-<tabset *ngIf="selection.hasSingleSelection">
-  <tab heading="Attributes (OSD map)">
-    <cd-table-key-value *ngIf="osd.loaded"
-                        [data]="osd.details.osd_map">
-    </cd-table-key-value>
-  </tab>
-  <tab heading="Metadata">
-    <cd-table-key-value *ngIf="osd.loaded"
-                        (fetchData)="osd.autoRefresh()"
-                        [data]="osd.details.osd_metadata">
-    </cd-table-key-value>
-  </tab>
-  <tab heading="Performance counter">
-    <cd-table-performance-counter *ngIf="osd.loaded"
-                                  serviceType="osd"
-                                  [serviceId]="osd.id">
-    </cd-table-performance-counter>
-  </tab>
-  <tab heading="Histogram">
-    <h3 *ngIf="osd.loaded && osd.histogram_failed">
-      Histogram not available -> <span class="text-warning">{{ osd.histogram_failed }}</span>
-    </h3>
-    <div class="row" *ngIf="osd.loaded && osd.details.histogram">
-      <div class="col-md-6">
-        <h4>Writes</h4>
-        <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_w_latency_in_bytes_histogram">
-        </cd-osd-performance-histogram>
-      </div>
-      <div class="col-md-6">
-        <h4>Reads</h4>
-        <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_r_latency_out_bytes_histogram">
-        </cd-osd-performance-histogram>
-      </div>
-    </div>
-  </tab>
-</tabset>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts
deleted file mode 100644 (file)
index c246182..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap';
-
-import { DataTableModule } from '../../../../shared/datatable/datatable.module';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
-import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
-import {
-  OsdPerformanceHistogramComponent
-} from '../osd-performance-histogram/osd-performance-histogram.component';
-import { OsdService } from '../osd.service';
-import { OsdDetailsComponent } from './osd-details.component';
-
-describe('OsdDetailsComponent', () => {
-  let component: OsdDetailsComponent;
-  let fixture: ComponentFixture<OsdDetailsComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      imports: [
-        HttpClientModule,
-        TabsModule.forRoot(),
-        PerformanceCounterModule,
-        DataTableModule
-      ],
-      declarations: [
-        OsdDetailsComponent,
-        OsdPerformanceHistogramComponent
-      ],
-      providers: [OsdService]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(OsdDetailsComponent);
-    component = fixture.componentInstance;
-
-    component.selection = new CdTableSelection();
-
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts
deleted file mode 100644 (file)
index 7f2af37..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Component, Input, OnChanges } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
-import { OsdService } from '../osd.service';
-
-@Component({
-  selector: 'cd-osd-details',
-  templateUrl: './osd-details.component.html',
-  styleUrls: ['./osd-details.component.scss']
-})
-export class OsdDetailsComponent implements OnChanges {
-  @Input() selection: CdTableSelection;
-
-  osd: any;
-
-  constructor(private osdService: OsdService) {}
-
-  ngOnChanges() {
-    this.osd = {
-      loaded: false
-    };
-    if (this.selection.hasSelection) {
-      this.osd = this.selection.first();
-      this.osd.autoRefresh = () => {
-        this.refresh();
-      };
-      this.refresh();
-    }
-  }
-
-  refresh() {
-    this.osdService.getDetails(this.osd.tree.id)
-      .subscribe((data: any) => {
-        this.osd.details = data;
-        if (!_.isObject(data.histogram)) {
-          this.osd.histogram_failed = data.histogram;
-          this.osd.details.histogram = undefined;
-        }
-        this.osd.loaded = true;
-      });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
deleted file mode 100644 (file)
index 2683102..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li class="breadcrumb-item">Cluster</li>
-    <li class="breadcrumb-item active">OSDs</li>
-  </ol>
-</nav>
-<cd-table [data]="osds"
-          (fetchData)="getOsdList()"
-          [columns]="columns"
-          selectionType="single"
-          (updateSelection)="updateSelection($event)">
-  <cd-osd-details cdTableDetail
-                  [selection]="selection">
-  </cd-osd-details>
-</cd-table>
-
-<ng-template #statusColor
-             let-value="value">
-  <span *ngFor="let state of value; last as last">
-    <span [class.text-success]="'up' === state || 'in' === state"
-          [class.text-warning]="'down' === state || 'out' === state">
-      {{ state }}</span><span *ngIf="!last">, </span>
-    <!-- Has to be on the same line to prevent a space between state and comma. -->
-  </span>
-</ng-template>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts
deleted file mode 100644 (file)
index 506b536..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { DataTableModule } from '../../../../shared/datatable/datatable.module';
-import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe';
-import { FormatterService } from '../../../../shared/services/formatter.service';
-import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
-import { OsdDetailsComponent } from '../osd-details/osd-details.component';
-import {
-  OsdPerformanceHistogramComponent
-} from '../osd-performance-histogram/osd-performance-histogram.component';
-import { OsdService } from '../osd.service';
-import { OsdListComponent } from './osd-list.component';
-
-describe('OsdListComponent', () => {
-  let component: OsdListComponent;
-  let fixture: ComponentFixture<OsdListComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      imports: [
-        HttpClientModule,
-        PerformanceCounterModule,
-        TabsModule.forRoot(),
-        DataTableModule
-      ],
-      declarations: [
-        OsdListComponent,
-        OsdDetailsComponent,
-        OsdPerformanceHistogramComponent
-      ],
-      providers: [OsdService, DimlessPipe, FormatterService]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(OsdListComponent);
-    component = fixture.componentInstance;
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts
deleted file mode 100644 (file)
index 29f0f22..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import { CellTemplate } from '../../../../shared/enum/cell-template.enum';
-import { CdTableColumn } from '../../../../shared/models/cd-table-column';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
-import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe';
-import { OsdService } from '../osd.service';
-
-@Component({
-  selector: 'cd-osd-list',
-  templateUrl: './osd-list.component.html',
-  styleUrls: ['./osd-list.component.scss']
-})
-
-export class OsdListComponent implements OnInit {
-  @ViewChild('statusColor') statusColor: TemplateRef<any>;
-
-  osds = [];
-  columns: CdTableColumn[];
-  selection = new CdTableSelection();
-
-  constructor(
-    private osdService: OsdService,
-    private dimlessPipe: DimlessPipe
-  ) { }
-
-  ngOnInit() {
-    this.columns = [
-      {prop: 'host.name', name: 'Host'},
-      {prop: 'id', name: 'ID', cellTransformation: CellTemplate.bold},
-      {prop: 'collectedStates', name: 'Status', cellTemplate: this.statusColor},
-      {prop: 'stats.numpg', name: 'PGs'},
-      {prop: 'usedPercent', name: 'Usage'},
-      {
-        prop: 'stats_history.out_bytes',
-        name: 'Read bytes',
-        cellTransformation: CellTemplate.sparkline
-      },
-      {
-        prop: 'stats_history.in_bytes',
-        name: 'Writes bytes',
-        cellTransformation: CellTemplate.sparkline
-      },
-      {prop: 'stats.op_r', name: 'Read ops', cellTransformation: CellTemplate.perSecond},
-      {prop: 'stats.op_w', name: 'Write ops', cellTransformation: CellTemplate.perSecond}
-    ];
-  }
-
-  updateSelection(selection: CdTableSelection) {
-    this.selection = selection;
-  }
-
-  getOsdList() {
-    this.osdService.getList().subscribe((data: any[]) => {
-      this.osds = data;
-      data.map((osd) => {
-        osd.collectedStates = this.collectStates(osd);
-        osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map(i => i[1]);
-        osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map(i => i[1]);
-        osd.usedPercent = this.dimlessPipe.transform(osd.stats.stat_bytes_used) + ' / ' +
-          this.dimlessPipe.transform(osd.stats.stat_bytes);
-        return osd;
-      });
-    });
-  }
-
-  collectStates(osd) {
-    const select = (onState, offState) => osd[onState] ? onState : offState;
-    return [select('up', 'down'), select('in', 'out')];
-  }
-
-  beforeShowDetails(selection: CdTableSelection) {
-    return selection.hasSingleSelection;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html
deleted file mode 100644 (file)
index 080f121..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<table>
-  <tr style="height: 10px;"
-      *ngFor="let row of valuesStyle">
-    <td style="width: 10px; height: 10px;"
-        *ngFor="let col of row"
-        [ngStyle]="col">
-    </td>
-  </tr>
-</table>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts
deleted file mode 100644 (file)
index 7ff7d64..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { OsdPerformanceHistogramComponent } from './osd-performance-histogram.component';
-
-describe('OsdPerformanceHistogramComponent', () => {
-  let component: OsdPerformanceHistogramComponent;
-  let fixture: ComponentFixture<OsdPerformanceHistogramComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ OsdPerformanceHistogramComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(OsdPerformanceHistogramComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts
deleted file mode 100644 (file)
index c3f0645..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-import { Component, Input, OnChanges } from '@angular/core';
-
-import * as _ from 'lodash';
-
-@Component({
-  selector: 'cd-osd-performance-histogram',
-  templateUrl: './osd-performance-histogram.component.html',
-  styleUrls: ['./osd-performance-histogram.component.scss']
-})
-export class OsdPerformanceHistogramComponent implements OnChanges {
-  @Input() histogram: any;
-  valuesStyle: any;
-  last = {};
-
-  constructor() { }
-
-  ngOnChanges() {
-    this.render();
-  }
-
-  hexdigits(v): string {
-    const i = Math.floor(v * 255).toString(16);
-    return i.length === 1 ? '0' + i : i;
-  }
-
-  hexcolor(r, g, b) {
-    return '#' + this.hexdigits(r) + this.hexdigits(g) + this.hexdigits(b);
-  }
-
-  render() {
-    if (!this.histogram) {
-      return;
-    }
-    let sum = 0;
-    let max = 0;
-
-    _.each(this.histogram.values, (row, i) => {
-      _.each(row, (col, j) => {
-        let val;
-        if (this.last && this.last[i] && this.last[i][j]) {
-          val = col - this.last[i][j];
-        } else {
-          val = col;
-        }
-        sum += val;
-        max = Math.max(max, val);
-      });
-    });
-
-    this.valuesStyle = this.histogram.values.map((row, i) => {
-      return row.map((col, j) => {
-        const val = this.last && this.last[i] && this.last[i][j] ? col - this.last[i][j] : col;
-        const g = max ? val / max : 0;
-        const r = 1 - g;
-        return {backgroundColor: this.hexcolor(r, g, 0)};
-      });
-    });
-
-    this.last = this.histogram.values;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts
deleted file mode 100644 (file)
index 115d6a4..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { OsdService } from './osd.service';
-
-describe('OsdService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [OsdService],
-      imports: [
-        HttpClientModule,
-      ],
-    });
-  });
-
-  it('should be created', inject([OsdService], (service: OsdService) => {
-    expect(service).toBeTruthy();
-  }));
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts
deleted file mode 100644 (file)
index cf9adf1..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class OsdService {
-  private path = 'api/osd';
-
-  constructor (private http: HttpClient) {}
-
-  getList () {
-    return this.http.get(`${this.path}`);
-  }
-
-  getDetails(id: number) {
-    return this.http.get(`${this.path}/${id}`);
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts
deleted file mode 100644 (file)
index cf4c025..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-
-import { ChartsModule } from 'ng2-charts';
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { SharedModule } from '../../shared/shared.module';
-import { DashboardService } from './dashboard.service';
-import { DashboardComponent } from './dashboard/dashboard.component';
-import { HealthPieComponent } from './health-pie/health-pie.component';
-import { HealthComponent } from './health/health.component';
-import { LogColorPipe } from './log-color.pipe';
-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 { PgStatusStylePipe } from './pg-status-style.pipe';
-import { PgStatusPipe } from './pg-status.pipe';
-
-@NgModule({
-  imports: [CommonModule, TabsModule.forRoot(), SharedModule, ChartsModule, RouterModule],
-  declarations: [
-    HealthComponent,
-    DashboardComponent,
-    MonSummaryPipe,
-    OsdSummaryPipe,
-    LogColorPipe,
-    MgrSummaryPipe,
-    PgStatusPipe,
-    MdsSummaryPipe,
-    PgStatusStylePipe,
-    HealthPieComponent
-  ],
-  providers: [DashboardService]
-})
-export class DashboardModule {}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts
deleted file mode 100644 (file)
index bf061e9..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { appendFile } from 'fs';
-
-import { DashboardService } from './dashboard.service';
-
-describe('DashboardService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [DashboardService],
-      imports: [HttpClientTestingModule, HttpClientModule]
-    });
-  });
-
-  it(
-    'should be created',
-    inject([DashboardService], (service: DashboardService) => {
-      expect(service).toBeTruthy();
-    })
-  );
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts
deleted file mode 100644 (file)
index cb51cb4..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class DashboardService {
-  constructor(private http: HttpClient) {}
-
-  getHealth() {
-    return this.http.get('api/dashboard/health');
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html
deleted file mode 100644 (file)
index 89a37fd..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<div>
-  <tabset *ngIf="hasGrafana">
-    <tab i18n-heading
-         heading="Health">
-      <cd-health></cd-health>
-    </tab>
-    <tab i18n-heading
-         heading="Statistics">
-    </tab>
-  </tabset>
-  <cd-health *ngIf="!hasGrafana"></cd-health>
-</div>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss
deleted file mode 100644 (file)
index 04eee2d..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-div {
-  padding-top: 20px;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts
deleted file mode 100644 (file)
index 80500c0..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { DashboardComponent } from './dashboard.component';
-
-describe('DashboardComponent', () => {
-  let component: DashboardComponent;
-  let fixture: ComponentFixture<DashboardComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ DashboardComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(DashboardComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  // it('should create', () => {
-  //   expect(component).toBeTruthy();
-  // });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts
deleted file mode 100644 (file)
index fc676c7..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-
-@Component({
-  selector: 'cd-dashboard',
-  templateUrl: './dashboard.component.html',
-  styleUrls: ['./dashboard.component.scss']
-})
-export class DashboardComponent implements OnInit {
-  hasGrafana = false; // TODO: Temporary var, remove when grafana is implemented
-
-  constructor() { }
-
-  ngOnInit() {
-  }
-
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html
deleted file mode 100644 (file)
index 7135f96..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-<div class="chart-container">
-  <canvas baseChart
-          #chartCanvas
-          [datasets]="chart.dataset"
-          [chartType]="chart.chartType"
-          [options]="chart.options"
-          [labels]="chart.labels"
-          [colors]="chart.colors"
-          width="120"
-          height="120"></canvas>
-  <div class="chartjs-tooltip"
-       #chartTooltip>
-    <table></table>
-  </div>
-</div>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss
deleted file mode 100644 (file)
index b3abf86..0000000
+++ /dev/null
@@ -1 +0,0 @@
-@import '../../../../styles/chart-tooltip.scss';
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts
deleted file mode 100644 (file)
index dca539f..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { HealthPieComponent } from './health-pie.component';
-
-describe('HealthPieComponent', () => {
-  let component: HealthPieComponent;
-  let fixture: ComponentFixture<HealthPieComponent>;
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        imports: [ChartsModule, SharedModule],
-        declarations: [HealthPieComponent]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(HealthPieComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts
deleted file mode 100644 (file)
index 196d871..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-import {
-  Component,
-  ElementRef,
-  EventEmitter,
-  Input,
-  OnChanges,
-  OnInit,
-  Output,
-  ViewChild
-} from '@angular/core';
-
-import * as Chart from 'chart.js';
-import * as _ from 'lodash';
-
-import { ChartTooltip } from '../../../shared/models/chart-tooltip';
-import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
-
-@Component({
-  selector: 'cd-health-pie',
-  templateUrl: './health-pie.component.html',
-  styleUrls: ['./health-pie.component.scss']
-})
-export class HealthPieComponent implements OnChanges, OnInit {
-  @ViewChild('chartCanvas') chartCanvasRef: ElementRef;
-  @ViewChild('chartTooltip') chartTooltipRef: ElementRef;
-
-  @Input() data: any;
-  @Input() tooltipFn: any;
-  @Output() prepareFn = new EventEmitter();
-
-  chart: any = {
-    chartType: 'doughnut',
-    dataset: [
-      {
-        label: null,
-        borderWidth: 0
-      }
-    ],
-    options: {
-      responsive: true,
-      legend: { display: false },
-      animation: { duration: 0 },
-
-      tooltips: {
-        enabled: false
-      }
-    },
-    colors: [
-      {
-        borderColor: 'transparent'
-      }
-    ]
-  };
-
-  constructor(private dimlessBinary: DimlessBinaryPipe) {}
-
-  ngOnInit() {
-    // An extension to Chart.js to enable rendering some
-    // text in the middle of a doughnut
-    Chart.pluginService.register({
-      beforeDraw: function(chart) {
-        if (!chart.options.center_text) {
-          return;
-        }
-
-        const width = chart.chart.width,
-          height = chart.chart.height,
-          ctx = chart.chart.ctx;
-
-        ctx.restore();
-        const fontSize = (height / 114).toFixed(2);
-        ctx.font = fontSize + 'em sans-serif';
-        ctx.textBaseline = 'middle';
-
-        const text = chart.options.center_text,
-          textX = Math.round((width - ctx.measureText(text).width) / 2),
-          textY = height / 2;
-
-        ctx.fillText(text, textX, textY);
-        ctx.save();
-      }
-    });
-
-    const getStyleTop = (tooltip, positionY) => {
-      return positionY + tooltip.caretY - tooltip.height - 10 + 'px';
-    };
-
-    const getStyleLeft = (tooltip, positionX) => {
-      return positionX + tooltip.caretX + 'px';
-    };
-
-    const getBody = (body) => {
-      const bodySplit = body[0].split(': ');
-      bodySplit[1] = this.dimlessBinary.transform(bodySplit[1]);
-      return bodySplit.join(': ');
-    };
-
-    const chartTooltip = new ChartTooltip(
-      this.chartCanvasRef,
-      this.chartTooltipRef,
-      getStyleLeft,
-      getStyleTop,
-    );
-    chartTooltip.getBody = getBody;
-
-    const self = this;
-    this.chart.options.tooltips.custom = (tooltip) => {
-      chartTooltip.customTooltips(tooltip);
-    };
-
-    this.prepareFn.emit([this.chart, this.data]);
-  }
-
-  ngOnChanges() {
-    this.prepareFn.emit([this.chart, this.data]);
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html
deleted file mode 100644 (file)
index 348324e..0000000
+++ /dev/null
@@ -1,209 +0,0 @@
-<div *ngIf="contentData">
-  <div class="row">
-    <!-- HEALTH -->
-    <div class="col-md-6">
-      <div class="well">
-        <fieldset>
-          <legend i18n>Health</legend>
-          <ng-container i18n>Overall status:</ng-container>
-          <span [ngStyle]="contentData.health.status | healthColor">{{ contentData.health.status }}</span>
-          <ul>
-            <li *ngFor="let check of contentData.health.checks">
-              <span [ngStyle]="check.severity | healthColor">{{ check.type }}</span>: {{ check.summary.message }}
-            </li>
-          </ul>
-        </fieldset>
-      </div>
-    </div>
-
-    <div class="col-md-6">
-      <!--STATS -->
-      <div class="row">
-        <div class="col-md-6">
-          <div class="well">
-            <div class="media">
-              <div class="media-left">
-                <i class="fa fa-database fa-fw"></i>
-              </div>
-              <div class="media-body">
-                <span class="media-heading"
-                      i18n="ceph monitors">
-                  <a routerLink="/monitor/">Monitors</a>
-                </span>
-                <span class="media-text">{{ contentData.mon_status | monSummary }}</span>
-              </div>
-            </div>
-          </div>
-        </div>
-        <div class="col-md-6">
-          <div class="well">
-            <div class="media">
-              <div class="media-left">
-                <i class="fa fa-hdd-o fa-fw"></i>
-              </div>
-              <div class="media-body">
-                <span class="media-heading"
-                      i18n="ceph OSDs">
-                  <a routerLink="/osd/">OSDs</a>
-                </span>
-                <span class="media-text">{{ contentData.osd_map | osdSummary }}</span>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div class="row">
-        <div class="col-md-6">
-          <div class="well">
-            <div class="media">
-              <div class="media-left">
-                <i class="fa fa-folder fa-fw"></i>
-              </div>
-              <div class="media-body">
-                <span class="media-heading"
-                      i18n>Metadata servers</span>
-                <span class="media-text">{{ contentData.fs_map | mdsSummary }}</span>
-              </div>
-            </div>
-          </div>
-        </div>
-        <div class="col-md-6">
-          <div class="well">
-            <div class="media">
-              <div class="media-left">
-                <i class="fa fa-cog fa-fw"></i>
-              </div>
-              <div class="media-body">
-                <span class="media-heading"
-                      i18n>Manager daemons</span>
-                <span class="media-text">{{ contentData.mgr_map | mgrSummary }}</span>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div class="row">
-    <!-- USAGE -->
-    <div class="col-md-6">
-      <div class="well">
-        <fieldset class="usage">
-          <legend i18n>Usage</legend>
-
-          <table class="ceph-chartbox">
-            <tr>
-              <td>
-                <span style="font-size: 45px;">{{ contentData.df.stats.total_objects | dimless }}</span>
-              </td>
-              <td>
-                <div class="center-block pie">
-                  <cd-health-pie [data]="contentData"
-                                 (prepareFn)="prepareRawUsage($event[0], $event[1])"></cd-health-pie>
-                </div>
-              </td>
-              <td>
-                <div class="center-block pie">
-                  <cd-health-pie [data]="contentData"
-                                 (prepareFn)="preparePoolUsage($event[0], $event[1])"></cd-health-pie>
-                </div>
-              </td>
-            </tr>
-            <tr>
-              <td i18n>Objects</td>
-              <td>
-                <ng-container i18n>Raw capacity</ng-container>
-                <br>
-                <ng-container i18n="disk used">({{ contentData.df.stats.total_used_bytes | dimlessBinary }} used)</ng-container>
-              </td>
-              <td i18n>Usage by pool</td>
-            </tr>
-          </table>
-        </fieldset>
-      </div>
-    </div>
-
-    <div class="col-md-6">
-      <div class="well">
-        <fieldset>
-          <legend i18n>Pools</legend>
-          <table class="table table-condensed">
-            <thead>
-              <tr>
-                <th i18n>Name</th>
-                <th i18n>PG status</th>
-                <th i18n>Usage</th>
-                <th colspan="2"
-                    i18n>Read</th>
-                <th colspan="2"
-                    i18n>Write</th>
-              </tr>
-            </thead>
-            <tbody>
-              <tr *ngFor="let pool of contentData.pools">
-                <td>{{ pool.pool_name }}</td>
-                <td [ngStyle]="pool.pg_status | pgStatusStyle">
-                  {{ pool.pg_status | pgStatus }}
-                </td>
-                <td>
-                  {{ pool.stats.bytes_used.latest | dimlessBinary }} / {{ pool.stats.max_avail.latest | dimlessBinary }}
-                </td>
-                <td>
-                  {{ pool.stats.rd_bytes.rate | dimless }}
-                </td>
-                <td>
-                  {{ pool.stats.rd.rate | dimless }} ops
-                </td>
-                <td>
-                  {{ pool.stats.wr_bytes.rate | dimless }}
-                </td>
-                <td>
-                  {{ pool.stats.wr.rate | dimless }} ops
-                </td>
-              </tr>
-            </tbody>
-          </table>
-        </fieldset>
-      </div>
-    </div>
-  </div>
-
-  <div class="row">
-    <div class="col-md-12">
-      <!-- LOGS -->
-      <div class="well">
-        <fieldset>
-          <legend i18n>Logs</legend>
-
-          <tabset>
-            <tab heading="Cluster log"
-                 class="text-monospace"
-                 i18n-heading>
-              <span *ngFor="let line of contentData.clog">
-                {{ line.stamp }}&nbsp;{{ line.priority }}&nbsp;
-                <span [ngStyle]="line | logColor">
-                  {{ line.message }}
-                  <br>
-                </span>
-              </span>
-            </tab>
-            <tab heading="Audit log"
-                 class="text-monospace"
-                 i18n-heading>
-              <span *ngFor="let line of contentData.audit_log">
-                {{ line.stamp }}&nbsp;{{ line.priority }}&nbsp;
-                <span [ngStyle]="line | logColor">
-                  <span style="font-weight: bold;">
-                    {{ line.message }}
-                  </span>
-                  <br>
-                </span>
-              </span>
-            </tab>
-          </tabset>
-        </fieldset>
-      </div>
-    </div>
-  </div>
-</div>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss
deleted file mode 100644 (file)
index 919b41d..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-table.ceph-chartbox {
-  width: 100%;
-
-  td {
-    text-align: center;
-    font-weight: bold;
-  }
-}
-
-.center-block {
-  width: 120px;
-}
-
-.pie {
-  height: 120px;
-  width: 120px;
-}
-
-.media {
-  display: block;
-  min-height: 60px;
-  width: 100%;
-
-  .media-left {
-    border-top-left-radius: 2px;
-    border-top-right-radius: 0;
-    border-bottom-right-radius: 0;
-    border-bottom-left-radius: 2px;
-    display: block;
-    float: left;
-    height: 60px;
-    width: 60px;
-    text-align: center;
-    font-size: 40px;
-    line-height: 60px;
-    padding-right: 0;
-
-    .fa {
-      font-size: 45px;
-    }
-  }
-
-  .media-body {
-    padding: 5px 10px;
-    margin-left: 60px;
-
-    .media-heading {
-      text-transform: uppercase;
-      display: block;
-      font-size: 14px;
-      white-space: nowrap;
-      overflow: hidden;
-      text-overflow: ellipsis;
-    }
-
-    .media-text {
-      display: block;
-      font-weight: bold;
-      font-size: 18px;
-    }
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts
deleted file mode 100644 (file)
index 983b145..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { DashboardService } from '../dashboard.service';
-import { HealthComponent } from './health.component';
-
-describe('HealthComponent', () => {
-  let component: HealthComponent;
-  let fixture: ComponentFixture<HealthComponent>;
-
-  const fakeService = {
-    getHealth() {
-      return {};
-    }
-  };
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        providers: [{ provide: DashboardService, useValue: fakeService }],
-        imports: [SharedModule],
-        declarations: [HealthComponent]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(HealthComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts
deleted file mode 100644 (file)
index 3cdddc9..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { DashboardService } from '../dashboard.service';
-
-@Component({
-  selector: 'cd-health',
-  templateUrl: './health.component.html',
-  styleUrls: ['./health.component.scss']
-})
-export class HealthComponent implements OnInit, OnDestroy {
-  contentData: any;
-  interval: number;
-
-  constructor(private dashboardService: DashboardService) {}
-
-  ngOnInit() {
-    this.getInfo();
-    this.interval = window.setInterval(() => {
-      this.getInfo();
-    }, 5000);
-  }
-
-  ngOnDestroy() {
-    clearInterval(this.interval);
-  }
-
-  getInfo() {
-    this.dashboardService.getHealth().subscribe((data: any) => {
-      this.contentData = data;
-    });
-  }
-
-  prepareRawUsage(chart, data) {
-    let rawUsageChartColor;
-
-    const rawUsageText =
-      Math.round(100 * (data.df.stats.total_used_bytes / data.df.stats.total_bytes)) + '%';
-
-    if (data.df.stats.total_used_bytes / data.df.stats.total_bytes >= data.osd_map.full_ratio) {
-      rawUsageChartColor = '#ff0000';
-    } else if (
-      data.df.stats.total_used_bytes / data.df.stats.total_bytes >=
-      data.osd_map.backfillfull_ratio
-    ) {
-      rawUsageChartColor = '#ff6600';
-    } else if (
-      data.df.stats.total_used_bytes / data.df.stats.total_bytes >=
-      data.osd_map.nearfull_ratio
-    ) {
-      rawUsageChartColor = '#ffc200';
-    } else {
-      rawUsageChartColor = '#00bb00';
-    }
-
-    chart.dataset[0].data = [data.df.stats.total_used_bytes, data.df.stats.total_avail_bytes];
-    chart.options.center_text = rawUsageText;
-    chart.colors = [{ backgroundColor: [rawUsageChartColor, '#424d52'] }];
-    chart.labels = ['Raw Used', 'Raw Available'];
-  }
-
-  preparePoolUsage(chart, data) {
-    const colors = [
-      '#3366CC',
-      '#109618',
-      '#990099',
-      '#3B3EAC',
-      '#0099C6',
-      '#DD4477',
-      '#66AA00',
-      '#B82E2E',
-      '#316395',
-      '#994499',
-      '#22AA99',
-      '#AAAA11',
-      '#6633CC',
-      '#E67300',
-      '#8B0707',
-      '#329262',
-      '#5574A6',
-      '#FF9900',
-      '#DC3912',
-      '#3B3EAC'
-    ];
-
-    const poolLabels = [];
-    const poolData = [];
-
-    _.each(data.df.pools, (pool, i) => {
-      poolLabels.push(pool['name']);
-      poolData.push(pool['stats']['bytes_used']);
-    });
-
-    chart.dataset[0].data = poolData;
-    chart.colors = [{ backgroundColor: colors }];
-    chart.labels = poolLabels;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts
deleted file mode 100644 (file)
index 43af68d..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { LogColorPipe } from './log-color.pipe';
-
-describe('LogColorPipe', () => {
-  it('create an instance', () => {
-    const pipe = new LogColorPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts
deleted file mode 100644 (file)
index eb60ddb..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
-  name: 'logColor'
-})
-export class LogColorPipe implements PipeTransform {
-  transform(value: any, args?: any): any {
-    if (value.priority === '[INF]') {
-      return ''; // Inherit
-    } else if (value.priority === '[WRN]') {
-      return {
-        color: '#ffa500',
-        'font-weight': 'bold'
-      };
-    } else if (value.priority === '[ERR]') {
-      return { color: '#FF2222' };
-    } else {
-      return '';
-    }
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts
deleted file mode 100644 (file)
index 37883a8..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { MdsSummaryPipe } from './mds-summary.pipe';
-
-describe('MdsSummaryPipe', () => {
-  it('create an instance', () => {
-    const pipe = new MdsSummaryPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts
deleted file mode 100644 (file)
index 9e6eeca..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
-  name: 'mdsSummary'
-})
-export class MdsSummaryPipe implements PipeTransform {
-  transform(value: any, args?: any): any {
-    if (!value) {
-      return '';
-    }
-
-    let standbys = 0;
-    let active = 0;
-    let standbyReplay = 0;
-    _.each(value.standbys, (s, i) => {
-      standbys += 1;
-    });
-
-    if (value.standbys && !value.filesystems) {
-      return standbys + ', no filesystems';
-    } else if (value.filesystems.length === 0) {
-      return 'no filesystems';
-    } else {
-      _.each(value.filesystems, (fs, i) => {
-        _.each(fs.mdsmap.info, (mds, j) => {
-          if (mds.state === 'up:standby-replay') {
-            standbyReplay += 1;
-          } else {
-            active += 1;
-          }
-        });
-      });
-
-      return active + ' active, ' + (standbys + standbyReplay) + ' standby';
-    }
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts
deleted file mode 100644 (file)
index fdab76c..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { MgrSummaryPipe } from './mgr-summary.pipe';
-
-describe('MgrSummaryPipe', () => {
-  it('create an instance', () => {
-    const pipe = new MgrSummaryPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts
deleted file mode 100644 (file)
index cf793e6..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
-  name: 'mgrSummary'
-})
-export class MgrSummaryPipe implements PipeTransform {
-  transform(value: any, args?: any): any {
-    if (!value) {
-      return '';
-    }
-
-    let result = 'active: ';
-    result += _.isUndefined(value.active_name) ? 'n/a' : value.active_name;
-
-    if (value.standbys.length) {
-      result += ', ' + value.standbys.length + ' standbys';
-    }
-
-    return result;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts
deleted file mode 100644 (file)
index 49526cf..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { MonSummaryPipe } from './mon-summary.pipe';
-
-describe('MonSummaryPipe', () => {
-  it('create an instance', () => {
-    const pipe = new MonSummaryPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts
deleted file mode 100644 (file)
index 6877e22..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
-  name: 'monSummary'
-})
-export class MonSummaryPipe implements PipeTransform {
-  transform(value: any, args?: any): any {
-    if (!value) {
-      return '';
-    }
-
-    let result = value.monmap.mons.length.toString() + ' (quorum ';
-    result += value.quorum.join(', ');
-    result += ')';
-
-    return result;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts
deleted file mode 100644 (file)
index 466eec1..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { OsdSummaryPipe } from './osd-summary.pipe';
-
-describe('OsdSummaryPipe', () => {
-  it('create an instance', () => {
-    const pipe = new OsdSummaryPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts
deleted file mode 100644 (file)
index b02d976..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
-  name: 'osdSummary'
-})
-export class OsdSummaryPipe implements PipeTransform {
-  transform(value: any, args?: any): any {
-    if (!value) {
-      return '';
-    }
-
-    let inCount = 0;
-    let upCount = 0;
-    _.each(value.osds, (osd, i) => {
-      if (osd.in) {
-        inCount++;
-      }
-      if (osd.up) {
-        upCount++;
-      }
-    });
-
-    return value.osds.length + ' (' + upCount + ' up, ' + inCount + ' in)';
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts
deleted file mode 100644 (file)
index 67c5f10..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { PgStatusStylePipe } from './pg-status-style.pipe';
-
-describe('PgStatusStylePipe', () => {
-  it('create an instance', () => {
-    const pipe = new PgStatusStylePipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts
deleted file mode 100644 (file)
index 4e9afab..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
-  name: 'pgStatusStyle'
-})
-export class PgStatusStylePipe implements PipeTransform {
-  transform(pgStatus: any, args?: any): any {
-    let warning = false;
-    let error = false;
-
-    _.each(pgStatus, (value, state) => {
-      if (
-        state.includes('inconsistent') ||
-        state.includes('incomplete') ||
-        !state.includes('active')
-      ) {
-        error = true;
-      }
-
-      if (
-        state !== 'active+clean' &&
-        state !== 'active+clean+scrubbing' &&
-        state !== 'active+clean+scrubbing+deep'
-      ) {
-        warning = true;
-      }
-    });
-
-    if (error) {
-      return { color: '#FF0000' };
-    }
-
-    if (warning) {
-      return { color: '#FFC200' };
-    }
-
-    return { color: '#00BB00' };
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts
deleted file mode 100644 (file)
index d7d5592..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { PgStatusPipe } from './pg-status.pipe';
-
-describe('PgStatusPipe', () => {
-  it('create an instance', () => {
-    const pipe = new PgStatusPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts
deleted file mode 100644 (file)
index 5c6c7b3..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
-  name: 'pgStatus'
-})
-export class PgStatusPipe implements PipeTransform {
-  transform(pgStatus: any, args?: any): any {
-    const strings = [];
-    _.each(pgStatus, (count, state) => {
-      strings.push(count + ' ' + state);
-    });
-
-    return strings.join(', ');
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts
deleted file mode 100644 (file)
index f2c2029..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-
-import { SharedModule } from '../../shared/shared.module';
-import {
-  PerformanceCounterComponent
-} from './performance-counter/performance-counter.component';
-import { TablePerformanceCounterService } from './services/table-performance-counter.service';
-import {
-  TablePerformanceCounterComponent
-} from './table-performance-counter/table-performance-counter.component';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    SharedModule,
-    RouterModule
-  ],
-  declarations: [
-    TablePerformanceCounterComponent,
-    PerformanceCounterComponent
-  ],
-  providers: [
-    TablePerformanceCounterService
-  ],
-  exports: [
-    TablePerformanceCounterComponent
-  ]
-})
-export class PerformanceCounterModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html
deleted file mode 100644 (file)
index ebb9ba9..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<fieldset>
-  <legend i18n>Performance Counters</legend>
-  <h3>{{ serviceType }}.{{ serviceId }}</h3>
-  <cd-table-performance-counter [serviceType]="serviceType"
-                                [serviceId]="serviceId">
-  </cd-table-performance-counter>
-</fieldset>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts
deleted file mode 100644 (file)
index a4cc717..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-
-import { PerformanceCounterModule } from '../performance-counter.module';
-import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
-import { PerformanceCounterComponent } from './performance-counter.component';
-
-describe('PerformanceCounterComponent', () => {
-  let component: PerformanceCounterComponent;
-  let fixture: ComponentFixture<PerformanceCounterComponent>;
-
-  const fakeService = {
-    get: (service_type: string, service_id: string) => {
-      return new Promise(function(resolve, reject) {
-        return [];
-      });
-    },
-    list: () => {
-      return new Promise(function(resolve, reject) {
-        return {};
-      });
-    }
-  };
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        imports: [
-          PerformanceCounterModule,
-          BsDropdownModule.forRoot(),
-          RouterTestingModule
-        ],
-        providers: [{ provide: TablePerformanceCounterService, useValue: fakeService }]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(PerformanceCounterComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts
deleted file mode 100644 (file)
index 25fa82e..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Component, OnDestroy } 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 implements OnDestroy {
-  serviceId: string;
-  serviceType: string;
-  routeParamsSubscribe: any;
-
-  constructor(private route: ActivatedRoute) {
-    this.routeParamsSubscribe = this.route.params.subscribe(
-      (params: { type: string; id: string }) => {
-        this.serviceId = params.id;
-        this.serviceType = params.type;
-      }
-    );
-  }
-
-  ngOnDestroy() {
-    this.routeParamsSubscribe.unsubscribe();
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts
deleted file mode 100644 (file)
index 6f0af94..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-
-import { TablePerformanceCounterService } from './table-performance-counter.service';
-
-describe('TablePerformanceCounterService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [TablePerformanceCounterService],
-      imports: [
-        HttpClientTestingModule,
-        BsDropdownModule.forRoot(),
-        HttpClientModule
-      ]
-    });
-  });
-
-  it(
-    'should be created',
-    inject([TablePerformanceCounterService], (service: TablePerformanceCounterService) => {
-      expect(service).toBeTruthy();
-    })
-  );
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts
deleted file mode 100644 (file)
index b6ac5d5..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class TablePerformanceCounterService {
-
-  private url = 'api/perf_counters';
-
-  constructor(private http: HttpClient) { }
-
-  list() {
-    return this.http.get(this.url)
-      .toPromise()
-      .then((resp: object): object => {
-        return resp;
-      });
-  }
-
-  get(service_type: string, service_id: string) {
-    const serviceType = service_type.replace('-', '_');
-
-    return this.http.get(`${this.url}/${serviceType}/${service_id}`)
-      .toPromise()
-      .then((resp: object): Array<object> => {
-        return resp['counters'];
-      });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html
deleted file mode 100644 (file)
index 6564dc1..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<cd-table [data]="counters"
-          [columns]="columns"
-          columnMode="flex"
-          (fetchData)="getCounters()">
-  <ng-template #valueTpl let-row="row">
-    {{ row.value | dimless }} {{ row.unit }}
-  </ng-template>
-</cd-table>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts
deleted file mode 100644 (file)
index 4baefe8..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
-import { TablePerformanceCounterComponent } from './table-performance-counter.component';
-
-describe('TablePerformanceCounterComponent', () => {
-  let component: TablePerformanceCounterComponent;
-  let fixture: ComponentFixture<TablePerformanceCounterComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ TablePerformanceCounterComponent ],
-      imports: [
-        HttpClientTestingModule,
-        HttpClientModule,
-        BsDropdownModule.forRoot(),
-        SharedModule
-      ],
-      providers: [ TablePerformanceCounterService ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(TablePerformanceCounterComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts
deleted file mode 100644 (file)
index 6ac05c9..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
-
-/**
- * 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: TablePerformanceCounterService) { }
-
-  ngOnInit() {
-    this.columns = [
-      {
-        name: 'Name',
-        prop: 'name',
-        flexGrow: 1
-      },
-      {
-        name: 'Description',
-        prop: 'description',
-        flexGrow: 1
-      },
-      {
-        name: 'Value',
-        cellTemplate: this.valueTpl,
-        flexGrow: 1
-      }
-    ];
-  }
-
-  getCounters() {
-    this.performanceCounterService.get(this.serviceType, this.serviceId)
-      .then((resp) => {
-        this.counters = resp;
-      });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html
deleted file mode 100644 (file)
index 81c5919..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<tabset *ngIf="selection.hasSingleSelection">
-  <tab i18n-heading
-       heading="Details">
-    <cd-table-key-value [data]="metadata"
-                        (fetchData)="getMetaData()">
-    </cd-table-key-value>
-  </tab>
-  <tab i18n-heading
-       heading="Performance Counters">
-    <cd-table-performance-counter serviceType="rgw"
-                                  [serviceId]="serviceId">
-    </cd-table-performance-counter>
-  </tab>
-</tabset>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts
deleted file mode 100644 (file)
index afce56a..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
-import { SharedModule } from '../../../shared/shared.module';
-import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
-import { RgwDaemonService } from '../services/rgw-daemon.service';
-import { RgwDaemonDetailsComponent } from './rgw-daemon-details.component';
-
-describe('RgwDaemonDetailsComponent', () => {
-  let component: RgwDaemonDetailsComponent;
-  let fixture: ComponentFixture<RgwDaemonDetailsComponent>;
-
-  const fakeService = {
-    get: (id: string) => {
-      return new Promise(function(resolve, reject) {
-        return [];
-      });
-    }
-  };
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ RgwDaemonDetailsComponent ],
-      imports: [
-        SharedModule,
-        PerformanceCounterModule,
-        TabsModule.forRoot()
-      ],
-      providers: [{ provide: RgwDaemonService, useValue: fakeService }]
-    });
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(RgwDaemonDetailsComponent);
-    component = fixture.componentInstance;
-
-    component.selection = new CdTableSelection();
-
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts
deleted file mode 100644 (file)
index 8ac62fa..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Component, Input, OnChanges } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
-import { RgwDaemonService } from '../services/rgw-daemon.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 = '';
-
-  @Input() selection: CdTableSelection;
-
-  constructor(private rgwDaemonService: RgwDaemonService) {}
-
-  ngOnChanges() {
-    // Get the service id of the first selected row.
-    if (this.selection.hasSelection) {
-      this.serviceId = this.selection.first().id;
-    }
-  }
-
-  getMetaData() {
-    if (_.isEmpty(this.serviceId)) {
-      return;
-    }
-    this.rgwDaemonService.get(this.serviceId).then(resp => {
-      this.metadata = resp['rgw_metadata'];
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html
deleted file mode 100644 (file)
index 64b703f..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li i18n
-        class="breadcrumb-item active"
-        aria-current="page">Object Gateway</li>
-  </ol>
-</nav>
-
-<cd-table [data]="daemons"
-          [columns]="columns"
-          columnMode="flex"
-          selectionType="single"
-          (updateSelection)="updateSelection($event)"
-          (fetchData)="getDaemonList()">
-  <cd-rgw-daemon-details cdTableDetail
-                         [selection]="selection">
-  </cd-rgw-daemon-details>
-</cd-table>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts
deleted file mode 100644 (file)
index c0d331e..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { DataTableModule } from '../../../shared/datatable/datatable.module';
-import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
-import { RgwDaemonDetailsComponent } from '../rgw-daemon-details/rgw-daemon-details.component';
-import { RgwDaemonService } from '../services/rgw-daemon.service';
-import { RgwDaemonListComponent } from './rgw-daemon-list.component';
-
-describe('RgwDaemonListComponent', () => {
-  let component: RgwDaemonListComponent;
-  let fixture: ComponentFixture<RgwDaemonListComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ RgwDaemonListComponent, RgwDaemonDetailsComponent ],
-      imports: [
-        DataTableModule,
-        HttpClientTestingModule,
-        HttpClientModule,
-        TabsModule.forRoot(),
-        PerformanceCounterModule
-      ],
-      providers: [ RgwDaemonService ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(RgwDaemonListComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts
deleted file mode 100644 (file)
index ce1c245..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Component } from '@angular/core';
-
-import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
-import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
-import { RgwDaemonService } from '../services/rgw-daemon.service';
-
-@Component({
-  selector: 'cd-rgw-daemon-list',
-  templateUrl: './rgw-daemon-list.component.html',
-  styleUrls: ['./rgw-daemon-list.component.scss']
-})
-export class RgwDaemonListComponent {
-
-  columns: Array<CdTableColumn> = [];
-  daemons: Array<object> = [];
-  selection = new CdTableSelection();
-
-  constructor(private rgwDaemonService: RgwDaemonService,
-              cephShortVersionPipe: CephShortVersionPipe) {
-    this.columns = [
-      {
-        name: 'ID',
-        prop: 'id',
-        flexGrow: 2
-      },
-      {
-        name: 'Hostname',
-        prop: 'server_hostname',
-        flexGrow: 2
-      },
-      {
-        name: 'Version',
-        prop: 'version',
-        flexGrow: 1,
-        pipe: cephShortVersionPipe
-      }
-    ];
-  }
-
-  getDaemonList() {
-    this.rgwDaemonService.list()
-      .then((resp) => {
-        this.daemons = resp;
-      });
-  }
-
-  updateSelection(selection: CdTableSelection) {
-    this.selection = selection;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts
deleted file mode 100644 (file)
index a888940..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { SharedModule } from '../../shared/shared.module';
-import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
-import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component';
-import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component';
-import { RgwDaemonService } from './services/rgw-daemon.service';
-
-@NgModule({
-  entryComponents: [
-    RgwDaemonDetailsComponent
-  ],
-  imports: [
-    CommonModule,
-    SharedModule,
-    PerformanceCounterModule,
-    TabsModule.forRoot()
-  ],
-  exports: [
-    RgwDaemonListComponent,
-    RgwDaemonDetailsComponent
-  ],
-  declarations: [
-    RgwDaemonListComponent,
-    RgwDaemonDetailsComponent
-  ],
-  providers: [
-    RgwDaemonService
-  ]
-})
-export class RgwModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts
deleted file mode 100644 (file)
index 691cc78..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { RgwDaemonService } from './rgw-daemon.service';
-
-describe('RgwDaemonService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [RgwDaemonService],
-      imports: [HttpClientTestingModule, HttpClientModule]
-    });
-  });
-
-  it(
-    'should be created',
-    inject([RgwDaemonService], (service: RgwDaemonService) => {
-      expect(service).toBeTruthy();
-    })
-  );
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts
deleted file mode 100644 (file)
index 907537e..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class RgwDaemonService {
-
-  private url = 'api/rgw/daemon';
-
-  constructor(private http: HttpClient) { }
-
-  list() {
-    return this.http.get(this.url)
-      .toPromise()
-      .then((resp: any) => {
-        return resp;
-      });
-  }
-
-  get(id: string) {
-    return this.http.get(`${this.url}/${id}`)
-      .toPromise()
-      .then((resp: any) => {
-        return resp;
-      });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts
deleted file mode 100644 (file)
index e96b1b3..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { SharedModule } from '../../shared/shared.module';
-
-import { LoginComponent } from './login/login.component';
-import { LogoutComponent } from './logout/logout.component';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    FormsModule,
-    SharedModule
-  ],
-  declarations: [LoginComponent, LogoutComponent],
-  exports: [LogoutComponent]
-})
-export class AuthModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html
deleted file mode 100644 (file)
index e0b33c8..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-<div class="login">
-  <div class="row full-height vertical-align">
-    <div class="col-sm-6 hidden-xs">
-      <img src="assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png"
-           alt="Ceph"
-           class="pull-right">
-    </div>
-    <div class="col-xs-10 col-sm-4 col-lg-3 col-xs-offset-1 col-sm-offset-0 col-md-offset-0 col-lg-offset-0">
-      <h1 i18n="The welcome message on the login page">Welcome to Ceph!</h1>
-      <form name="loginForm"
-            (ngSubmit)="login()"
-            #loginForm="ngForm"
-            novalidate>
-
-        <!-- Username -->
-        <div class="form-group has-feedback"
-             [ngClass]="{'has-error': (loginForm.submitted || username.dirty) && username.invalid}">
-          <input name="username"
-                 [(ngModel)]="model.username"
-                 #username="ngModel"
-                 type="text"
-                 placeholder="Enter your username..."
-                 class="form-control"
-                 required
-                 autofocus>
-          <div class="help-block"
-               *ngIf="(loginForm.submitted || username.dirty) && username.invalid">Username is required</div>
-        </div>
-
-        <!-- Password -->
-        <div class="form-group has-feedback"
-             [ngClass]="{'has-error': (loginForm.submitted || password.dirty) && password.invalid}">
-          <div class="input-group">
-            <input id="password"
-                   name="password"
-                   [(ngModel)]="model.password"
-                   #password="ngModel"
-                   type="password"
-                   placeholder="Enter your password..."
-                   class="form-control"
-                   required>
-            <span class="input-group-btn">
-              <button type="button"
-                      class="btn btn-default btn-password"
-                      cdPasswordButton="password">
-              </button>
-            </span>
-          </div>
-          <div class="help-block"
-               *ngIf="(loginForm.submitted || password.dirty) && password.invalid">Password is required
-          </div>
-        </div>
-
-        <!-- Stay signed in -->
-        <div class="checkbox checkbox-primary">
-          <input id="stay_signed_in"
-                 name="stay_signed_in"
-                 type="checkbox"
-                 [(ngModel)]="model.stay_signed_in">
-          <label for="stay_signed_in"
-                 i18n="A checkbox on the login page to do not expire session on browser close">
-            Keep me logged in
-          </label>
-        </div>
-
-        <input type="submit"
-               class="btn btn-openattic btn-block"
-               [disabled]="loginForm.invalid"
-               value="Login">
-      </form>
-    </div>
-  </div>
-</div>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss
deleted file mode 100644 (file)
index 1f77356..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-@import '../../../../defaults';
-
-.login {
-  height: 100%;
-
-  .row {
-    color: #ececec;
-    background-color: #474544;
-  }
-
-  h1 {
-    margin-top: 0;
-    margin-bottom: 30px;
-  }
-
-  .btn-password,
-  .form-control {
-    color: #ececec;
-    background-color: #555555;
-  }
-
-  .btn-password:focus {
-    outline-color: #66afe9;
-  }
-
-  .checkbox-primary input[type="checkbox"]:checked + label::before,
-  .checkbox-primary input[type="radio"]:checked + label::before {
-    background-color: $oa-color-blue;
-    border-color: $oa-color-blue;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts
deleted file mode 100644 (file)
index b8307b5..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { ToastModule } from 'ng2-toastr';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { LoginComponent } from './login.component';
-
-describe('LoginComponent', () => {
-  let component: LoginComponent;
-  let fixture: ComponentFixture<LoginComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      imports: [
-        FormsModule,
-        SharedModule,
-        RouterTestingModule,
-        HttpClientTestingModule,
-        ToastModule.forRoot()
-      ],
-      declarations: [
-        LoginComponent
-      ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(LoginComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts
deleted file mode 100644 (file)
index f8f4625..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Component, OnInit, ViewContainerRef } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { ToastsManager } from 'ng2-toastr';
-
-import { Credentials } from '../../../shared/models/credentials';
-import { AuthStorageService } from '../../../shared/services/auth-storage.service';
-import { AuthService } from '../../../shared/services/auth.service';
-
-@Component({
-  selector: 'cd-login',
-  templateUrl: './login.component.html',
-  styleUrls: ['./login.component.scss']
-})
-export class LoginComponent implements OnInit {
-
-  model = new Credentials();
-
-  constructor(private authService: AuthService,
-              private authStorageService: AuthStorageService,
-              private router: Router,
-              public toastr: ToastsManager,
-              private vcr: ViewContainerRef) {
-    this.toastr.setRootViewContainerRef(vcr);
-  }
-
-  ngOnInit() {
-    if (this.authStorageService.isLoggedIn()) {
-      this.router.navigate(['']);
-    }
-  }
-
-  login() {
-    this.authService.login(this.model).then(() => {
-      this.router.navigate(['']);
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html
deleted file mode 100644 (file)
index 993fd95..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<a i18n-title
-   title="Sign Out"
-   (click)="logout()">
-  <i class="fa fa-sign-out"></i>
-  <ng-container i18n>Logout</ng-container>
-</a>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts
deleted file mode 100644 (file)
index 318ca82..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { LogoutComponent } from './logout.component';
-
-describe('LogoutComponent', () => {
-  let component: LogoutComponent;
-  let fixture: ComponentFixture<LogoutComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      imports: [
-        SharedModule,
-        RouterTestingModule,
-        HttpClientTestingModule
-      ],
-      declarations: [
-        LogoutComponent
-      ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(LogoutComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts
deleted file mode 100644 (file)
index 4bf11e3..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { AuthService } from '../../../shared/services/auth.service';
-
-@Component({
-  selector: 'cd-logout',
-  templateUrl: './logout.component.html',
-  styleUrls: ['./logout.component.scss']
-})
-export class LogoutComponent implements OnInit {
-
-  constructor(private authService: AuthService,
-              private router: Router) { }
-
-  ngOnInit() {
-  }
-
-  logout() {
-    this.authService.logout().then(() => {
-      this.router.navigate(['/login']);
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts
deleted file mode 100644 (file)
index bd17681..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { AuthModule } from './auth/auth.module';
-import { NavigationModule } from './navigation/navigation.module';
-import { NotFoundComponent } from './not-found/not-found.component';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    NavigationModule,
-    AuthModule
-  ],
-  exports: [NavigationModule],
-  declarations: [NotFoundComponent]
-})
-export class CoreModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts
deleted file mode 100644 (file)
index 823d4fe..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-
-import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
-
-import { AppRoutingModule } from '../../app-routing.module';
-import { SharedModule } from '../../shared/shared.module';
-import { AuthModule } from '../auth/auth.module';
-import { NavigationComponent } from './navigation/navigation.component';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    AuthModule,
-    BsDropdownModule.forRoot(),
-    AppRoutingModule,
-    SharedModule,
-    RouterModule
-  ],
-  declarations: [NavigationComponent],
-  exports: [NavigationComponent]
-})
-export class NavigationModule {}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html
deleted file mode 100644 (file)
index cd7e804..0000000
+++ /dev/null
@@ -1,228 +0,0 @@
-<nav class="navbar navbar-default navbar-openattic">
-  <!-- Brand and toggle get grouped for better mobile display -->
-
-  <div class="navbar-header tc_logo_component">
-    <a class="navbar-brand"
-       href="#">
-      <img src="assets/Ceph_Logo_Standard_RGB_White_120411_fa.png"
-           alt="Ceph">
-    </a>
-
-    <button type="button"
-            class="navbar-toggle collapsed"
-            data-toggle="collapse"
-            data-target="#bs-example-navbar-collapse-1">
-      <span i18n
-            class="sr-only">Toggle navigation
-      </span>
-      <span class="icon-bar"></span>
-      <span class="icon-bar"></span>
-      <span class="icon-bar"></span>
-    </button>
-  </div>
-
-  <!-- Collect the nav links, forms, and other content for toggling -->
-  <div class="collapse navbar-collapse"
-       id="bs-example-navbar-collapse-1">
-    <ul class="nav navbar-nav navbar-primary">
-
-      <!-- Dashboard -->
-      <li routerLinkActive="active"
-          class="tc_menuitem tc_menuitem_dashboard">
-        <a i18n
-           routerLink="/dashboard">
-          <i class="fa fa-heartbeat fa-fw"
-             [ngStyle]="summaryData?.health_status | healthColor"></i>
-          <span>Dashboard</span>
-        </a>
-      </li>
-
-      <!-- Cluster -->
-      <li dropdown
-          routerLinkActive="active"
-          class="dropdown tc_menuitem tc_menuitem_cluster">
-        <a dropdownToggle
-           class="dropdown-toggle"
-           data-toggle="dropdown">
-          <ng-container i18n>Cluster</ng-container>
-          <span class="caret"></span>
-        </a>
-        <ul *dropdownMenu
-            class="dropdown-menu">
-          <li routerLinkActive="active"
-              class="tc_submenuitem tc_submenuitem_hosts">
-            <a i18n
-               class="dropdown-item"
-               routerLink="/hosts">Hosts
-            </a>
-          </li>
-          <li routerLinkActive="active"
-              class="tc_submenuitem tc_submenuitem_cluster_monitor">
-            <a i18n
-               class="dropdown-item"
-               routerLink="/monitor/"> Monitors
-            </a>
-          </li>
-          <li routerLinkActive="active"
-              class="tc_submenuitem tc_submenuitem_hosts">
-            <a i18n
-               class="dropdown-item"
-               routerLink="/osd">OSDs
-            </a>
-          </li>
-          <li routerLinkActive="active"
-              class="tc_submenuitem tc_submenuitem_configuration">
-            <a i18n
-               class="dropdown-item"
-               routerLink="/configuration">Configuration Doc.
-            </a>
-          </li>
-        </ul>
-      </li>
-
-      <!-- Block -->
-      <li dropdown
-          routerLinkActive="active"
-          class="dropdown tc_menuitem tc_menuitem_block">
-        <a dropdownToggle
-           class="dropdown-toggle"
-           data-toggle="dropdown"
-           [ngStyle]="blockHealthColor()">
-          <ng-container i18n>Block</ng-container>
-          <span class="caret"></span>
-        </a>
-
-        <ul class="dropdown-menu">
-          <li routerLinkActive="active"
-              class="tc_submenuitem tc_submenuitem_block_mirroring">
-            <a i18n
-               class="dropdown-item"
-               routerLink="/mirroring/"> Mirroring
-              <small *ngIf="summaryData?.rbd_mirroring?.warnings !== 0"
-                     class="label label-warning">{{ summaryData?.rbd_mirroring?.warnings }}</small>
-              <small *ngIf="summaryData?.rbd_mirroring?.errors !== 0"
-                     class="label label-danger">{{ summaryData?.rbd_mirroring?.errors }}</small>
-            </a>
-          </li>
-
-          <li routerLinkActive="active">
-            <a i18n
-               class="dropdown-item"
-               routerLink="/block/iscsi">iSCSI</a>
-          </li>
-
-          <li class="dropdown-submenu">
-            <a class="dropdown-toggle"
-               data-toggle="dropdown">Pools</a>
-            <ul *dropdownMenu
-                class="dropdown-menu">
-              <li routerLinkActive="active"
-                  class="tc_submenuitem tc_submenuitem_pools"
-                  *ngFor="let rbdPool of rbdPools">
-                <a i18n
-                   class="dropdown-item"
-                   routerLink="/block/pool/{{ rbdPool }}">{{ rbdPool }}
-                </a>
-              </li>
-              <li class="tc_submenuitem tc_submenuitem_cephfs_nofs"
-                  *ngIf="rbdPools.length === 0">
-                <a class="dropdown-item disabled"
-                   i18n>There are no pools</a>
-              </li>
-            </ul>
-          </li>
-        </ul>
-      </li>
-
-      <!-- Filesystem -->
-      <li dropdown
-          routerLinkActive="active"
-          class="dropdown tc_menuitem tc_menuitem_cephs">
-        <a dropdownToggle
-           class="dropdown-toggle"
-           data-toggle="dropdown">
-          <ng-container i18n>Filesystems</ng-container>
-          <span class="caret"></span>
-        </a>
-        <ul *dropdownMenu
-            class="dropdown-menu">
-          <li routerLinkActive="active"
-              class="tc_submenuitem tc_submenuitem_cephfs_fs"
-              *ngFor="let fs of summaryData?.filesystems">
-            <a i18n
-               class="dropdown-item"
-               routerLink="/cephfs/{{fs.id}}">{{ fs.name }}
-            </a>
-          </li>
-          <li class="tc_submenuitem tc_submenuitem_cephfs_nofs"
-              *ngIf="summaryData.filesystems.length === 0">
-            <span i18n>There are no filesystems</span>
-          </li>
-        </ul>
-      </li>
-      <!--
-  <li routerLinkActive="active"
-          class="tc_menuitem tc_menuitem_ceph_osds">
-        <a i18n
-           routerLink="/cephOsds">OSDs
-        </a>
-      </li>
-      <li routerLinkActive="active"
-          class="tc_menuitem tc_menuitem_ceph_pools">
-        <a i18n
-           routerLink="/cephPools">Pools
-        </a>
-      </li>
-      -->
-
-      <!-- Object Gateway -->
-      <li routerLinkActive="active"
-          class="tc_menuitem tc_menuitem_rgw">
-        <a i18n
-           routerLink="/rgw">Object Gateway
-        </a>
-      </li>
-
-      <!--<li class="dropdown tc_menuitem tc_menuitem_ceph_rgw">
-        <a href=""
-           class="dropdown-toggle"
-           data-toggle="dropdown">
-          <ng-container i18n>Object Gateway</ng-container>
-          <span class="caret"></span>
-        </a>
-        <ul *dropdownMenu
-            class="dropdown-menu">
-          <li routerLinkActive="active"
-              class="tc_submenuitem tc_submenuitem_rgw_users">
-            <a i18n
-               class="dropdown-item"
-               routerLink="/rgw-users">Users
-            </a>
-          </li>
-          <li routerLinkActive="active"
-              class="tc_submenuitem tc_submenuitem_rgw_buckets">
-            <a i18n
-               class="dropdown-item"
-               routerLink="/rgw-buckets">Buckets
-            </a>
-          </li>
-        </ul>
-      </li>
-      <li routerLinkActive="active"
-          class="tc_menuitem tc_submenuitem_settings">
-        <a i18n
-           routerLink="/settings">Settings
-        </a>
-      </li> -->
-    </ul>
-    <!-- /.navbar-primary -->
-
-    <ul class="nav navbar-nav navbar-utility">
-      <li class="tc_logout">
-        <cd-logout class="oa-navbar"></cd-logout>
-      </li>
-    </ul>
-    <!-- /.navbar-utility -->
-  </div>
-  <!-- /.navbar-collapse -->
-</nav>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts
deleted file mode 100644 (file)
index 7548b2b..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { LogoutComponent } from '../../auth/logout/logout.component';
-import { NavigationComponent } from './navigation.component';
-
-describe('NavigationComponent', () => {
-  let component: NavigationComponent;
-  let fixture: ComponentFixture<NavigationComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      imports: [
-        SharedModule,
-        RouterTestingModule,
-        HttpClientTestingModule
-      ],
-      declarations: [
-        NavigationComponent,
-        LogoutComponent
-      ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(NavigationComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.ts
deleted file mode 100644 (file)
index ee61c41..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { SummaryService } from '../../../shared/services/summary.service';
-
-@Component({
-  selector: 'cd-navigation',
-  templateUrl: './navigation.component.html',
-  styleUrls: ['./navigation.component.scss']
-})
-export class NavigationComponent implements OnInit {
-  summaryData: any;
-  rbdPools: Array<any> = [];
-
-  constructor(private summaryService: SummaryService) {}
-
-  ngOnInit() {
-    this.summaryService.summaryData$.subscribe((data: any) => {
-      this.summaryData = data;
-      this.rbdPools = data.rbd_pools;
-    });
-  }
-
-  blockHealthColor() {
-    if (this.summaryData && this.summaryData.rbd_mirroring) {
-      if (this.summaryData.rbd_mirroring.errors > 0) {
-        return { color: '#d9534f' };
-      } else if (this.summaryData.rbd_mirroring.warnings > 0) {
-        return { color: '#f0ad4e' };
-      }
-    }
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.html
deleted file mode 100644 (file)
index 0f3847b..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="row">
-  <div class="col-md-12 text-center">
-    <h1 i18n>Sorry, we could not find what you were looking for</h1>
-
-    <img class="img-responsive center-block img-rounded"
-         src="/assets/1280px-Mimic_Octopus2.jpg">
-    <span>
-      "<a href="https://www.flickr.com/photos/37707866@N00/4838953223">Mimic Octopus</a>" by prilfish is licensed under
-      <a rel="nofollow"
-         class="external text"
-         href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0</a>
-    </span>
-  </div>
-</div>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.scss
deleted file mode 100644 (file)
index e94d9f2..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-h1 {
-  font-size: -webkit-xxx-large;
-}
-
-h2 {
-  font-size: xx-large;
-}
-
-*{
-  font-family: monospace;
-}
-
-img{
-  width: 50vw;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.spec.ts
deleted file mode 100644 (file)
index 35189ed..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { NotFoundComponent } from './not-found.component';
-
-describe('NotFoundComponent', () => {
-  let component: NotFoundComponent;
-  let fixture: ComponentFixture<NotFoundComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ NotFoundComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(NotFoundComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.ts
deleted file mode 100644 (file)
index d12bc32..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
-  selector: 'cd-not-found',
-  templateUrl: './not-found.component.html',
-  styleUrls: ['./not-found.component.scss']
-})
-export class NotFoundComponent {
-  constructor() {}
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/components.module.ts
deleted file mode 100644 (file)
index fe65bea..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-import { AlertModule } from 'ngx-bootstrap';
-
-import { SparklineComponent } from './sparkline/sparkline.component';
-import { ViewCacheComponent } from './view-cache/view-cache.component';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    AlertModule.forRoot(),
-    ChartsModule
-  ],
-  declarations: [
-    ViewCacheComponent,
-    SparklineComponent
-  ],
-  providers: [],
-  exports: [
-    ViewCacheComponent,
-    SparklineComponent
-  ]
-})
-export class ComponentsModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html
deleted file mode 100644 (file)
index 4b7a1b8..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<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_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss
deleted file mode 100644 (file)
index ec7d982..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-@import '../../../../styles/chart-tooltip.scss';
-
-.chart-container {
-  position: static !important;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts
deleted file mode 100644 (file)
index 4a879c3..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AppModule } from '../../../app.module';
-import { SparklineComponent } from './sparkline.component';
-
-describe('SparklineComponent', () => {
-  let component: SparklineComponent;
-  let fixture: ComponentFixture<SparklineComponent>;
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        imports: [AppModule]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(SparklineComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts
deleted file mode 100644 (file)
index fa20ce3..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-import { Component, ElementRef, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
-import { Input } from '@angular/core';
-
-import { ChartTooltip } from '../../../shared/models/chart-tooltip';
-
-@Component({
-  selector: 'cd-sparkline',
-  templateUrl: './sparkline.component.html',
-  styleUrls: ['./sparkline.component.scss']
-})
-export class SparklineComponent implements OnInit, OnChanges {
-  @ViewChild('sparkCanvas') chartCanvasRef: ElementRef;
-  @ViewChild('sparkTooltip') chartTooltipRef: ElementRef;
-
-  @Input() data: any;
-  @Input()
-  style = {
-    height: '30px',
-    width: '100px'
-  };
-
-  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 = {
-    animation: {
-      duration: 0
-    },
-    responsive: true,
-    maintainAspectRatio: false,
-    legend: {
-      display: false
-    },
-    elements: {
-      line: {
-        borderWidth: 1
-      }
-    },
-    tooltips: {
-      enabled: false,
-      mode: 'index',
-      intersect: false,
-      custom: undefined
-    },
-    scales: {
-      yAxes: [
-        {
-          display: false
-        }
-      ],
-      xAxes: [
-        {
-          display: false
-        }
-      ]
-    }
-  };
-
-  public datasets: Array<any> = [
-    {
-      data: []
-    }
-  ];
-
-  public labels: Array<any> = [];
-
-  constructor() {}
-
-  ngOnInit() {
-    const getStyleTop = (tooltip, positionY) => {
-      return (tooltip.caretY - tooltip.height - tooltip.yPadding - 5) + 'px';
-    };
-
-    const getStyleLeft = (tooltip, positionX) => {
-      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 => {
-      chartTooltip.customTooltips(tooltip);
-    };
-  }
-
-  ngOnChanges(changes: SimpleChanges) {
-    this.datasets[0].data = changes['data'].currentValue;
-    this.datasets = [...this.datasets];
-    this.labels = [...Array(changes['data'].currentValue.length)];
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.html
deleted file mode 100644 (file)
index 1d71da2..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-<alert i18n
-       type="info"
-       *ngIf="status === vcs.ValueNone">
-  Retrieving data, please wait.
-</alert>
-
-<alert i18n
-       type="warning"
-       *ngIf="status === vcs.ValueStale">
-  Displaying previously cached data.
-</alert>
-
-<alert i18n
-       type="danger"
-       *ngIf="status === vcs.ValueException">
-  Could not load data. Please check the cluster health.
-</alert>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts
deleted file mode 100644 (file)
index da68def..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AlertModule } from 'ngx-bootstrap';
-
-import { ViewCacheComponent } from './view-cache.component';
-
-describe('ViewCacheComponent', () => {
-  let component: ViewCacheComponent;
-  let fixture: ComponentFixture<ViewCacheComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ ViewCacheComponent ],
-      imports: [AlertModule.forRoot()]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(ViewCacheComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.ts
deleted file mode 100644 (file)
index 63bc979..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Component, Input, OnInit } from '@angular/core';
-
-import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-
-@Component({
-  selector: 'cd-view-cache',
-  templateUrl: './view-cache.component.html',
-  styleUrls: ['./view-cache.component.scss']
-})
-export class ViewCacheComponent implements OnInit {
-  @Input() status: ViewCacheStatus;
-  vcs = ViewCacheStatus;
-
-  constructor() {}
-
-  ngOnInit() {}
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/datatable.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/datatable.module.ts
deleted file mode 100644 (file)
index b09a31e..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { RouterModule } from '@angular/router';
-
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
-import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
-
-import { ComponentsModule } from '../components/components.module';
-import { PipesModule } from '../pipes/pipes.module';
-import { TableKeyValueComponent } from './table-key-value/table-key-value.component';
-import { TableComponent } from './table/table.component';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    NgxDatatableModule,
-    FormsModule,
-    BsDropdownModule.forRoot(),
-    PipesModule,
-    ComponentsModule,
-    RouterModule
-  ],
-  declarations: [
-    TableComponent,
-    TableKeyValueComponent
-  ],
-  exports: [
-    TableComponent,
-    NgxDatatableModule,
-    TableKeyValueComponent
-  ]
-})
-export class DataTableModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html
deleted file mode 100644 (file)
index d0ab74c..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<cd-table [data]="tableData"
-          [columns]="columns"
-          columnMode="flex"
-          [toolHeader]="false"
-          [header]="false"
-          [footer]="false"
-          [limit]="0"
-          (fetchData)="reloadData()">
-</cd-table>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts
deleted file mode 100644 (file)
index 16e05bd..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
-
-import { ComponentsModule } from '../../components/components.module';
-import { TableComponent } from '../table/table.component';
-import { TableKeyValueComponent } from './table-key-value.component';
-
-describe('TableKeyValueComponent', () => {
-  let component: TableKeyValueComponent;
-  let fixture: ComponentFixture<TableKeyValueComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ TableComponent, TableKeyValueComponent ],
-      imports: [ FormsModule, NgxDatatableModule, ComponentsModule, RouterTestingModule ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(TableKeyValueComponent);
-    component = fixture.componentInstance;
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-
-  it('should make key value object pairs out of arrays with length two', () => {
-    component.data = [
-      ['someKey', 0],
-      [3, 'something'],
-    ];
-    component.ngOnInit();
-    expect(component.tableData.length).toBe(2);
-    expect(component.tableData[0].key).toBe('someKey');
-    expect(component.tableData[1].value).toBe('something');
-  });
-
-  it('should transform arrays', () => {
-    component.data = [
-      ['someKey', [1, 2, 3]],
-      [3, 'something']
-    ];
-    component.ngOnInit();
-    expect(component.tableData.length).toBe(2);
-    expect(component.tableData[0].key).toBe('someKey');
-    expect(component.tableData[0].value).toBe('1, 2, 3');
-    expect(component.tableData[1].value).toBe('something');
-  });
-
-  it('should remove pure object values', () => {
-    component.data = [
-      [3, 'something'],
-      ['will be removed', { a: 3, b: 4, c: 5}]
-    ];
-    component.ngOnInit();
-    expect(component.tableData.length).toBe(1);
-    expect(component.tableData[0].value).toBe('something');
-  });
-
-  it('should make key value object pairs out of an object', () => {
-    component.data = {
-      3: 'something',
-      someKey: 0
-    };
-    component.ngOnInit();
-    expect(component.tableData.length).toBe(2);
-    expect(component.tableData[0].value).toBe('something');
-    expect(component.tableData[1].key).toBe('someKey');
-  });
-
-  it('should make do nothing if data is correct', () => {
-    component.data = [
-      {
-        key: 3,
-        value: 'something'
-      },
-      {
-        key: 'someKey',
-        value: 0
-      }
-    ];
-    component.ngOnInit();
-    expect(component.tableData.length).toBe(2);
-    expect(component.tableData[0].value).toBe('something');
-    expect(component.tableData[1].key).toBe('someKey');
-  });
-
-  it('should throw error if miss match', () => {
-    component.data = 38;
-    expect(() => component.ngOnInit()).toThrowError('Wrong data format');
-    component.data = [['someKey', 0, 3]];
-    expect(() => component.ngOnInit()).toThrowError('Wrong array format');
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts
deleted file mode 100644 (file)
index 101580f..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { CellTemplate } from '../../enum/cell-template.enum';
-import { CdTableColumn } from '../../models/cd-table-column';
-
-/**
- * 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 {
-
-  columns: Array<CdTableColumn> = [];
-
-  @Input() data: any;
-
-  tableData: {
-    key: string,
-    value: any
-  }[];
-
-  /**
-   * The function that will be called to update the input data.
-   */
-  @Output() fetchData = new EventEmitter();
-
-  constructor() { }
-
-  ngOnInit() {
-    this.columns = [
-      {
-        prop: 'key',
-        flexGrow: 1,
-        cellTransformation: CellTemplate.bold
-      },
-      {
-        prop: 'value',
-        flexGrow: 3
-      }
-    ];
-    this.useData();
-  }
-
-  ngOnChanges(changes) {
-    this.useData();
-  }
-
-  useData() {
-    let temp = [];
-    if (!this.data) {
-      return; // Wait for data
-    } else if (_.isArray(this.data)) {
-      const first = this.data[0];
-      if (_.isPlainObject(first) && _.has(first, 'key') && _.has(first, 'value')) {
-        temp = [...this.data];
-      } else {
-        if (_.isArray(first)) {
-          if (first.length === 2) {
-            temp = this.data.map(a => ({
-              key: a[0],
-              value: a[1]
-            }));
-          } else {
-            throw new Error('Wrong array format');
-          }
-        }
-      }
-    } else if (_.isPlainObject(this.data)) {
-      temp = Object.keys(this.data).map(k => ({
-        key: k,
-        value: this.data[k]
-      }));
-    } else {
-      throw new Error('Wrong data format');
-    }
-    this.tableData = temp.map(o => {
-      if (_.isArray(o.value)) {
-        o.value = o.value.join(', ');
-      } else if (_.isObject(o.value)) {
-        return;
-      }
-      return o;
-    }).filter(o => o); // Filters out undefined
-  }
-
-  reloadData() {
-    // Forward event triggered by the 'cd-table' datatable.
-    this.fetchData.emit();
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html
deleted file mode 100644 (file)
index ba6adf5..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-<div class="dataTables_wrapper">
-  <div class="dataTables_header clearfix"
-       *ngIf="toolHeader">
-    <!-- actions -->
-    <div class="oadatatableactions">
-      <ng-content select="table-actions"></ng-content>
-    </div>
-    <!-- end actions -->
-
-    <!-- search -->
-    <div class="input-group">
-      <span class="input-group-addon">
-        <i class="glyphicon glyphicon-search"></i>
-      </span>
-      <input class="form-control"
-             type="text"
-             [(ngModel)]="search"
-             (keyup)='updateFilter($event)'>
-      <span class="input-group-btn">
-        <button type="button"
-                class="btn btn-default clear-input tc_clearInputBtn"
-                (click)="updateFilter()">
-          <i class="icon-prepend fa fa-remove"></i>
-        </button>
-      </span>
-    </div>
-    <!-- end search -->
-
-    <!-- pagination limit -->
-    <div class="input-group dataTables_paginate">
-      <input class="form-control"
-             type="number"
-             min="1"
-             max="9999"
-             [value]="limit"
-             (click)="setLimit($event)"
-             (keyup)="setLimit($event)"
-             (blur)="setLimit($event)">
-    </div>
-    <!-- end pagination limit-->
-
-    <!-- show hide columns -->
-    <div class="widget-toolbar">
-      <div dropdown
-           class="dropdown tc_menuitem tc_menuitem_cluster">
-        <a dropdownToggle
-           class="btn btn-sm btn-default dropdown-toggle tc_columnBtn"
-           data-toggle="dropdown">
-          <i class="fa fa-lg fa-table"></i>
-        </a>
-        <ul *dropdownMenu
-            class="dropdown-menu">
-          <li *ngFor="let column of columns">
-            <label>
-              <input type="checkbox"
-                     (change)="toggleColumn($event)"
-                     [name]="column.prop"
-                     [checked]="!column.isHidden">
-              <span>{{ column.name }}</span>
-            </label>
-          </li>
-        </ul>
-      </div>
-    </div>
-    <!-- end show hide columns -->
-
-    <!-- refresh button -->
-    <div class="widget-toolbar tc_refreshBtn">
-      <a (click)="refreshBtn()">
-        <i class="fa fa-lg fa-refresh"
-           [class.fa-spin]="updating || loadingIndicator"></i>
-      </a>
-    </div>
-    <!-- end refresh button -->
-  </div>
-  <ngx-datatable #table
-                 class="bootstrap oadatatable"
-                 [cssClasses]="paginationClasses"
-                 [selectionType]="selectionType"
-                 [selected]="selection.selected"
-                 (select)="onSelect()"
-                 [sorts]="sorts"
-                 [columns]="tableColumns"
-                 [columnMode]="columnMode"
-                 [rows]="rows"
-                 [rowClass]="getRowClass()"
-                 [headerHeight]="header ? 'auto' : 0"
-                 [footerHeight]="footer ? 'auto' : 0"
-                 [limit]="limit > 0 ? limit : undefined"
-                 [loadingIndicator]="loadingIndicator"
-                 [rowIdentity]="rowIdentity()"
-                 [rowHeight]="'auto'">
-  </ngx-datatable>
-</div>
-
-<!-- Table Details -->
-<ng-content select="[cdTableDetail]"></ng-content>
-
-<!-- cell templates that can be accessed from outside -->
-<ng-template #tableCellBoldTpl
-             let-value="value">
-  <strong>{{ value }}</strong>
-</ng-template>
-
-<ng-template #sparklineTpl
-             let-value="value">
-  <cd-sparkline [data]="value"></cd-sparkline>
-</ng-template>
-
-<ng-template #routerLinkTpl
-             let-row="row"
-             let-value="value">
-  <a [routerLink]="[row.cdLink]">{{ value }}</a>
-</ng-template>
-
-<ng-template #perSecondTpl
-             let-row="row"
-             let-value="value">
-  {{ value }} /s
-</ng-template>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.scss
deleted file mode 100644 (file)
index db9c4eb..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-@import '../../../../defaults';
-
-.dataTables_wrapper {
-  margin-bottom: 25px;
-  .separator {
-    height: 30px;
-    border-left: 1px solid rgba(0,0,0,.09);
-    padding-left: 5px;
-    margin-left: 5px;
-    display: inline-block;
-    vertical-align: middle;
-  }
-  .widget-toolbar {
-    display: inline-block;
-    float: right;
-    width: auto;
-    height: 30px;
-    line-height: 28px;
-    position: relative;
-    border-left: 1px solid rgba(0,0,0,.09);
-    cursor: pointer;
-    padding: 0 8px;
-    text-align: center;
-  }
-  .dropdown-menu {
-    white-space: nowrap;
-    & > li {
-      cursor: pointer;
-      & > label {
-        width: 100%;
-        margin-bottom: 0;
-        padding-left: 20px;
-        padding-right: 20px;
-        cursor: pointer;
-        &:hover {
-          background-color: #f5f5f5;
-        }
-        & > input {
-          cursor: pointer;
-        }
-      }
-    }
-  }
-  th.oadatatablecheckbox {
-    width: 16px;
-  }
-  .dataTables_length>input {
-    line-height: 25px;
-    text-align: right;
-  }
-}
-.dataTables_header {
-  background-color: #f6f6f6;
-  border: 1px solid #d1d1d1;
-  border-bottom: none;
-  padding: 5px;
-  position: relative;
-  .oadatatableactions {
-    display: inline-block;
-  }
-  .form-group {
-    padding-left: 8px;
-  }
-  .input-group {
-    float: right;
-    border-left: 1px solid rgba(0,0,0,.09);
-    padding-left: 8px;
-    width: 40%;
-    max-width: 350px;
-    .form-control {
-      height: 30px;
-    }
-    .clear-input {
-      height: 30px;
-      i {
-        vertical-align: text-top;
-      }
-    }
-  }
-  .input-group.dataTables_paginate {
-    width: 8%;
-    min-width: 85px;
-  }
-}
-
-::ng-deep .oadatatable {
-  border: $border-color;
-  margin-bottom: 0;
-  max-width: none!important;
-  .progress-linear {
-    display: block;
-    position: relative;
-    width: 100%;
-    height: 5px;
-    padding: 0;
-    margin: 0;
-    .container {
-      background-color: $oa-color-light-blue;
-      .bar {
-        left: 0;
-        height: 100%;
-        width: 100%;
-        position: absolute;
-        overflow: hidden;
-        background-color: $oa-color-light-blue;
-      }
-      .bar:before{
-        display: block;
-        position: absolute;
-        content: "";
-        left: -200px;
-        width: 200px;
-        height: 100%;
-        background-color: $oa-color-blue;
-        animation: progress-loading 3s linear infinite;
-      }
-    }
-  }
-  .datatable-header {
-    background-clip: padding-box;
-    background-color: #f9f9f9;
-    background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
-    background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
-    background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
-    background-repeat: repeat-x;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
-    .sort-asc, .sort-desc {
-      color: $oa-color-blue;
-    }
-    .datatable-header-cell{
-      @include table-cell;
-      text-align: left;
-      font-weight: bold;
-      .datatable-header-cell-label {
-        &:after {
-          font-family: FontAwesome;
-          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: none;
-      }
-    }
-  }
-  .datatable-body {
-    .empty-row {
-      background-color: $warning-background-color;
-      text-align: center;
-      font-weight: bold;
-      font-style: italic;
-      padding-top: 5px;
-      padding-bottom: 5px;
-    }
-    .datatable-body-row {
-      &.clickable:hover .datatable-row-group {
-        background-color: #eee;
-        transition-property: background;
-        transition-duration: .3s;
-        transition-timing-function: linear;
-      }
-      &.datatable-row-even {
-        background-color: #ffffff;
-      }
-      &.datatable-row-odd {
-        background-color: #f6f6f6;
-      }
-      &.active, &.active:hover {
-        background-color: $bg-color-light-blue;
-      }
-      .datatable-body-cell{
-        @include table-cell;
-        &:first-child {
-          border-left: none;
-        }
-        .datatable-body-cell-label {
-          display: block;
-        }
-      }
-    }
-  }
-  .datatable-footer {
-    .selected-count, .page-count {
-      font-style: italic;
-      padding-left: 5px;
-    }
-    .datatable-pager .pager {
-      margin-right: 5px;
-      .pages {
-        & > a, & > span {
-          display: inline-block;
-          padding: 5px 10px;
-          margin-bottom: 5px;
-          border: none;
-        }
-        a:hover {
-          background-color: $oa-color-light-blue;
-        }
-        &.active > a {
-          background-color: $bg-color-light-blue;
-        }
-      }
-    }
-  }
-}
-
-@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_v2/frontend/src/app/shared/datatable/table/table.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.spec.ts
deleted file mode 100644 (file)
index 60ec7d0..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { NgxDatatableModule, TableColumn } from '@swimlane/ngx-datatable';
-
-import { ComponentsModule } from '../../components/components.module';
-import { TableComponent } from './table.component';
-
-describe('TableComponent', () => {
-  let component: TableComponent;
-  let fixture: ComponentFixture<TableComponent>;
-  const columns: TableColumn[] = [];
-  const createFakeData = (n) => {
-    const data = [];
-    for (let i = 0; i < n; i++) {
-      data.push({
-        a: i,
-        b: i * i,
-        c: -(i % 10)
-      });
-    }
-    return data;
-  };
-
-  beforeEach(
-    async(() => {
-      TestBed.configureTestingModule({
-        declarations: [TableComponent],
-        imports: [NgxDatatableModule, FormsModule, ComponentsModule, RouterTestingModule]
-      }).compileComponents();
-    })
-  );
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(TableComponent);
-    component = fixture.componentInstance;
-  });
-
-  beforeEach(() => {
-    component.data = createFakeData(100);
-    component.useData();
-    component.columns = [
-      {prop: 'a'},
-      {prop: 'b'},
-      {prop: 'c'}
-    ];
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-
-  it('should have rows', () => {
-    expect(component.data.length).toBe(100);
-    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.limit).toBe(1);
-    expect(component.limit).toEqual(jasmine.any(Number));
-    e.target.value = '-20';
-    component.setLimit(e);
-    expect(component.limit).toBe(1);
-  });
-
-  it('should search for 13', () => {
-    component.search = '13';
-    expect(component.rows.length).toBe(100);
-    component.updateFilter(true);
-    expect(component.rows[0].a).toBe(13);
-    expect(component.rows[1].b).toBe(1369);
-    expect(component.rows[2].b).toBe(3136);
-    expect(component.rows.length).toBe(3);
-  });
-
-  it('should restore full table after search', () => {
-    component.search = '13';
-    expect(component.rows.length).toBe(100);
-    component.updateFilter(true);
-    expect(component.rows.length).toBe(3);
-    component.updateFilter();
-    expect(component.rows.length).toBe(100);
-  });
-
-  describe('after ngInit', () => {
-    const toggleColumn = (prop, checked) => {
-      component.toggleColumn({
-        target: {
-          name: prop,
-          checked: checked
-        }
-      });
-    };
-
-    beforeEach(() => {
-      component.ngOnInit();
-      component.table.sorts = component.sorts;
-    });
-
-    it('should have updated the column definitions', () => {
-      expect(component.columns[0].flexGrow).toBe(1);
-      expect(component.columns[1].flexGrow).toBe(2);
-      expect(component.columns[2].flexGrow).toBe(2);
-      expect(component.columns[2].resizeable).toBe(false);
-    });
-
-    it('should have table columns', () => {
-      expect(component.tableColumns.length).toBe(3);
-      expect(component.tableColumns).toEqual(component.columns);
-    });
-
-    it('should have a unique identifier which is search for', () => {
-      expect(component.identifier).toBe('a');
-      expect(component.sorts[0].prop).toBe('a');
-      expect(component.sorts).toEqual(component.createSortingDefinition('a'));
-    });
-
-    it('should remove column "a"', () => {
-      toggleColumn('a', false);
-      expect(component.table.sorts[0].prop).toBe('b');
-      expect(component.tableColumns.length).toBe(2);
-    });
-
-    it('should not be able to remove all columns', () => {
-      toggleColumn('a', false);
-      toggleColumn('b', false);
-      toggleColumn('c', false);
-      expect(component.table.sorts[0].prop).toBe('c');
-      expect(component.tableColumns.length).toBe(1);
-    });
-
-    it('should enable column "a" again', () => {
-      toggleColumn('a', false);
-      toggleColumn('a', true);
-      expect(component.table.sorts[0].prop).toBe('b');
-      expect(component.tableColumns.length).toBe(3);
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts
deleted file mode 100644 (file)
index 9f04e91..0000000
+++ /dev/null
@@ -1,282 +0,0 @@
-import {
-  AfterContentChecked,
-  Component,
-  EventEmitter,
-  Input,
-  OnChanges,
-  OnDestroy,
-  OnInit,
-  Output,
-  TemplateRef,
-  Type,
-  ViewChild
-} from '@angular/core';
-import {
-  DatatableComponent,
-  SortDirection,
-  SortPropDir,
-  TableColumnProp
-} from '@swimlane/ngx-datatable';
-
-import * as _ from 'lodash';
-import 'rxjs/add/observable/timer';
-import { Observable } from 'rxjs/Observable';
-
-import { CdTableColumn } from '../../models/cd-table-column';
-import { CdTableSelection } from '../../models/cd-table-selection';
-
-@Component({
-  selector: 'cd-table',
-  templateUrl: './table.component.html',
-  styleUrls: ['./table.component.scss']
-})
-export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy {
-  @ViewChild(DatatableComponent) table: DatatableComponent;
-  @ViewChild('tableCellBoldTpl') tableCellBoldTpl: TemplateRef<any>;
-  @ViewChild('sparklineTpl') sparklineTpl: TemplateRef<any>;
-  @ViewChild('routerLinkTpl') routerLinkTpl: TemplateRef<any>;
-  @ViewChild('perSecondTpl') perSecondTpl: 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 the tool header, including reload button, pagination and search fields?
-  @Input() toolHeader ?= 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 ?= 10;
-
-  /**
-   * 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.
-   */
-  @Input() autoReload: any = 5000;
-
-  // Which row property is unique for a row
-  @Input() identifier = 'id';
-  // Allows other components to specify which type of selection they want,
-  // e.g. 'single' or 'multi'.
-  @Input() selectionType: string = undefined;
-
-  /**
-   * 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();
-
-  /**
-   * 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();
-
-  /**
-   * Use this variable to access the selected row(s).
-   */
-  selection = new CdTableSelection();
-
-  tableColumns: CdTableColumn[];
-  cellTemplates: {
-    [key: string]: TemplateRef<any>
-  } = {};
-  search = '';
-  rows = [];
-  loadingIndicator = true;
-  paginationClasses = {
-    pagerLeftArrow: 'i fa fa-angle-double-left',
-    pagerRightArrow: 'i fa fa-angle-double-right',
-    pagerPrevious: 'i fa fa-angle-left',
-    pagerNext: 'i fa fa-angle-right'
-  };
-  private subscriber;
-  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;
-
-  constructor() {}
-
-  ngOnInit() {
-    this._addTemplates();
-    if (!this.sorts) {
-      this.identifier = this.columns.some(c => c.prop === this.identifier) ?
-        this.identifier :
-        this.columns[0].prop + '';
-      this.sorts = this.createSortingDefinition(this.identifier);
-    }
-    this.columns.map(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;
-      }
-      return c;
-    });
-    this.tableColumns = this.columns.filter(c => !c.isHidden);
-    if (this.autoReload) { // Also if nothing is bound to fetchData nothing will be triggered
-      // Force showing the loading indicator because it has been set to False in
-      // useData() when this method was triggered by ngOnChanges().
-      this.loadingIndicator = true;
-      this.subscriber = Observable.timer(0, this.autoReload).subscribe(x => {
-        return this.reloadData();
-      });
-    }
-  }
-
-  ngOnDestroy() {
-    if (this.subscriber) {
-      this.subscriber.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;
-      this.table.recalculate();
-    }
-  }
-
-  _addTemplates() {
-    this.cellTemplates.bold = this.tableCellBoldTpl;
-    this.cellTemplates.sparkline = this.sparklineTpl;
-    this.cellTemplates.routerLink = this.routerLinkTpl;
-    this.cellTemplates.perSecond = this.perSecondTpl;
-  }
-
-  ngOnChanges(changes) {
-    this.useData();
-  }
-
-  setLimit(e) {
-    const value = parseInt(e.target.value, 10);
-    if (value > 0) {
-      this.limit = value;
-    }
-  }
-
-  reloadData() {
-    if (!this.updating) {
-      this.fetchData.emit();
-      this.updating = true;
-    }
-  }
-
-  refreshBtn () {
-    this.loadingIndicator = true;
-    this.reloadData();
-  }
-
-  rowIdentity() {
-    return (row) => {
-      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.rows = [...this.data];
-    if (this.search.length > 0) {
-      this.updateFilter(true);
-    }
-    this.loadingIndicator = false;
-    this.updating = false;
-  }
-
-  onSelect() {
-    this.selection.update();
-    this.updateSelection.emit(_.clone(this.selection));
-  }
-
-  toggleColumn($event: any) {
-    const prop: TableColumnProp = $event.target.name;
-    const hide = !$event.target.checked;
-    if (hide && this.tableColumns.length === 1) {
-      $event.target.checked = true;
-      return;
-    }
-    _.find(this.columns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
-    this.updateColumns();
-  }
-
-  updateColumns () {
-    this.tableColumns = this.columns.filter(c => !c.isHidden);
-    const sortProp = this.table.sorts[0].prop;
-    if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) {
-      this.table.onColumnSort({sorts: this.createSortingDefinition(this.tableColumns[0].prop)});
-    }
-    this.table.recalculate();
-  }
-
-  createSortingDefinition (prop: TableColumnProp): SortPropDir[] {
-    return [
-      {
-        prop: prop,
-        dir: SortDirection.asc
-      }
-    ];
-  }
-
-  updateFilter(event?) {
-    if (!event) {
-      this.search = '';
-    }
-    const val = this.search.toLowerCase();
-    const columns = this.columns;
-    // update the rows
-    this.rows = this.data.filter((d) => {
-      return (
-        columns.filter(c => {
-          return (
-            (_.isString(d[c.prop]) || _.isNumber(d[c.prop])) &&
-            (d[c.prop] + '').toLowerCase().indexOf(val) !== -1
-          );
-        }).length > 0
-      );
-    });
-    // Whenever the filter changes, always go back to the first page
-    this.table.offset = 0;
-  }
-
-  getRowClass() {
-    // Return the function used to populate a row's CSS classes.
-    return () => {
-      return {
-        clickable: !_.isUndefined(this.selectionType)
-      };
-    };
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.spec.ts
deleted file mode 100644 (file)
index 1fc8f9c..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-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_v2/frontend/src/app/shared/directives/password-button.directive.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.ts
deleted file mode 100644 (file)
index b375ba2..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Directive, ElementRef, HostListener, Input, OnInit, Renderer2 } from '@angular/core';
-
-@Directive({
-  selector: '[cdPasswordButton]'
-})
-export class PasswordButtonDirective implements OnInit {
-  private inputElement: any;
-  private iElement: any;
-
-  @Input('cdPasswordButton') private cdPasswordButton: string;
-
-  constructor(private el: ElementRef, private renderer: Renderer2) { }
-
-  ngOnInit() {
-    this.inputElement = document.getElementById(this.cdPasswordButton);
-    this.iElement = this.renderer.createElement('i');
-    this.renderer.addClass(this.iElement, 'icon-prepend');
-    this.renderer.addClass(this.iElement, 'fa');
-    this.renderer.appendChild(this.el.nativeElement, this.iElement);
-    this.update();
-  }
-
-  private update() {
-    if (this.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() {
-    // Modify the type of the input field.
-    this.inputElement.type = (this.inputElement.type === 'password') ? 'text' : 'password';
-    // Update the button icon/tooltip.
-    this.update();
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts
deleted file mode 100644 (file)
index 7c1c216..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-export enum CellTemplate {
-  bold = 'bold',
-  sparkline = 'sparkline',
-  perSecond = 'perSecond',
-  routerLink = 'routerLink'
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/view-cache-status.enum.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/view-cache-status.enum.ts
deleted file mode 100644 (file)
index 169059c..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-export enum ViewCacheStatus {
-  ValueOk = 0,
-  ValueStale = 1,
-  ValueNone = 2,
-  ValueException = 3
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-column.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-column.ts
deleted file mode 100644 (file)
index bf45c48..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-import { TableColumn } from '@swimlane/ngx-datatable';
-import { CellTemplate } from '../enum/cell-template.enum';
-
-export interface CdTableColumn extends TableColumn {
-  cellTransformation?: CellTemplate;
-  isHidden?: boolean;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-selection.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-selection.ts
deleted file mode 100644 (file)
index 9732abc..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-export class CdTableSelection {
-  selected: any[] = [];
-  hasMultiSelection: boolean;
-  hasSingleSelection: boolean;
-  hasSelection: boolean;
-
-  constructor() {
-    this.update();
-  }
-
-  /**
-   * Recalculate the variables based on the current number
-   * of selected rows.
-   */
-  update() {
-    this.hasSelection = this.selected.length > 0;
-    this.hasSingleSelection = this.selected.length === 1;
-    this.hasMultiSelection = this.selected.length > 1;
-  }
-
-  /**
-   * Get the first selected row.
-   * @return {any | null}
-   */
-  first() {
-    return this.hasSelection ? this.selected[0] : null;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/chart-tooltip.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/chart-tooltip.ts
deleted file mode 100644 (file)
index 56962f3..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-import { ElementRef } from '@angular/core';
-
-import * as _ from 'lodash';
-
-export class ChartTooltip {
-  tooltipEl: any;
-  chartEl: any;
-  getStyleLeft: Function;
-  getStyleTop: Function;
-  customColors = {
-    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) {
-    // 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 => {
-        return bodyItem.lines;
-      });
-
-      let innerHtml = '<thead>';
-
-      titleLines.forEach(title => {
-        innerHtml += '<tr><th>' + this.getTitle(title) + '</th></tr>';
-      });
-      innerHtml += '</thead><tbody>';
-
-      bodyLines.forEach((body, i) => {
-        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) {
-    return body;
-  }
-
-  getTitle(title) {
-    return title;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.ts
deleted file mode 100644 (file)
index b33c366..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-export class Credentials {
-  username: string;
-  password: string;
-  stay_signed_in = false;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts
deleted file mode 100644 (file)
index bfe10c2..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { CephShortVersionPipe } from './ceph-short-version.pipe';
-
-describe('CephShortVersionPipe', () => {
-  it('create an instance', () => {
-    const pipe = new CephShortVersionPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts
deleted file mode 100644 (file)
index 9599112..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
-  name: 'cephShortVersion'
-})
-export class CephShortVersionPipe implements PipeTransform {
-  transform(value: any, args?: 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_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts
deleted file mode 100644 (file)
index 2424ebc..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-import { FormatterService } from '../services/formatter.service';
-import { DimlessBinaryPipe } from './dimless-binary.pipe';
-
-describe('DimlessBinaryPipe', () => {
-  it('create an instance', () => {
-    const formatterService = new FormatterService();
-    const pipe = new DimlessBinaryPipe(formatterService);
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.ts
deleted file mode 100644 (file)
index 92f0008..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-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, args?: any): any {
-    return this.formatter.format_number(value, 1024, [
-      'B',
-      'KiB',
-      'MiB',
-      'GiB',
-      'TiB',
-      'PiB'
-    ]);
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.spec.ts
deleted file mode 100644 (file)
index 4bbfdd8..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-import { FormatterService } from '../services/formatter.service';
-import { DimlessPipe } from './dimless.pipe';
-
-describe('DimlessPipe', () => {
-  it('create an instance', () => {
-    const formatterService = new FormatterService();
-    const pipe = new DimlessPipe(formatterService);
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.ts
deleted file mode 100644 (file)
index 5e02846..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-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, args?: any): any {
-    return this.formatter.format_number(value, 1000, [
-      ' ',
-      'k',
-      'M',
-      'G',
-      'T',
-      'P'
-    ]);
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.spec.ts
deleted file mode 100644 (file)
index 1427de3..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { FilterPipe } from './filter.pipe';
-
-describe('FilterPipe', () => {
-  it('create an instance', () => {
-    const pipe = new FilterPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.ts
deleted file mode 100644 (file)
index 21115a7..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
-  name: 'filter'
-})
-export class FilterPipe implements PipeTransform {
-  transform(value: any, args?: any): any {
-    return value.filter(row => {
-      let result = true;
-
-      args.forEach(filter => {
-        if (!filter.value) {
-          return;
-        }
-
-        result = result && filter.applyFilter(row, filter.value);
-        if (!result) {
-          return result;
-        }
-      });
-
-      return result;
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.spec.ts
deleted file mode 100644 (file)
index e0e44e0..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { HealthColorPipe } from './health-color.pipe';
-
-describe('HealthColorPipe', () => {
-  it('create an instance', () => {
-    const pipe = new HealthColorPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.ts
deleted file mode 100644 (file)
index 9d82475..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
-  name: 'healthColor'
-})
-export class HealthColorPipe implements PipeTransform {
-  transform(value: any, args?: any): any {
-    if (value === 'HEALTH_OK') {
-      return { color: '#00bb00' };
-    } else if (value === 'HEALTH_WARN') {
-      return { color: '#ffa500' };
-    } else if (value === 'HEALTH_ERR') {
-      return { color: '#ff0000' };
-    } else {
-      return null;
-    }
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.spec.ts
deleted file mode 100644 (file)
index 768f12a..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { ListPipe } from './list.pipe';
-
-describe('ListPipe', () => {
-  it('create an instance', () => {
-    const pipe = new ListPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.ts
deleted file mode 100644 (file)
index 1e37919..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
-  name: 'list'
-})
-export class ListPipe implements PipeTransform {
-  transform(value: any, args?: any): any {
-    return value.join(', ');
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/pipes.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/pipes.module.ts
deleted file mode 100644 (file)
index 51dc736..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { CephShortVersionPipe } from './ceph-short-version.pipe';
-import { DimlessBinaryPipe } from './dimless-binary.pipe';
-import { DimlessPipe } from './dimless.pipe';
-import { FilterPipe } from './filter.pipe';
-import { HealthColorPipe } from './health-color.pipe';
-import { ListPipe } from './list.pipe';
-import { RelativeDatePipe } from './relative-date.pipe';
-
-@NgModule({
-  imports: [CommonModule],
-  declarations: [
-    DimlessBinaryPipe,
-    HealthColorPipe,
-    DimlessPipe,
-    CephShortVersionPipe,
-    RelativeDatePipe,
-    ListPipe,
-    FilterPipe
-  ],
-  exports: [
-    DimlessBinaryPipe,
-    HealthColorPipe,
-    DimlessPipe,
-    CephShortVersionPipe,
-    RelativeDatePipe,
-    ListPipe,
-    FilterPipe
-  ],
-  providers: [
-    CephShortVersionPipe,
-    DimlessBinaryPipe,
-    DimlessPipe,
-    RelativeDatePipe,
-    ListPipe
-  ]
-})
-export class PipesModule {}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts
deleted file mode 100644 (file)
index 1295b0d..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { RelativeDatePipe } from './relative-date.pipe';
-
-describe('RelativeDatePipe', () => {
-  it('create an instance', () => {
-    const pipe = new RelativeDatePipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.ts
deleted file mode 100644 (file)
index 6bfa395..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-
-import * as moment from 'moment';
-
-@Pipe({
-  name: 'relativeDate'
-})
-export class RelativeDatePipe implements PipeTransform {
-  constructor() {}
-
-  transform(value: any, args?: any): any {
-    if (!value) {
-      return 'unknown';
-    }
-    return moment(value * 1000).fromNow();
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts
deleted file mode 100644 (file)
index a3ec803..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
-
-import { AuthStorageService } from './auth-storage.service';
-
-@Injectable()
-export class AuthGuardService implements CanActivate {
-
-  constructor(private router: Router, private authStorageService: AuthStorageService) {
-  }
-
-  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
-    if (this.authStorageService.isLoggedIn()) {
-      return true;
-    }
-    this.router.navigate(['/login']);
-    return false;
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts
deleted file mode 100644 (file)
index f09250d..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-import {
-  HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest,
-  HttpResponse
-} from '@angular/common/http';
-import { Injectable } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { ToastsManager } from 'ng2-toastr';
-import 'rxjs/add/operator/do';
-import { Observable } from 'rxjs/Observable';
-
-import { AuthStorageService } from './auth-storage.service';
-
-@Injectable()
-export class AuthInterceptorService implements HttpInterceptor {
-
-  constructor(private router: Router,
-              private authStorageService: AuthStorageService,
-              public toastr: ToastsManager) {
-  }
-
-  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
-    return next.handle(request).do((event: HttpEvent<any>) => {
-      if (event instanceof HttpResponse) {
-        // do nothing
-      }
-    }, (err: any) => {
-      if (err instanceof HttpErrorResponse) {
-        if (err.status === 404) {
-          this.router.navigate(['/404']);
-          return;
-        }
-
-        this.toastr.error(err.error.detail || '', `${err.status} - ${err.statusText}`);
-        if (err.status === 401) {
-          this.authStorageService.remove();
-          this.router.navigate(['/login']);
-        }
-      }
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts
deleted file mode 100644 (file)
index cd6dbbe..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class AuthStorageService {
-
-  constructor() {
-  }
-
-  set(username: string) {
-    localStorage.setItem('dashboard_username', username);
-  }
-
-  remove() {
-    localStorage.removeItem('dashboard_username');
-  }
-
-  isLoggedIn() {
-    return localStorage.getItem('dashboard_username') !== null;
-  }
-
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts
deleted file mode 100644 (file)
index 88a7136..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-import { Credentials } from '../models/credentials';
-import { AuthStorageService } from './auth-storage.service';
-
-@Injectable()
-export class AuthService {
-
-  constructor(private authStorageService: AuthStorageService,
-              private http: HttpClient) {
-  }
-
-  login(credentials: Credentials) {
-    return this.http.post('api/auth', credentials).toPromise().then((resp: Credentials) => {
-      this.authStorageService.set(resp.username);
-    });
-  }
-
-  logout() {
-    return this.http.delete('api/auth').toPromise().then(() => {
-      this.authStorageService.remove();
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.spec.ts
deleted file mode 100644 (file)
index dcb5a9e..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { ConfigurationService } from './configuration.service';
-
-describe('ConfigurationService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [ConfigurationService],
-      imports: [HttpClientTestingModule, HttpClientModule]
-    });
-  });
-
-  it(
-    'should be created',
-    inject([ConfigurationService], (service: ConfigurationService) => {
-      expect(service).toBeTruthy();
-    })
-  );
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.ts
deleted file mode 100644 (file)
index 41ac7bb..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class ConfigurationService {
-  constructor(private http: HttpClient) {}
-
-  getConfigData() {
-    return this.http.get('api/cluster_conf/');
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.spec.ts
deleted file mode 100644 (file)
index f3a99b5..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-import { inject, TestBed } from '@angular/core/testing';
-
-import { FormatterService } from './formatter.service';
-
-describe('FormatterService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [FormatterService]
-    });
-  });
-
-  it('should be created', inject([FormatterService], (service: FormatterService) => {
-    expect(service).toBeTruthy();
-  }));
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.ts
deleted file mode 100644 (file)
index 3986408..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class FormatterService {
-  constructor() {}
-
-  truncate(n, maxWidth) {
-    const stringized = n.toString();
-    const parts = stringized.split('.');
-    if (parts.length === 1) {
-      // Just an int
-      return stringized;
-    } else {
-      const fractionalDigits = maxWidth - parts[0].length - 1;
-      if (fractionalDigits <= 0) {
-        // No width available for the fractional part, drop
-        // it and the decimal point
-        return parts[0];
-      } else {
-        return stringized.substring(0, maxWidth);
-      }
-    }
-  }
-
-  format_number(n, divisor, units) {
-    const width = 4;
-    let unit = 0;
-
-    if (n == null) {
-      // People shouldn't really be passing null, but let's
-      // do something sensible instead of barfing.
-      return '-';
-    }
-
-    while (Math.floor(n / divisor ** unit).toString().length > width - 1) {
-      unit = unit + 1;
-    }
-
-    let truncatedFloat;
-    if (unit > 0) {
-      truncatedFloat = this.truncate(
-        (n / Math.pow(divisor, unit)).toString(),
-        width
-      );
-    } else {
-      truncatedFloat = this.truncate(n, width);
-    }
-
-    return truncatedFloat === '' ? '-' : (truncatedFloat + units[unit]);
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/host.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/host.service.ts
deleted file mode 100644 (file)
index 3d28cd7..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class HostService {
-
-  constructor(private http: HttpClient) {
-  }
-
-  list() {
-    return this.http.get('api/host').toPromise().then((resp: any) => {
-      return resp;
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/pool.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/pool.service.ts
deleted file mode 100644 (file)
index 8ac6de9..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class PoolService {
-
-  constructor(private http: HttpClient) {
-  }
-
-  rbdPoolImages(pool) {
-    return this.http.get(`api/rbd/${pool}`).toPromise().then((resp: any) => {
-      return resp;
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts
deleted file mode 100644 (file)
index 0f59831..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { RbdMirroringService } from './rbd-mirroring.service';
-
-describe('RbdMirroringService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [RbdMirroringService],
-      imports: [HttpClientTestingModule, HttpClientModule]
-    });
-  });
-
-  it('should be created', inject([RbdMirroringService], (service: RbdMirroringService) => {
-    expect(service).toBeTruthy();
-  }));
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.ts
deleted file mode 100644 (file)
index b840b30..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class RbdMirroringService {
-  constructor(private http: HttpClient) {}
-
-  get() {
-    return this.http.get('api/rbdmirror');
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/services.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/services.module.ts
deleted file mode 100644 (file)
index 04d4a3c..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { ConfigurationService } from './configuration.service';
-import { FormatterService } from './formatter.service';
-import { RbdMirroringService } from './rbd-mirroring.service';
-import { SummaryService } from './summary.service';
-import { TcmuIscsiService } from './tcmu-iscsi.service';
-
-@NgModule({
-  imports: [CommonModule],
-  declarations: [],
-  providers: [
-    FormatterService,
-    SummaryService,
-    TcmuIscsiService,
-    ConfigurationService,
-    RbdMirroringService
-  ]
-})
-export class ServicesModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.spec.ts
deleted file mode 100644 (file)
index 23af983..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { SharedModule } from '../shared.module';
-import { SummaryService } from './summary.service';
-
-describe('SummaryService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [SummaryService],
-      imports: [HttpClientTestingModule, SharedModule]
-    });
-  });
-
-  it(
-    'should be created',
-    inject([SummaryService], (service: SummaryService) => {
-      expect(service).toBeTruthy();
-    })
-  );
-});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.ts
deleted file mode 100644 (file)
index 9556930..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-import { Subject } from 'rxjs/Subject';
-
-import { AuthStorageService } from './auth-storage.service';
-
-@Injectable()
-export class SummaryService {
-  // Observable sources
-  private summaryDataSource = new Subject();
-
-  // Observable streams
-  summaryData$ = this.summaryDataSource.asObservable();
-
-  constructor(private http: HttpClient, private authStorageService: AuthStorageService) {
-    this.refresh();
-  }
-
-  refresh() {
-    if (this.authStorageService.isLoggedIn()) {
-      this.http.get('api/summary').subscribe(data => {
-        this.summaryDataSource.next(data);
-      });
-    }
-
-    setTimeout(() => {
-      this.refresh();
-    }, 5000);
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/tcmu-iscsi.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/tcmu-iscsi.service.ts
deleted file mode 100644 (file)
index 2f36bb8..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class TcmuIscsiService {
-
-  constructor(private http: HttpClient) {
-  }
-
-  tcmuiscsi() {
-    return this.http.get('api/tcmuiscsi').toPromise().then((resp: any) => {
-      return resp;
-    });
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts
deleted file mode 100644 (file)
index 7651338..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { ComponentsModule } from './components/components.module';
-import { DataTableModule } from './datatable/datatable.module';
-import { PasswordButtonDirective } from './directives/password-button.directive';
-import { PipesModule } from './pipes/pipes.module';
-import { AuthGuardService } from './services/auth-guard.service';
-import { AuthStorageService } from './services/auth-storage.service';
-import { AuthService } from './services/auth.service';
-import { FormatterService } from './services/formatter.service';
-import { HostService } from './services/host.service';
-import { PoolService } from './services/pool.service';
-import { ServicesModule } from './services/services.module';
-
-@NgModule({
-  imports: [
-    CommonModule,
-    PipesModule,
-    ComponentsModule,
-    ServicesModule,
-    DataTableModule
-  ],
-  declarations: [
-    PasswordButtonDirective
-  ],
-  exports: [
-    ComponentsModule,
-    PipesModule,
-    ServicesModule,
-    PasswordButtonDirective,
-    DataTableModule
-  ],
-  providers: [
-    AuthService,
-    AuthStorageService,
-    AuthGuardService,
-    PoolService,
-    FormatterService,
-    HostService
-  ],
-})
-export class SharedModule {}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/.gitkeep b/src/pybind/mgr/dashboard_v2/frontend/src/assets/.gitkeep
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/1280px-Mimic_Octopus2.jpg b/src/pybind/mgr/dashboard_v2/frontend/src/assets/1280px-Mimic_Octopus2.jpg
deleted file mode 100644 (file)
index f8cf2a8..0000000
Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/1280px-Mimic_Octopus2.jpg and /dev/null differ
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png
deleted file mode 100644 (file)
index 26d602b..0000000
Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png and /dev/null differ
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png
deleted file mode 100644 (file)
index 0f07b83..0000000
Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png and /dev/null differ
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/loading.gif b/src/pybind/mgr/dashboard_v2/frontend/src/assets/loading.gif
deleted file mode 100755 (executable)
index 8fb88de..0000000
Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/loading.gif and /dev/null differ
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/logo-mini.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/logo-mini.png
deleted file mode 100644 (file)
index b3446a8..0000000
Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/logo-mini.png and /dev/null differ
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/notification-icons.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/notification-icons.png
deleted file mode 100644 (file)
index d609a7c..0000000
Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/notification-icons.png and /dev/null differ
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/defaults.scss b/src/pybind/mgr/dashboard_v2/frontend/src/defaults.scss
deleted file mode 100644 (file)
index 8e25b71..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-$warning-background-color: #fff3cd;
-$oa-color-blue: #288cea;
-$oa-color-light-blue: #afd9ee;
-$bg-color-light-blue: #d9edf7;
-$border-color: 1px solid #d1d1d1;
-@mixin table-cell {
-  padding: 5px;
-  border: none;
-  border-left: $border-color;
-  border-bottom: $border-color;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.prod.ts b/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.prod.ts
deleted file mode 100644 (file)
index 3612073..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-export const environment = {
-  production: true
-};
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.ts b/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.ts
deleted file mode 100644 (file)
index b7f639a..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-// 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 = {
-  production: false
-};
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/favicon.ico b/src/pybind/mgr/dashboard_v2/frontend/src/favicon.ico
deleted file mode 100644 (file)
index 90e538b..0000000
Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/favicon.ico and /dev/null differ
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/index.html b/src/pybind/mgr/dashboard_v2/frontend/src/index.html
deleted file mode 100644 (file)
index 05a8f70..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-  <meta charset="utf-8">
-  <title>Ceph</title>
-
-  <script>
-    document.write('<base href="' + document.location+ '" />');
-  </script>
-
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <link rel="icon" type="image/x-icon" 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_v2/frontend/src/main.ts b/src/pybind/mgr/dashboard_v2/frontend/src/main.ts
deleted file mode 100644 (file)
index 91ec6da..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-import { enableProdMode } from '@angular/core';
-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)
-  .catch(err => console.log(err));
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss b/src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss
deleted file mode 100755 (executable)
index aa819a2..0000000
+++ /dev/null
@@ -1,1184 +0,0 @@
-/*
-  Basics
-  Branding
-  Breadcrumb
-  Buttons
-  Dropdown
-  Grid
-  Modal
-  Navbar
-  Navs
-  Notifications
-  Pagination
-  Panel
-  Table
-  Typo
-
-  Login
-  Statistics
-
-  ApiRecorder
-  Caret
-  Datatables
-  Feedback
-  FlexElement
-  Grafana
-  Graph
-  Progressbar
-  TagForm
-  Trees
-  CSS Fix
-*/
-
-@import 'defaults';
-
-$fa-font-path: "../node_modules/font-awesome/fonts";
-@import "../node_modules/font-awesome/scss/font-awesome";
-
-/* Basics */
-html {
-  background-color: #ffffff;
-}
-html,
-body {
-  width: 100%;
-  height: 100%;
-  font-size: 12px;
-}
-optgroup {
-  font-weight: bold;
-  font-style: italic;
-}
-option {
-  font-weight: normal;
-  font-style: normal;
-}
-.full-height {
-  height: 100%;
-}
-.vertical-align {
-  display: flex;
-  align-items: center;
-}
-.loading {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-}
-.bg-color-darken {
-  background-color: #404040!important;
-}
-.bg-color-greenLight {
-  background-color: #71843f!important;
-}
-.bg-color-red {
-  background-color: #a90329!important;
-}
-.no-margin {
-  margin: 0;
-}
-.margin-left-md {
-  margin-left: 15px
-}
-.margin-right-md {
-  margin-right: 15px
-}
-.margin-right-sm {
-  margin-right: 10px
-}
-.margin-bottom-md {
-  margin-bottom: 15px
-}
-.no-padding {
-  padding: 0;
-}
-.small-padding {
-  padding: 5px;
-}
-.no-border {
-  border: 0px;
-  box-shadow: 0px 0px 0px !important;
-}
-.no-wrap {
-  white-space: nowrap;
-}
-.strikethrough {
-  text-decoration: line-through;
-}
-.italic {
-  font-style: italic;
-}
-.bold {
-  font-weight: bold;
-}
-.text-right {
-  text-align: right;
-}
-.text-monospace {
-  font-family: monospace;
-}
-
-/* Branding */
-.navbar-openattic .navbar-brand,
-.navbar-openattic .navbar-brand:hover{
-  color: #ececec;
-  height: auto;
-  margin: 15px 0 15px 20px;
-  padding: 0;
-  -webkit-align-self: flex-start;
-  align-self: flex-start;
-}
-.navbar-openattic .navbar-brand>img {
-  height: 25px;
-}
-
-/* Breadcrumb */
-.breadcrumb {
-  padding: 8px 0;
-  background-color: transparent;
-  border-radius: 0;
-}
-.breadcrumb>li+li:before {
-  padding: 0 5px 0 7px;
-  color: #474544;
-  font-family: "FontAwesome";
-  content: "\f101";
-}
-.breadcrumb>li>span {
-  color: #474544;
-}
-
-/* Icons */
-.icon-warning {
-  color: #f0ad4e;
-}
-.icon-danger {
-  color: #c9302c;
-}
-
-/* Buttons */
-.btn-openattic {
-  color: #ececec;
-  background-color: $oa-color-blue;
-  border-color: $oa-color-blue;
-}
-.btn-primary {
-  color: #ececec;
-  background-color: $oa-color-blue;
-  border-color: #2172bf;
-}
-.btn-primary:hover,
-.btn-primary:focus,
-.btn-primary:active,
-.btn-primary.active,
-.open .dropdown-toggle.btn-primary {
-  color: #ececec;
-  background-color: #2582D9;
-  border-color: #2172bf;
-}
-.btn-primary:active,
-.btn-primary.active,
-.open .dropdown-toggle.btn-primary {
-  background-image: none;
-}
-.btn-primary.disabled,
-.btn-primary[disabled],
-fieldset[disabled] .btn-primary,
-.btn-primary.disabled:hover,
-.btn-primary[disabled]:hover,
-fieldset[disabled] .btn-primary:hover,
-.btn-primary.disabled:focus,
-.btn-primary[disabled]:focus,
-fieldset[disabled] .btn-primary:focus,
-.btn-primary.disabled:active,
-.btn-primary[disabled]:active,
-fieldset[disabled] .btn-primary:active,
-.btn-primary.disabled.active,
-.btn-primary[disabled].active,
-fieldset[disabled] .btn-primary.active {
-  background-color: $oa-color-blue;
-  border-color: #2172bf;
-}
-.btn-primary .badge {
-  color: $oa-color-blue;
-  background-color: #ececec;
-}
-.btn-primary .caret {
-  color: #ececec;
-}
-.btn-group>.btn>i.fa,
-button.btn.btn-label>i.fa {
-  /** Add space between icon and text */
-  padding-right: 5px;
-}
-
-/* Dropdown */
-.dropdown-menu {
-  min-width: 50px;
-}
-.dropdown-menu>li>a {
-  color: #474544;
-  cursor: pointer;
-}
-.dropdown-menu>li>a>i.fa {
-  /** Add space between icon and text */
-  padding-right: 5px;
-}
-.dropdown-menu>.active>a {
-  color: #ececec;
-  background-color: $oa-color-blue;
-}
-.dataTables_wrapper .dropdown-menu>li.divider {
-  cursor: auto;
-}
-
-/* Grid */
-.container,
-.container-fluid {
-  padding-left: 30px;
-  padding-right: 30px;
-}
-.row {
-  margin-left: -30px;
-  margin-right: -30px;
-}
-.col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9,
-.col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9,
-.col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9,
-.col-xs-1, .col-xs-10, .col-xs-11, .col-xs-12, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9 {
-  padding-left: 30px;
-  padding-right: 30px;
-}
-
-/* Modal */
-.modal-dialog {
-  margin: 30px auto !important;
-}
-.modal .modal-content .openattic-modal-header,
-.modal .modal-content .openattic-modal-content,
-.modal .modal-content .openattic-modal-footer {
-  padding: 10px 20px;
-}
-.modal .modal-content .openattic-modal-header {
-  border-bottom: 1px solid #cccccc;
-  border-radius: 5px 5px 0 0;
-  background-color: #f5f5f5;
-}
-.modal .modal-content .openattic-modal-content {
-  padding: 20px 20px 10px 20px;
-  overflow-x: auto;
-  max-height: 70vh;
-}
-.modal .modal-content .openattic-modal-content p {
-  margin-bottom: 10px;
-}
-.modal .modal-content .openattic-modal-content legend {
-  font-size: 1.833em;
-}
-.modal .modal-content .openattic-modal-footer {
-  border-top: 1px solid #cccccc;
-  border-radius: 0 0 5px 5px;
-  background-color: #f5f5f5;
-}
-.modal .modal-content .openattic-modal-header span {
-  display: block;
-  font-size: 16px; /* Same as .panel-title */
-}
-
-/* Modal Table (Task Queue) */
-table.task-queue-table thead {
-  display: flex;
-  flex-flow: row;
-}
-table.task-queue-table thead tr {
-  display: flex;
-  align-items: stretch;
-  width: 100%;
-}
-table.task-queue-table tbody {
-  display: flex;
-  flex-flow: row wrap;
-}
-table.task-queue-table tbody tr {
-  display: flex;
-  width: 100%
-}
-table.task-queue-table > * > tr > * {
-  flex: 1;
-}
-table.task-queue-table > * > tr > .oadatatablecheckbox {
-  flex: 0;
-}
-div.task-queue-modal-content {
-  height: 40em;
-}
-div.openattic-modal-content div.modal-scroll {
-  max-height: 26em;
-  overflow: auto;
-  border-bottom: 1px solid #e1e1e1;
-}
-div.task-queue-modal-content div.dataTables_wrapper {
-  margin-bottom: 0;
-}
-div.task-queue-modal-content div.dataTables_wrapper th.oadatatablecheckbox {
-  width: 100%;
-}
-div.task-queue-modal-content div.dataTables_wrapper div.widget-toolbar.tc_refreshBtn{
-  width: 36px;
-}
-ul.task-queue-pagination {
-  display: table;
-  margin: auto;
-  padding-top: 10px;
-}
-
-/* Navbar */
-.navbar-openattic {
-  margin-bottom: 0;
-  background: #474544;
-  border: 0;
-  border-radius: 0;
-  border-top: 4px solid $oa-color-blue;
-  font-size: 1.2em;
-}
-.navbar-openattic .navbar-header {
-  display: flex;
-  float: none;
-}
-.navbar-openattic .navbar-toggle {
-  margin-left: auto;
-  border: 0;
-}
-.navbar-openattic .navbar-toggle:focus,
-.navbar-openattic .navbar-toggle:hover {
-  background-color: transparent;
-  outline: 0;
-}
-.navbar-openattic .navbar-toggle .icon-bar {
-  background-color: #ececec;
-}
-.navbar-openattic .navbar-toggle:focus .icon-bar,
-.navbar-openattic .navbar-toggle:hover .icon-bar {
-  -webkit-box-shadow: 0 0 3px #fff;
-  box-shadow: 0 0 3px #fff;
-}
-.navbar-openattic .navbar-collapse {
-  padding: 0;
-}
-.navbar-openattic .navbar-nav>li>a,
-.navbar-openattic .navbar-nav>li>.oa-navbar>a {
-  color: #ececec;
-  line-height: 1;
-  padding: 10px 20px;
-  position: relative;
-  display: block;
-  text-decoration: none;
-}
-.navbar-openattic .navbar-nav>li>a:focus,
-.navbar-openattic .navbar-nav>li>a:hover,
-.navbar-openattic .navbar-nav>li>.oa-navbar>a:focus,
-.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
-  color: #ececec;
-}
-.navbar-openattic .navbar-nav>li>a:hover,
-.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
-  background-color: #505050;
-}
-.navbar-openattic .navbar-nav>.open>a,
-.navbar-openattic .navbar-nav>.open>a:hover,
-.navbar-openattic .navbar-nav>.open>a:focus,
-.navbar-openattic .navbar-nav>.open>.oa-navbar>a,
-.navbar-openattic .navbar-nav>.open>.oa-navbar>a:hover,
-.navbar-openattic .navbar-nav>.open>.oa-navbar>a:focus {
-  color: #ececec;
-  border-color: transparent;
-  background-color: transparent;
-}
-.navbar-openattic .navbar-primary>li>a {
-  border: 0;
-}
-.navbar-openattic .navbar-primary>.active>a,
-.navbar-openattic .navbar-primary>.active>a:hover,
-.navbar-openattic .navbar-primary>.active>a:focus {
-  color: #ececec;
-  background-color: $oa-color-blue;
-  border: 0;
-}
-.navbar-openattic .navbar-utility a,
-.navbar-openattic .navbar-utility .fa{
-  font-size: 1.0em;
-}
-.navbar-openattic .navbar-utility>.active>a {
-  color: #ececec;
-  background-color: #505050;
-}
-.navbar-openattic .navbar-utility>li>.open>a,
-.navbar-openattic .navbar-utility>li>.open>a:hover,
-.navbar-openattic .navbar-utility>li>.open>a:focus {
-  color: #ececec;
-  border-color: transparent;
-  background-color: transparent;
-}
-@media (min-width: 768px) {
-  .navbar-openattic .navbar-primary>li>a {
-    border-bottom: 4px solid transparent;
-  }
-  .navbar-openattic .navbar-primary>.active>a,
-  .navbar-openattic .navbar-primary>.active>a:hover,
-  .navbar-openattic .navbar-primary>.active>a:focus {
-    background-color: transparent;
-    border-bottom: 4px solid $oa-color-blue;
-  }
-  .navbar-openattic .navbar-utility {
-    border-bottom: 0;
-    font-size: 11px;
-    position: absolute;
-    right: 0;
-    top: 0;
-  }
-}
-@media (max-width: 767px) {
-  .navbar-openattic .navbar-nav {
-    margin: 0;
-  }
-  .navbar-openattic .navbar-collapse,
-  .navbar-openattic .navbar-form {
-    border-color: #ececec;
-  }
-  .navbar-openattic .navbar-collapse {
-    padding: 0;
-  }
-  .navbar-nav .open .dropdown-menu {
-    padding-top: 0;
-    padding-bottom: 0;
-    background-color: #505050;
-  }
-  .navbar-nav .open .dropdown-menu .dropdown-header,
-  .navbar-nav .open .dropdown-menu>li>a {
-    padding: 5px 15px 5px 35px;
-  }
-  .navbar-openattic .navbar-nav .open .dropdown-menu>li>a {
-    color: #ececec;
-  }
-  .navbar-openattic .navbar-nav .open .dropdown-menu>.active>a {
-    color: #ececec;
-    background-color: $oa-color-blue;
-  }
-  .navbar-openattic .navbar-nav>li>a:hover {
-    background-color: $oa-color-blue;
-  }
-  .navbar-openattic .navbar-utility {
-    border-top: 1px solid #ececec;
-  }
-  .navbar-openattic .navbar-primary>.active>a,
-  .navbar-openattic .navbar-primary>.active>a:hover,
-  .navbar-openattic .navbar-primary>.active>a:focus {
-    background-color: $oa-color-blue;
-  }
-}
-
-/* Navs */
-.nav-tabs {
-  margin-bottom: 15px;
-}
-.nav-tabs-openattic {
-  margin-top: -15px;
-  margin-bottom: 15px;
-}
-.nav-tabs-openattic>li>a {
-  padding: 7px 15px 4px 15px;
-}
-.nav-tabs-openattic>li.active>a,
-.nav-tabs-openattic>li.active>a:active,
-.nav-tabs-openattic>li.active>a:focus,
-.nav-tabs-openattic>li.active>a:hover {
-  border: 0!important;
-  border-bottom: 3px solid $oa-color-blue!important;
-}
-
-/* Notifications */
-#toasty .toast.toasty-theme-bootstrap {
-  opacity: 1
-}
-
-/* Pagination */
-.pagination {
-  display: block;
-  margin: 0;
-}
-.pagination>.disabled>a,
-.pagination>.disabled>a:focus,
-.pagination>.disabled>a:hover,
-.pagination>.disabled>span,
-.pagination>.disabled>span:focus,
-.pagination>.disabled>span:hover {
-  -webkit-box-shadow: none;
-  box-shadow: none;
-  cursor: not-allowed;
-  background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
-  background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
-  background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
-}
-.pagination>.active>a,
-.pagination>.active>a:focus,
-.pagination>.active>a:hover,
-.pagination>.active>span,
-.pagination>.active>span:focus,
-.pagination>.active>span:hover,
-.pagination>.disabled>a,
-.pagination>.disabled>a:focus,
-.pagination>.disabled>a:hover,
-.pagination>.disabled>span,
-.pagination>.disabled>span:focus,
-.pagination>.disabled>span:hover,
-.pagination>li>a,
-.pagination>li>span,
-.panel-group
-.panel-heading {
-  background-repeat: repeat-x;
-  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
-}
-.pagination>li>a,
-.pagination>li>span {
-  background-color: #eee;
-  background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
-  background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
-  background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
-  border-color: #b7b7b7;
-  color: #4d5258;
-  cursor: pointer;
-  font-weight: 600;
-  padding: 2px 10px;
-}
-.pagination>.active>span,
-.pagination>.active>span:focus,
-.pagination>.active>span:hover {
-  color: $oa-color-blue;
-  border-color: #fff #e1e1e1 #f4f4f4;
-  border-width: 0 1px;
-}
-
-/* Panel */
-.panel .panel-toolbar {
-  float: right;
-}
-.panel .panel-toolbar div {
-  display: inline-block;
-}
-.panel .panel-toolbar>a,
-.panel .panel-toolbar>.dropdown>a {
-  padding-left: 5px;
-}
-.panel-dashboard {
-  height: 100%;
-  padding-top: 60px;
-}
-.panel-dashboard>.panel-heading {
-  cursor: move;
-  position: relative;
-  margin-top: -60px;
-  width: 100%;
-}
-.panel-dashboard>.panel-heading>.panel-title {
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  -o-text-overflow: ellipsis;
-}
-.panel-dashboard>.panel-heading>.toolbar a {
-  text-decoration: none;
-}
-.panel-dashboard>.panel-body {
-  height: 100%;
-  overflow: auto;
-}
-.panel-dashboard>.panel-body .indent {
-  margin-top: 10px;
-  margin-left: 10px;
-}
-.panel-dashboard .overlay {
-  position: absolute;
-  bottom: 5px;
-  right: 5px;
-  z-index: 10;
-}
-.panel-dashboard .max-height {
-  height: 100%;
-}
-.panel-dashboard .max-height.alert-is-shown {
-  height: 85%;
-}
-.panel-dashboard .fa-2x{
-  vertical-align: middle;
-  margin-right: 0.5em;
-}
-.panel-dashboard .alert.bottom-margin-zero {
-  margin-bottom: 0;
-}
-.panel-openattic {
-  border: $border-color;
-  border-top: 0;
-  border-radius: 0;
-}
-.panel-openattic>.panel-heading {
-  border-top: 2px solid $oa-color-blue;
-  border-radius: 0;
-  padding: 20px 15px;
-}
-.panel-openattic>.panel-heading>.panel-title {
-  color: #333333;
-  font-size: 1.333em;
-  margin: 0;
-  padding: 0;
-}
-.panel-openattic>.panel-body {
-  background: #ffffff;
-  border-top: $border-color;
-  padding: 10px 15px;
-}
-.panel-openattic>.panel-footer {
-  background: #ffffff;
-  border-top: $border-color;
-}
-
-/* Typo */
-a {
-  color: $oa-color-blue;
-}
-a:hover,
-a:focus{
-  color: #474544;
-}
-h1 {
-  letter-spacing: -1px;
-  font-size: 2em;
-}
-h2 {
-  letter-spacing: -1px;
-  font-size: 1.833em;
-}
-h3{
-  display: block;
-  font-size: 1.583em;
-  font-weight: 400;
-}
-h3.sub-title {
-  color: #666666;
-  margin-left: 15px;
-}
-h4{
-  font-size: 1.5em;
-  line-height: normal
-}
-h5{
-  font-size: 1.417em;
-  font-weight: 300;
-  line-height: normal;
-}
-h6{
-  font-size: 1.25em;
-  font-weight: 700;
-  line-height: normal;
-}
-
-/*************************************************************/
-
-/* Statistics */
-.statistics-content {
-  margin: 0 -20px;
-}
-
-/*************************************************************/
-
-/* ApiRecorder */
-.apirecorder {
-  resize: none;
-  width:100%;
-}
-.apirecorder-enabled {
-  color: red;
-}
-
-/* Caret */
-.caret {
-  color: $oa-color-blue;
-}
-
-/* Feedback */
-#feedback .feedback-button {
-  position: fixed;
-  top: 50%;
-  right: 0;
-  padding: 2px 16px;
-  cursor: pointer;
-  color: #ffffff;
-  font-size: 1.2em;
-  font-weight: 700;
-  background-color: $oa-color-blue;
-  border-radius: 5px 5px 0 0;
-  z-index: 99999;
-}
-#feedback .feedback-button:hover {
-  background-color: #2172bf;
-}
-#feedback .feedback-button-transform {
-  -webkit-transform: rotate(-90deg) translate(50%, -100%);
-  -moz-transform: rotate(-90deg) translate(50%, -100%);
-  -ms-transform: rotate(-90deg) translate(50%, -100%);
-  -o-transform: rotate(-90deg) translate(50%, -100%);
-  transform: rotate(-90deg) translate(50%, -100%);
-  -webkit-transform-origin: top right;
-  -moz-transform-origin: top right;
-  -ms-transform-origin: top right;
-  -o-transform-origin: top right;
-  transform-origin: top right;
-}
-#feedback .feedback-button-active {
-  right: 299px;
-}
-#feedback .feedback-button .fa,
-#feedback .feedback-button .glyphicon{
-  padding-right: 6px;
-}
-#feedback .feedback-panel {
-  position: fixed;
-  top: 0;
-  right: -300px;
-  padding: 20px;
-  width: 300px;
-  height: 100%;
-  background-color: #ffffff;
-  border-left: 5px solid $oa-color-blue;
-  z-index: 99999;
-  overflow-y: auto;
-}
-#feedback .feedback-panel-active {
-  right: 0;
-}
-#feedback .feedback-transition {
-  transition: right 150ms cubic-bezier(0.0, 0.0, 0.2, 1);
-}
-
-/* FlexElement */
-/* Container */
-.flex-container {
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-}
-.flex-wrap {
-  -webkit-flex-wrap: wrap;
-  -ms-flex-wrap: wrap;
-  flex-wrap: wrap;
-}
-.flex-nowrap {
-  -webkit-flex-wrap: nowrap;
-  -ms-flex-wrap: nowrap;
-  flex-wrap: nowrap;
-}
-.flex-row {
-  -webkit-flex-direction: row;
-  -ms-flex-direction: row;
-  flex-direction: row;
-}
-.flex-column {
-  -webkit-flex-direction: column;
-  -ms-flex-direction: column;
-  flex-direction: column;
-}
-/* Items */
-.flex-item {
-  margin-bottom: 10px;
-  padding: 15px;
-}
-.flex-item-1 { -webkit-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; padding: 0 10px; }
-.flex-item-2 { -webkit-flex: 2; -moz-flex: 2; -ms-flex: 2; flex: 2; padding: 0 10px; }
-.flex-item-3 { -webkit-flex: 3; -moz-flex: 3; -ms-flex: 3; flex: 3; padding: 0 10px; }
-.flex-item-4 { -webkit-flex: 4; -moz-flex: 4; -ms-flex: 4; flex: 4; padding: 0 10px; }
-.flex-item-5 { -webkit-flex: 5; -moz-flex: 5; -ms-flex: 5; flex: 5; padding: 0 10px; }
-.flex-item-6 { -webkit-flex: 6; -moz-flex: 6; -ms-flex: 6; flex: 6; padding: 0 10px; }
-.flex-item-7 { -webkit-flex: 7; -moz-flex: 7; -ms-flex: 7; flex: 7; padding: 0 10px; }
-.flex-item-8 { -webkit-flex: 8; -moz-flex: 8; -ms-flex: 8; flex: 8; padding: 0 10px; }
-.flex-item-9 { -webkit-flex: 9; -moz-flex: 9; -ms-flex: 9; flex: 9; padding: 0 10px; }
-
-/* Grafana */
-.grafana-container {
-  margin-top: 20px;
-  height: 64px;
-  background:url(./assets/loading.gif) center center no-repeat;
-}
-.grafana {
-  width: 100%;
-  min-height: 600px;
-}
-
-/* Progressbar */
-.progress-bar {
-  background-image: none !important;
-}
-.progress-bar-info {
-  background-color: $oa-color-blue;
-}
-.progress-bar-freespace {
-  background-color: #ddd;
-}
-.progress-bar-stolenspace {
-  background-color: #aaa;
-}
-.progress-bar-outer{
-  margin-top: 5px !important;
-}
-.progress-bar-outer div {
-  border-radius: 31px;
-  background-color: #ffffff;
-  border: 1px solid #ccc;
-  box-shadow: 0 0 0 0;
-  -webkit-box-shadow: 0 0 0 0;
-  -moz-box-shadow: 0 0 0 0;
-  margin: 0;
-  height: 16px;
-}
-.progress-bar-outer div div {
-  background-color: #0091d9;
-}
-.progress-bar-outer div div span {
-  position: relative;
-  top: -3px;
-}
-.oaprogress {
-  position: relative;
-  margin-bottom: 0;
-}
-.oaprogress div.progress-bar {
-  position: static;
-}
-.oaprogress span {
-  position: absolute;
-  display: block;
-  width: 100%;
-  color: black;
-  font-weight: normal;
-}
-
-tags-input .tags {
-  border-radius: 4px;
-  border: 1px solid #ccc;
-  box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-/* TagForm */
-.tag-form label {
-  display: block;
-  margin-bottom: 6px;
-  line-height: 19px;
-  font-weight: 400;
-  font-size: 13px;
-  color: #333;
-  text-align: left;
-  white-space: normal;
-}
-
-/* Trees */
-.tree {
-  min-height: 20px;
-  -webkit-border-radius: 4px;
-  -moz-border-radius: 4px;
-  border-radius: 4px;
-}
-.tree>ul {
-  padding-left: 0;
-}
-.tree ul ul {
-  padding-left: 34px;
-  padding-top: 10px;
-}
-.tree li {
-  list-style-type: none;
-  margin: 0;
-  padding: 5px;
-  position: relative;
-}
-.tree li span {
-  -moz-border-radius: 5px;
-  -webkit-border-radius: 5px;
-  border: 1px dotted #999;
-  border-radius: 5px;
-  display: inline-block;
-  padding: 3px 8px;
-  text-decoration: none;
-  -webkit-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
-  -moz-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
-  -o-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
-  transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
-}
-.tree>ul>li::after,
-.tree>ul>li:before {
-  border: 0;
-}
-.tree li:after,
-.tree li:before {
-  content: '';
-  left: -20px;
-  position: absolute;
-  right: auto;
-}
-.tree li:before {
-  border-left: 1px solid #999;
-  bottom: 50px;
-  height: 100%;
-  top: -11px;
-  width: 1px;
-  -webkit-transition: "border-color 0.1s ease 0.1s";
-  -moz-transition: "border-color 0.1s ease 0.1s";
-  -o-transition: "border-color 0.1s ease 0.1s";
-  transition: "border-color 0.1s ease 0.1s";
-}
-.tree li:after {
-  border-top: 1px solid #999;
-  height: 20px;
-  top: 18px;
-  width: 25px;
-}
-.tree li:last-child::before {
-  height: 30px;
-}
-
-.scrollable-menu {
-  height: auto;
-  max-height: 200px;
-  overflow-x: hidden;
-}
-
-.toggle, .toggle-on, .toggle-off {
-  border-radius: 20px;
-}
-
-.toggle .toggle-handle {
-  border-radius: 20px;
-}
-
-/* CSS Fix */
-a {
-  cursor: pointer;
-}
-form .input-group-addon {
-  color: #a2a2a2 !important;
-  background-color: transparent;
-}
-uib-accordion .panel-title,
-.panel .accordion-title {
-  font-size: 14px !important;
-}
-.panel-body h2:first-child {
-  margin-top: 0;
-}
-.actions {
-  padding-bottom: 10px;
-}
-.pull-left {
-  float: left;
-}
-.code-clogs {
-  display: block;
-  padding: 9px;
-  margin: 0 0 10px;
-  font-size: 13px;
-  line-height: 1.42857143;
-  color: #333;
-  word-break: break-all;
-  word-wrap: break-word;
-  background-color: #f5f5f5;
-  border: 1px solid #ccc;
-  border-radius: 4px;
-  font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
-}
-.degree-sign:after {
-  content: "\00B0 C"!important;
-}
-.formactions.well {
-  overflow: auto;
-  padding: 10px 20px;
-}
-.disabled {
-  pointer-events: none;
-}
-.clickable {
-  cursor: pointer;
-}
-.non-clickable {
-  cursor: initial;
-}
-.locked {
-  cursor: default!important;
-}
-.list-nomargin {
-  margin: 0;
-}
-
-.has-error .has-error-btn {
-  background-color: #f2dede;
-  border-color: #a94442;
-}
-
-.has-error .has-error-btn:disabled:hover {
-  background-color: #f2dede;
-  border-color: #a94442;
-}
-
-/* If javascript is disabled. */
-.noscript {
-  padding-top: 5em;
-}
-.noscript p {
-  color: #777;
-}
-
-/* Notifications */
-
-.notification div.img-circle {
-  width: 50px;
-  height: 50px;
-  position: relative;
-}
-.notification.info div.img-circle {
-  background-color: #5bc0de;
-}
-.notification.error div.img-circle {
-  background-color: #d9534f;
-}
-.notification.success div.img-circle {
-  background-color: #5cb85c;
-}
-.notification.warning div.img-circle {
-  background-color: #f0ad4e;
-}
-
-.notification .icon {
-  background-repeat: no-repeat;
-  background-image: url('./assets/notification-icons.png') !important;
-  height: 36px;
-  width: 36px;
-  position: absolute;
-  margin: 7px;
-}
-.notification.info .icon {
-  background-position: -36px 0;
-}
-.notification.error .icon {
-  background-position: -108px 0;
-}
-.notification.success .icon {
-  background-position: 0 0;
-}
-.notification.warning .icon {
-  background-position: -72px 0;
-}
-
-.required {
-  color: #d04437;
-}
-
-/* oa-helper  */
-oa-helper i {
-  color: $oa-color-blue;
-  cursor: pointer;
-}
-
-.page-footer {
-  font-size: 12px;
-  color: #777;
-  text-align: center;
-  margin-left: 150px;
-  margin-right: 150px;
-  margin-top: 50px;
-  margin-bottom: 50px;
-}
-
-hr.oa-hr-small {
-  margin-top: 5px;
-  margin-bottom: 5px;
-}
-
-.table>thead>tr>th.rbd-striping-object{
-  min-width: 60px;
-}
-.table>thead>tr>th.rbd-striping-stripe {
-  min-width: 100px;
-}
-.rbd-striping-column-separator {
-  width: 1px;
-}
-
-.table>tbody>tr>td.rbd-striping-cell-top {
-  border-top: 1px solid #ccc;
-  border-left: 1px solid #ccc;
-  border-right: 1px solid #ccc;
-}
-.table>tbody>tr>td.rbd-striping-cell-center {
-  border-top: 1px dashed #ccc;
-  border-left: 1px solid #ccc;
-  border-right: 1px solid #ccc;
-}
-.table>tbody>tr>td.rbd-striping-cell-bottom {
-  border-bottom: 1px solid #ccc;
-  border-left: 1px solid #ccc;
-  border-right: 1px solid #ccc;
-}
-
-.dropdown-submenu {
-    position: relative;
-}
-
-.dropdown-submenu>.dropdown-menu {
-    top: 0;
-    left: 100%;
-    margin-top: -6px;
-    margin-left: -1px;
-    -webkit-border-radius: 0 6px 6px 6px;
-    -moz-border-radius: 0 6px 6px;
-    border-radius: 0 6px 6px 6px;
-}
-
-.dropdown-submenu:hover>.dropdown-menu {
-    display: block;
-}
-
-.dropdown-submenu>a:after {
-    display: block;
-    content: " ";
-    float: right;
-    width: 0;
-    height: 0;
-    border-color: transparent;
-    border-style: solid;
-    border-width: 5px 0 5px 5px;
-    border-left-color: $oa-color-blue;
-    margin-top: 5px;
-    margin-right: -10px;
-}
-
-.dropdown-submenu:hover>a:after {
-    border-left-color: $oa-color-blue;
-}
-
-.dropdown-submenu.pull-left {
-    float: none;
-}
-
-.dropdown-submenu.pull-left>.dropdown-menu {
-    left: -100%;
-    margin-left: 10px;
-    -webkit-border-radius: 6px 0 6px 6px;
-    -moz-border-radius: 6px 0 6px 6px;
-    border-radius: 6px 0 6px 6px;
-}
-
-/* Forms */
-.form-group>.control-label>span.required {
-  @extend .fa;
-  @extend .fa-asterisk;
-  @extend .required;
-  font-size: 6px;
-  padding-left: 4px;
-  vertical-align: text-top;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/polyfills.ts b/src/pybind/mgr/dashboard_v2/frontend/src/polyfills.ts
deleted file mode 100644 (file)
index caac2e0..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * 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/docs/ts/latest/guide/browser-support.html
- */
-
-/***************************************************************************************************
- * BROWSER POLYFILLS
- */
-
-/** IE9, IE10 and IE11 requires all of the following polyfills. **/
-import 'core-js/es6/array';
-import 'core-js/es6/date';
-import 'core-js/es6/function';
-import 'core-js/es6/map';
-import 'core-js/es6/math';
-import 'core-js/es6/number';
-import 'core-js/es6/object';
-import 'core-js/es6/parse-float';
-import 'core-js/es6/parse-int';
-import 'core-js/es6/regexp';
-import 'core-js/es6/set';
-import 'core-js/es6/string';
-import 'core-js/es6/symbol';
-import 'core-js/es6/weak-map';
-import 'core-js/es7/object';
-
-/** IE10 and IE11 requires the following for NgClass support on SVG elements */
-// import 'classlist.js';  // Run `npm install --save classlist.js`.
-
-/** IE10 and IE11 requires the following for the Reflect API. */
-// import 'core-js/es6/reflect';
-
-/** Evergreen browsers require these. **/
-// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
-import 'core-js/es7/reflect';
-
-/**
- * Required to support Web Animations `@angular/platform-browser/animations`.
- * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
- **/
-// import 'web-animations-js';  // Run `npm install --save web-animations-js`.
-
-/***************************************************************************************************
- * 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_v2/frontend/src/styles.scss b/src/pybind/mgr/dashboard_v2/frontend/src/styles.scss
deleted file mode 100644 (file)
index c10c1ee..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/* You can add global styles to this file, and also import other style files */
-@import './openattic-theme.scss';
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/styles/chart-tooltip.scss b/src/pybind/mgr/dashboard_v2/frontend/src/styles/chart-tooltip.scss
deleted file mode 100644 (file)
index 835bb36..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-.chart-container {
-  position: absolute;
-  margin: auto;
-  cursor: pointer;
-  overflow: visible;
-}
-
-canvas {
-  -moz-user-select: none;
-  -webkit-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-}
-
-.chartjs-tooltip {
-  opacity: 0;
-  position: absolute;
-  background: rgba(0, 0, 0, 0.7);
-  color: white;
-  border-radius: 3px;
-  -webkit-transition: all 0.1s ease;
-  transition: all 0.1s ease;
-  pointer-events: none;
-  font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif !important;
-
-  -webkit-transform: translate(-50%, 0);
-  transform: translate(-50%, 0);
-
-  &.transform-left {
-    transform: translate(-10%, 0);
-
-    &::after {
-      left: 10%;
-    }
-  }
-
-  &.transform-right {
-    transform: translate(-90%, 0);
-
-    &::after {
-      left: 90%;
-    }
-  }
-}
-
-.chartjs-tooltip::after {
-  content: ' ';
-  position: absolute;
-  top: 100%; /* At the bottom of the tooltip */
-  left: 50%;
-  margin-left: -5px;
-  border-width: 5px;
-  border-style: solid;
-  border-color: black transparent transparent transparent;
-}
-
-::ng-deep .chartjs-tooltip-key {
-  display: inline-block;
-  width: 10px;
-  height: 10px;
-  margin-right: 10px;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/test.ts b/src/pybind/mgr/dashboard_v2/frontend/src/test.ts
deleted file mode 100644 (file)
index 19beece..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/* tslint:disable:ordered-imports */
-// This file is required by karma.conf.js and loads recursively all the .spec and framework files
-
-import 'zone.js/dist/long-stack-trace-zone';
-import 'zone.js/dist/proxy.js';
-import 'zone.js/dist/sync-test';
-import 'zone.js/dist/jasmine-patch';
-import 'zone.js/dist/async-test';
-import 'zone.js/dist/fake-async-test';
-import { getTestBed } from '@angular/core/testing';
-import {
-  BrowserDynamicTestingModule,
-  platformBrowserDynamicTesting
-} from '@angular/platform-browser-dynamic/testing';
-
-// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
-declare const __karma__: any;
-declare const require: any;
-
-// Prevent Karma from running prematurely.
-__karma__.loaded = function () {};
-
-// First, initialize the Angular testing environment.
-getTestBed().initTestEnvironment(
-  BrowserDynamicTestingModule,
-  platformBrowserDynamicTesting()
-);
-// Then we find all the tests.
-const context = require.context('./', true, /\.spec\.ts$/);
-// And load the modules.
-context.keys().map(context);
-// Finally, start Karma to run the tests.
-__karma__.start();
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.app.json b/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.app.json
deleted file mode 100644 (file)
index 39ba8db..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "extends": "../tsconfig.json",
-  "compilerOptions": {
-    "outDir": "../out-tsc/app",
-    "baseUrl": "./",
-    "module": "es2015",
-    "types": []
-  },
-  "exclude": [
-    "test.ts",
-    "**/*.spec.ts"
-  ]
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.spec.json b/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.spec.json
deleted file mode 100644 (file)
index 63d89ff..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-  "extends": "../tsconfig.json",
-  "compilerOptions": {
-    "outDir": "../out-tsc/spec",
-    "baseUrl": "./",
-    "module": "commonjs",
-    "target": "es5",
-    "types": [
-      "jasmine",
-      "node"
-    ]
-  },
-  "files": [
-    "test.ts"
-  ],
-  "include": [
-    "**/*.spec.ts",
-    "**/*.d.ts"
-  ]
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/typings.d.ts b/src/pybind/mgr/dashboard_v2/frontend/src/typings.d.ts
deleted file mode 100644 (file)
index ef5c7bd..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/* SystemJS module definition */
-declare var module: NodeModule;
-interface NodeModule {
-  id: string;
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/tsconfig.json b/src/pybind/mgr/dashboard_v2/frontend/tsconfig.json
deleted file mode 100644 (file)
index a6c016b..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  "compileOnSave": false,
-  "compilerOptions": {
-    "outDir": "./dist/out-tsc",
-    "sourceMap": true,
-    "declaration": false,
-    "moduleResolution": "node",
-    "emitDecoratorMetadata": true,
-    "experimentalDecorators": true,
-    "target": "es5",
-    "typeRoots": [
-      "node_modules/@types"
-    ],
-    "lib": [
-      "es2017",
-      "dom"
-    ]
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/tslint.json b/src/pybind/mgr/dashboard_v2/frontend/tslint.json
deleted file mode 100644 (file)
index d2486f1..0000000
+++ /dev/null
@@ -1,180 +0,0 @@
-{
-  "rulesDirectory": [
-    "node_modules/codelyzer"
-  ],
-  "extends": [
-    "tslint-eslint-rules"
-  ],
-  "rules": {
-    "no-consecutive-blank-lines": true,
-    "arrow-return-shorthand": true,
-    "callable-types": true,
-    "class-name": true,
-    "comment-format": [
-      true,
-      "check-space"
-    ],
-    "curly": true,
-    "eofline": true,
-    "forin": true,
-    "import-blacklist": [
-      true,
-      "rxjs",
-      "rxjs/Rx"
-    ],
-    "import-spacing": true,
-    "indent": [
-      true,
-      "spaces"
-    ],
-    "interface-over-type-literal": true,
-    "label-position": true,
-    "max-line-length": [
-      true,
-      100
-    ],
-    "member-access": false,
-    "member-ordering": [
-      true,
-      {
-        "order": [
-          "static-field",
-          "instance-field",
-          "static-method",
-          "instance-method"
-        ]
-      }
-    ],
-    "no-arg": true,
-    "no-bitwise": true,
-    "no-console": [
-      true,
-      "debug",
-      "info",
-      "time",
-      "timeEnd",
-      "trace"
-    ],
-    "no-construct": true,
-    "no-debugger": true,
-    "no-duplicate-super": true,
-    "no-empty": false,
-    "no-empty-interface": true,
-    "no-eval": true,
-    "no-inferrable-types": [
-      true,
-      "ignore-params"
-    ],
-    "no-misused-new": true,
-    "no-non-null-assertion": true,
-    "no-shadowed-variable": true,
-    "no-string-literal": false,
-    "no-string-throw": true,
-    "no-switch-case-fall-through": true,
-    "no-trailing-whitespace": true,
-    "no-unnecessary-initializer": true,
-    "no-unused-expression": true,
-    "no-use-before-declare": true,
-    "no-var-keyword": true,
-    "object-literal-sort-keys": false,
-    "one-line": [
-      true,
-      "check-open-brace",
-      "check-catch",
-      "check-else",
-      "check-whitespace"
-    ],
-    "prefer-const": true,
-    "quotemark": [
-      true,
-      "single"
-    ],
-    "radix": true,
-    "semicolon": [
-      true,
-      "always"
-    ],
-    "triple-equals": [
-      true,
-      "allow-null-check"
-    ],
-    "typedef-whitespace": [
-      true,
-      {
-        "call-signature": "nospace",
-        "index-signature": "nospace",
-        "parameter": "nospace",
-        "property-declaration": "nospace",
-        "variable-declaration": "nospace"
-      }
-    ],
-    "unified-signatures": true,
-    "variable-name": [
-      true,
-      "check-format",
-      "allow-snake-case"
-    ],
-    "whitespace": [
-      true,
-      "check-branch",
-      "check-decl",
-      "check-operator",
-      "check-separator",
-      "check-type",
-      "check-module"
-    ],
-    "directive-selector": [
-      true,
-      "attribute",
-      "cd",
-      "camelCase"
-    ],
-    "component-selector": [
-      true,
-      "element",
-      "cd",
-      "kebab-case"
-    ],
-    "angular-whitespace": [true, "check-interpolation", "check-semicolon"],
-    "no-output-on-prefix": true,
-    "use-input-property-decorator": true,
-    "use-output-property-decorator": true,
-    "use-host-property-decorator": true,
-    "no-attribute-parameter-decorator": true,
-    "no-input-rename": true,
-    "no-output-rename": true,
-    "use-life-cycle-interface": true,
-    "use-pipe-transform-interface": true,
-    "component-class-suffix": true,
-    "directive-class-suffix": true,
-    "no-forward-ref": true,
-    "no-output-named-after-standard-event": true,
-    "ordered-imports": true,
-    "no-extra-semi": true,
-    "ter-no-irregular-whitespace": true,
-    "no-multi-spaces": true,
-    "brace-style": [
-      true,
-      "1tbs",
-      {
-        "allowSingleLine": false
-      }
-    ],
-    "ter-indent": [
-      true,
-      2,
-      {
-        "SwitchCase": 1,
-        "FunctionDeclaration": {
-          "body": 1,
-          "parameters": "first"
-        },
-        "FunctionExpression": {
-          "body": 1,
-          "parameters": "first"
-        }
-      }
-    ],
-    "space-in-parens": [true, "never"]
-  }
-}
diff --git a/src/pybind/mgr/dashboard_v2/module.py b/src/pybind/mgr/dashboard_v2/module.py
deleted file mode 100644 (file)
index 6877bd8..0000000
+++ /dev/null
@@ -1,292 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-openATTIC mgr plugin (based on CherryPy)
-"""
-from __future__ import absolute_import
-
-import errno
-import os
-import socket
-try:
-    from urlparse import urljoin
-except ImportError:
-    from urllib.parse import urljoin
-try:
-    import cherrypy
-except ImportError:
-    # To be picked up and reported by .can_run()
-    cherrypy = None
-
-from mgr_module import MgrModule, MgrStandbyModule
-
-if 'COVERAGE_ENABLED' in os.environ:
-    import coverage
-    _cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)))
-    _cov.start()
-
-# pylint: disable=wrong-import-position
-from . import logger, mgr
-from .controllers.auth import Auth
-from .tools import load_controllers, json_error_page, SessionExpireAtBrowserCloseTool, \
-                   NotificationQueue, RequestLoggingTool
-from .settings import options_command_list, handle_option_command
-
-
-# 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
-
-
-def prepare_url_prefix(url_prefix):
-    """
-    return '' if no prefix, or '/prefix' without slash in the end.
-    """
-    url_prefix = urljoin('/', url_prefix)
-    return url_prefix.rstrip('/')
-
-
-class Module(MgrModule):
-    """
-    dashboard module entrypoint
-    """
-
-    COMMANDS = [
-        {
-            'cmd': 'dashboard set-login-credentials '
-                   'name=username,type=CephString '
-                   'name=password,type=CephString',
-            'desc': 'Set the login credentials',
-            'perm': 'w'
-        },
-        {
-            'cmd': 'dashboard set-session-expire '
-                   'name=seconds,type=CephInt',
-            'desc': 'Set the session expire timeout',
-            'perm': 'w'
-        }
-    ]
-    COMMANDS.extend(options_command_list())
-
-    @property
-    def url_prefix(self):
-        return self._url_prefix
-
-    def __init__(self, *args, **kwargs):
-        super(Module, self).__init__(*args, **kwargs)
-        mgr.init(self)
-        self._url_prefix = ''
-
-    @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: incomplete build?"
-
-        return True, ""
-
-    @classmethod
-    def get_frontend_path(cls):
-        current_dir = os.path.dirname(os.path.abspath(__file__))
-        return os.path.join(current_dir, 'frontend/dist')
-
-    def configure_cherrypy(self):
-        server_addr = self.get_localized_config('server_addr', '::')
-        server_port = self.get_localized_config('server_port', '8080')
-        if server_addr is None:
-            raise RuntimeError(
-                'no server_addr configured; '
-                'try "ceph config-key put mgr/{}/{}/server_addr <ip>"'
-                .format(self.module_name, self.get_mgr_id()))
-        self.log.info('server_addr: %s server_port: %s', server_addr,
-                      server_port)
-
-        self._url_prefix = prepare_url_prefix(self.get_config('url_prefix',
-                                                              default=''))
-
-        # Initialize custom handlers.
-        cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
-        cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
-        cherrypy.tools.request_logging = RequestLoggingTool()
-
-        # 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
-        }
-        cherrypy.config.update(config)
-
-        config = {
-            '/': {
-                'tools.staticdir.on': True,
-                'tools.staticdir.dir': self.get_frontend_path(),
-                'tools.staticdir.index': 'index.html'
-            }
-        }
-
-        # Publish the URI that others may use to access the service we're
-        # about to start serving
-        self.set_uri("http://{0}:{1}{2}/".format(
-            socket.getfqdn() if server_addr == "::" else server_addr,
-            server_port,
-            self.url_prefix
-        ))
-
-        cherrypy.tree.mount(Module.ApiRoot(self), '{}/api'.format(self.url_prefix))
-        cherrypy.tree.mount(Module.StaticRoot(), '{}/'.format(self.url_prefix), config=config)
-
-    def serve(self):
-        if 'COVERAGE_ENABLED' in os.environ:
-            _cov.start()
-        self.configure_cherrypy()
-
-        cherrypy.engine.start()
-        NotificationQueue.start_queue()
-        logger.info('Waiting for engine...')
-        cherrypy.engine.block()
-        if 'COVERAGE_ENABLED' in os.environ:
-            _cov.stop()
-            _cov.save()
-        logger.info('Engine done')
-
-    def shutdown(self):
-        super(Module, self).shutdown()
-        logger.info('Stopping server...')
-        NotificationQueue.stop()
-        cherrypy.engine.exit()
-        logger.info('Stopped server')
-
-    def handle_command(self, cmd):
-        res = handle_option_command(cmd)
-        if res[0] != -errno.ENOSYS:
-            return res
-        if cmd['prefix'] == 'dashboard set-login-credentials':
-            Auth.set_login_credentials(cmd['username'], cmd['password'])
-            return 0, 'Username and password updated', ''
-        elif cmd['prefix'] == 'dashboard set-session-expire':
-            self.set_config('session-expire', str(cmd['seconds']))
-            return 0, 'Session expiration timeout updated', ''
-
-        return (-errno.EINVAL, '', 'Command not found \'{0}\''
-                .format(cmd['prefix']))
-
-    def notify(self, notify_type, notify_id):
-        NotificationQueue.new_notification(notify_type, notify_id)
-
-    class ApiRoot(object):
-
-        _cp_config = {
-            'tools.sessions.on': True,
-            'tools.authenticate.on': True
-        }
-
-        def __init__(self, mgrmod):
-            self.ctrls = load_controllers()
-            logger.debug('Loaded controllers: %s', self.ctrls)
-
-            first_level_ctrls = [ctrl for ctrl in self.ctrls
-                                 if '/' not in ctrl._cp_path_]
-            multi_level_ctrls = set(self.ctrls).difference(first_level_ctrls)
-
-            for ctrl in first_level_ctrls:
-                logger.info('Adding controller: %s -> /api/%s', ctrl.__name__,
-                            ctrl._cp_path_)
-                inst = ctrl()
-                setattr(Module.ApiRoot, ctrl._cp_path_, inst)
-
-            for ctrl in multi_level_ctrls:
-                path_parts = ctrl._cp_path_.split('/')
-                path = '/'.join(path_parts[:-1])
-                key = path_parts[-1]
-                parent_ctrl_classes = [c for c in self.ctrls
-                                       if c._cp_path_ == path]
-                if len(parent_ctrl_classes) != 1:
-                    logger.error('No parent controller found for %s! '
-                                 'Please check your path in the ApiController '
-                                 'decorator!', ctrl)
-                else:
-                    inst = ctrl()
-                    setattr(parent_ctrl_classes[0], key, inst)
-
-        @cherrypy.expose
-        def index(self):
-            tpl = """API Endpoints:<br>
-            <ul>
-            {lis}
-            </ul>
-            """
-            endpoints = ['<li><a href="{}">{}</a></li>'.format(ctrl._cp_path_, ctrl.__name__) for
-                         ctrl in self.ctrls]
-            return tpl.format(lis='\n'.join(endpoints))
-
-    class StaticRoot(object):
-        pass
-
-
-class StandbyModule(MgrStandbyModule):
-    def serve(self):
-        server_addr = self.get_localized_config('server_addr', '::')
-        server_port = self.get_localized_config('server_port', '7000')
-        if server_addr is None:
-            msg = 'no server_addr configured; try "ceph config-key set ' \
-                  'mgr/dashboard/server_addr <ip>"'
-            raise RuntimeError(msg)
-        self.log.info("server_addr: %s server_port: %s",
-                      server_addr, server_port)
-        cherrypy.config.update({
-            'server.socket_host': server_addr,
-            'server.socket_port': int(server_port),
-            'engine.autoreload.on': False
-        })
-
-        module = self
-
-        class Root(object):
-            @cherrypy.expose
-            def index(self):
-                active_uri = module.get_active_uri()
-                if active_uri:
-                    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)
-
-        url_prefix = prepare_url_prefix(self.get_config('url_prefix',
-                                                        default=''))
-        cherrypy.tree.mount(Root(), "{}/".format(url_prefix), {})
-        self.log.info("Starting engine...")
-        cherrypy.engine.start()
-        self.log.info("Waiting for engine...")
-        cherrypy.engine.wait(state=cherrypy.engine.states.STOPPED)
-        self.log.info("Engine done.")
-
-    def shutdown(self):
-        self.log.info("Stopping server...")
-        cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
-        cherrypy.engine.stop()
-        self.log.info("Stopped server")
diff --git a/src/pybind/mgr/dashboard_v2/requirements.txt b/src/pybind/mgr/dashboard_v2/requirements.txt
deleted file mode 100644 (file)
index f6191ea..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-astroid==1.6.1
-attrs==17.4.0
-backports.functools-lru-cache==1.4
-cheroot==6.0.0
-CherryPy==13.1.0
-configparser==3.5.0
-coverage==4.4.2
-enum34==1.1.6
-funcsigs==1.0.2
-isort==4.2.15
-lazy-object-proxy==1.3.1
-mccabe==0.6.1
-mock==2.0.0
-more-itertools==4.1.0
-pbr==3.1.1
-pluggy==0.6.0
-portend==2.2
-py==1.5.2
-pycodestyle==2.3.1
-pycparser==2.18
-pylint==1.8.2
-pytest==3.3.2
-pytest-cov==2.5.1
-python-bcrypt==0.3.2
-pytz==2017.3
-requests==2.18.4
-singledispatch==3.4.0.3
-six==1.11.0
-tempora==1.10
-tox==2.9.1
-virtualenv==15.1.0
-wrapt==1.10.11
diff --git a/src/pybind/mgr/dashboard_v2/run-backend-api-tests.sh b/src/pybind/mgr/dashboard_v2/run-backend-api-tests.sh
deleted file mode 100755 (executable)
index 64ed636..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env bash
-
-# run from ./
-
-# creating temp directory to store virtualenv and teuthology
-TEMP_DIR=`mktemp -d`
-
-get_cmake_variable() {
-    local variable=$1
-    grep "$variable" CMakeCache.txt | cut -d "=" -f 2
-}
-
-read -r -d '' TEUTHOLOFY_PY_REQS <<EOF
-apache-libcloud==2.2.1 \
-asn1crypto==0.22.0 \
-bcrypt==3.1.4 \
-certifi==2018.1.18 \
-cffi==1.10.0 \
-chardet==3.0.4 \
-configobj==5.0.6 \
-cryptography==2.1.4 \
-enum34==1.1.6 \
-gevent==1.2.2 \
-greenlet==0.4.13 \
-idna==2.5 \
-ipaddress==1.0.18 \
-Jinja2==2.9.6 \
-manhole==1.5.0 \
-MarkupSafe==1.0 \
-netaddr==0.7.19 \
-packaging==16.8 \
-paramiko==2.4.0 \
-pexpect==4.4.0 \
-psutil==5.4.3 \
-ptyprocess==0.5.2 \
-pyasn1==0.2.3 \
-pycparser==2.17 \
-PyNaCl==1.2.1 \
-pyparsing==2.2.0 \
-python-dateutil==2.6.1 \
-PyYAML==3.12 \
-requests==2.18.4 \
-six==1.10.0 \
-urllib3==1.22
-EOF
-
-
-CURR_DIR=`pwd`
-
-cd $TEMP_DIR
-
-virtualenv --python=/usr/bin/python venv
-source venv/bin/activate
-eval pip install $TEUTHOLOFY_PY_REQS
-pip install -r $CURR_DIR/requirements.txt
-deactivate
-
-git clone https://github.com/ceph/teuthology.git
-
-cd $CURR_DIR
-cd ../../../../build
-
-CEPH_MGR_PY_VERSION_MAJOR=$(get_cmake_variable MGR_PYTHON_VERSION | cut -d '.' -f1)
-if [ -n "$CEPH_MGR_PY_VERSION_MAJOR" ]; then
-    CEPH_PY_VERSION_MAJOR=${CEPH_MGR_PY_VERSION_MAJOR}
-else
-    if [ $(get_cmake_variable WITH_PYTHON2) = ON ]; then
-        CEPH_PY_VERSION_MAJOR=2
-    else
-        CEPH_PY_VERSION_MAJOR=3
-    fi
-fi
-
-export COVERAGE_ENABLED=true
-export COVERAGE_FILE=.coverage.mgr.dashboard
-
-MGR=2 RGW=1 ../src/vstart.sh -n -d
-sleep 10
-
-source $TEMP_DIR/venv/bin/activate
-BUILD_DIR=`pwd`
-
-if [ "$#" -gt 0 ]; then
-  TEST_CASES=""
-  for t in "$@"; do
-    TEST_CASES="$TESTS_CASES $t"
-  done
-else
-  TEST_CASES=`for i in \`ls $BUILD_DIR/../qa/tasks/mgr/dashboard_v2/test_*\`; do F=$(basename $i); M="${F%.*}"; echo -n " tasks.mgr.dashboard_v2.$M"; done`
-  TEST_CASES="tasks.mgr.test_dashboard_v2 $TEST_CASES"
-fi
-
-export PATH=$BUILD_DIR/bin:$PATH
-export LD_LIBRARY_PATH=$BUILD_DIR/lib/cython_modules/lib.${CEPH_PY_VERSION_MAJOR}/:$BUILD_DIR/lib
-export PYTHONPATH=$TEMP_DIR/teuthology:$BUILD_DIR/../qa:$BUILD_DIR/lib/cython_modules/lib.${CEPH_PY_VERSION_MAJOR}/
-eval python ../qa/tasks/vstart_runner.py $TEST_CASES
-
-deactivate
-killall ceph-mgr
-sleep 10
-../src/stop.sh
-sleep 5
-
-cd $CURR_DIR
-rm -rf $TEMP_DIR
-
diff --git a/src/pybind/mgr/dashboard_v2/run-frontend-unittests.sh b/src/pybind/mgr/dashboard_v2/run-frontend-unittests.sh
deleted file mode 100755 (executable)
index 96f5db4..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-cd $CEPH_ROOT/src/pybind/mgr/dashboard_v2/frontend
-
-npm run build -- --prod
-npm run test -- --browsers PhantomJS --watch=false
-npm run lint
diff --git a/src/pybind/mgr/dashboard_v2/run-tox.sh b/src/pybind/mgr/dashboard_v2/run-tox.sh
deleted file mode 100755 (executable)
index f5784fb..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-# run from ./ or from ../
-: ${MGR_DASHBOARD_V2_VIRTUALENV:=/tmp/mgr-dashboard_v2-virtualenv}
-: ${WITH_PYTHON3:=ON}
-test -d dashboard_v2 && cd dashboard_v2
-
-if [ -e tox.ini ]; then
-    TOX_PATH=`readlink -f tox.ini`
-else
-    TOX_PATH=`readlink -f $(dirname $0)/tox.ini`
-fi
-
-if [ -z $CEPH_BUILD_DIR ]; then
-    export CEPH_BUILD_DIR=$(dirname ${TOX_PATH})
-fi
-
-source ${MGR_DASHBOARD_V2_VIRTUALENV}/bin/activate
-
-if [ "$WITH_PYTHON3" = "ON" ]; then
-  ENV_LIST="cov-init,py27,py3,cov-report,lint"
-else
-  ENV_LIST="cov-init,py27,cov-report,lint"
-fi
-
-tox -c ${TOX_PATH} -e $ENV_LIST
-
diff --git a/src/pybind/mgr/dashboard_v2/services/__init__.py b/src/pybind/mgr/dashboard_v2/services/__init__.py
deleted file mode 100644 (file)
index 139759b..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
diff --git a/src/pybind/mgr/dashboard_v2/services/ceph_service.py b/src/pybind/mgr/dashboard_v2/services/ceph_service.py
deleted file mode 100644 (file)
index cb27e1e..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import time
-import collections
-from collections import defaultdict
-
-from .. import mgr
-
-
-class CephService(object):
-    @classmethod
-    def get_service_map(cls, service_name):
-        service_map = {}
-        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(cls, service_name, service_id):
-        for server in mgr.list_servers():
-            for service in server['services']:
-                if service['type'] == service_name:
-                    inst_id = service['id']
-                    if inst_id == service_id:
-                        metadata = mgr.get_metadata(service_name, inst_id)
-                        status = mgr.get_daemon_status(service_name, inst_id)
-                        return {
-                            'id': inst_id,
-                            'type': service_name,
-                            'hostname': server['hostname'],
-                            'metadata': metadata,
-                            'status': status
-                        }
-        return None
-
-    @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 = defaultdict(lambda: defaultdict(
-            lambda: collections.deque(maxlen=10)))
-
-        df = mgr.get("df")
-        pool_stats_dict = dict([(p['id'], p['stats']) for p in df['pools']])
-        now = time.time()
-        for pool_id, stats in pool_stats_dict.items():
-            for stat_name, stat_val in stats.items():
-                pool_stats[pool_id][stat_name].appendleft((now, stat_val))
-
-        for pool in pools:
-            pool['pg_status'] = pg_summary['by_pool'][pool['pool'].__str__()]
-            stats = pool_stats[pool['pool']]
-            s = {}
-
-            def get_rate(series):
-                if len(series) >= 2:
-                    return (float(series[0][1]) - float(series[1][1])) / \
-                        (float(series[0][0]) - float(series[1][0]))
-                return 0
-
-            for stat_name, stat_series in stats.items():
-                s[stat_name] = {
-                    'latest': stat_series[0][1],
-                    'rate': get_rate(stat_series),
-                    'series': [i for i in stat_series]
-                }
-            pool['stats'] = s
-            pools_w_stats.append(pool)
-        return pools_w_stats
diff --git a/src/pybind/mgr/dashboard_v2/settings.py b/src/pybind/mgr/dashboard_v2/settings.py
deleted file mode 100644 (file)
index 4f68fbb..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import errno
-import inspect
-from six import add_metaclass
-
-from . import mgr
-
-
-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)
-    """
-    pass
-
-
-class SettingsMeta(type):
-    def __getattr__(cls, attr):
-        default, stype = getattr(Options, attr)
-        return stype(mgr.get_config(attr, default))
-
-    def __setattr__(cls, attr, value):
-        if not attr.startswith('_') and hasattr(Options, attr):
-            mgr.set_config(attr, str(value))
-        else:
-            setattr(SettingsMeta, attr, value)
-
-
-# pylint: disable=no-init
-@add_metaclass(SettingsMeta)
-class Settings(object):
-    pass
-
-
-def _options_command_map():
-    def filter_attr(member):
-        return not inspect.isroutine(member)
-
-    cmd_map = {}
-    for option, value 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('_', '-'))
-        cmd_map[key_get] = {'name': option, 'type': None}
-        cmd_map[key_set] = {'name': option, 'type': value[1]}
-    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 not opt['type']:
-            cmd_list.append({
-                'cmd': '{}'.format(cmd),
-                'desc': 'Get the {} option value'.format(opt['name']),
-                'perm': 'r'
-            })
-        else:
-            cmd_list.append({
-                'cmd': '{} name=value,type={}'
-                       .format(cmd, py2ceph(opt['type'])),
-                'desc': 'Set the {} option value'.format(opt['name']),
-                'perm': 'w'
-            })
-
-    return cmd_list
-
-
-def handle_option_command(cmd):
-    if cmd['prefix'] not in _OPTIONS_COMMAND_MAP:
-        return (-errno.ENOSYS, '', "Command not found '{}'".format(cmd['prefix']))
-
-    opt = _OPTIONS_COMMAND_MAP[cmd['prefix']]
-    if not opt['type']:
-        # get option
-        return 0, str(getattr(Settings, opt['name'])), ''
-
-    # set option
-    setattr(Settings, opt['name'], opt['type'](cmd['value']))
-    return 0, 'Option {} updated'.format(opt['name']), ''
diff --git a/src/pybind/mgr/dashboard_v2/tests/__init__.py b/src/pybind/mgr/dashboard_v2/tests/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard_v2/tests/helper.py b/src/pybind/mgr/dashboard_v2/tests/helper.py
deleted file mode 100644 (file)
index effe21d..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-# -*- coding: utf-8 -*-
-# pylint: disable=W0212
-from __future__ import absolute_import
-
-import json
-
-import cherrypy
-from cherrypy.test import helper
-
-from ..controllers.auth import Auth
-from ..tools import json_error_page, SessionExpireAtBrowserCloseTool
-
-
-class ControllerTestCase(helper.CPWebCase):
-    def __init__(self, *args, **kwargs):
-        cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
-        cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
-        cherrypy.config.update({'error_page.default': json_error_page})
-        super(ControllerTestCase, self).__init__(*args, **kwargs)
-
-    def _request(self, url, method, data=None):
-        if not data:
-            b = None
-            h = None
-        else:
-            b = json.dumps(data)
-            h = [('Content-Type', 'application/json'),
-                 ('Content-Length', str(len(b)))]
-        self.getPage(url, method=method, body=b, headers=h)
-
-    def _get(self, url):
-        self._request(url, 'GET')
-
-    def _post(self, url, data=None):
-        self._request(url, 'POST', data)
-
-    def _delete(self, url, data=None):
-        self._request(url, 'DELETE', data)
-
-    def _put(self, url, data=None):
-        self._request(url, 'PUT', data)
-
-    def jsonBody(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):
-        """Fail if value != self.body."""
-        json_body = self.jsonBody()
-        if data != json_body:
-            if msg is None:
-                msg = 'expected body:\n%r\n\nactual body:\n%r' % (
-                    data, json_body)
-            self._handlewebError(msg)
diff --git a/src/pybind/mgr/dashboard_v2/tests/test_notification.py b/src/pybind/mgr/dashboard_v2/tests/test_notification.py
deleted file mode 100644 (file)
index bca27f9..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import random
-import time
-import unittest
-
-
-from ..tools import NotificationQueue
-
-
-class Listener(object):
-    def __init__(self):
-        NotificationQueue.register(self.log_type1, 'type1')
-        NotificationQueue.register(self.log_type2, 'type2')
-        NotificationQueue.register(self.log_type1_3, ['type1', 'type3'])
-        NotificationQueue.register(self.log_all)
-        self.type1 = []
-        self.type2 = []
-        self.type1_3 = []
-        self.all = []
-
-        # 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.append(val)
-
-    def log_type2(self, val):
-        self.type2.append(val)
-
-    def log_type1_3(self, val):
-        self.type1_3.append(val)
-
-    def log_all(self, val):
-        self.all.append(val)
-
-    def clear(self):
-        self.type1 = []
-        self.type2 = []
-        self.type1_3 = []
-        self.all = []
-
-
-class NotificationQueueTest(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.listener = Listener()
-
-    def setUp(self):
-        self.listener.clear()
-
-    def test_invalid_register(self):
-        with self.assertRaises(Exception) as ctx:
-            NotificationQueue.register(None, 1)
-        self.assertEqual(str(ctx.exception),
-                         "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])
-
-    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, 500):
-            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)
diff --git a/src/pybind/mgr/dashboard_v2/tests/test_rbd_mirroring.py b/src/pybind/mgr/dashboard_v2/tests/test_rbd_mirroring.py
deleted file mode 100644 (file)
index 7087ba6..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-from __future__ import absolute_import
-
-import json
-import mock
-
-import cherrypy
-
-from .. import mgr
-from ..controllers.summary import Summary
-from ..controllers.rbd_mirroring import RbdMirror
-from .helper 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_local_count': 5,
-        'image_remote_count': 6,
-        'image_error_count': 7,
-        'image_warning_count': 8,
-        'name': 'pool_name'
-    }
-}
-
-mock_get_daemon_status = {
-    'json': json.dumps(_status)
-}
-
-mock_osd_map = {
-    'pools': [{
-        'pool_name': 'rbd',
-        'application_metadata': {'rbd'}
-    }]
-}
-
-
-class RbdMirroringControllerTest(ControllerTestCase):
-
-    @classmethod
-    def setup_server(cls):
-        mgr.list_servers.return_value = mock_list_servers
-        mgr.get_metadata.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': []},
-
-        }[key]
-        mgr.url_prefix = ''
-        mgr.get_mgr_id.return_value = 0
-        mgr.have_mon_connection.return_value = True
-
-        RbdMirror._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
-
-        Summary._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
-
-        cherrypy.tree.mount(RbdMirror(), '/api/test/rbdmirror')
-        cherrypy.tree.mount(Summary(), '/api/test/summary')
-
-    @mock.patch('dashboard_v2.controllers.rbd_mirroring.rbd')
-    def test_default(self, rbd_mock):  # pylint: disable=W0613
-        self._get('/api/test/rbdmirror')
-        result = self.jsonBody()
-        self.assertStatus(200)
-        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_v2.controllers.rbd_mirroring.rbd')
-    def test_summary(self, rbd_mock):  # pylint: disable=W0613
-        """We're also testing `summary`, as it also uses code from `rbd_mirroring.py`"""
-        self._get('/api/test/summary')
-        self.assertStatus(200)
-
-        summary = self.jsonBody()['rbd_mirroring']
-        self.assertEqual(summary, {'errors': 0, 'warnings': 1})
diff --git a/src/pybind/mgr/dashboard_v2/tests/test_settings.py b/src/pybind/mgr/dashboard_v2/tests/test_settings.py
deleted file mode 100644 (file)
index 92fcf7f..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import errno
-import unittest
-
-from .. import mgr
-from .. import settings
-from ..settings import Settings, handle_option_command
-
-
-class SettingsTest(unittest.TestCase):
-    CONFIG_KEY_DICT = {}
-
-    @classmethod
-    def setUpClass(cls):
-        # pylint: disable=protected-access
-        settings.Options.GRAFANA_API_HOST = ('localhost', str)
-        settings.Options.GRAFANA_API_PORT = (3000, int)
-        settings._OPTIONS_COMMAND_MAP = settings._options_command_map()
-
-    @classmethod
-    def mock_set_config(cls, attr, val):
-        cls.CONFIG_KEY_DICT[attr] = val
-
-    @classmethod
-    def mock_get_config(cls, attr, default):
-        return cls.CONFIG_KEY_DICT.get(attr, default)
-
-    def setUp(self):
-        self.CONFIG_KEY_DICT.clear()
-        mgr.set_config.side_effect = self.mock_set_config
-        mgr.get_config.side_effect = self.mock_get_config
-        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')
-
-    def test_set_setting(self):
-        Settings.GRAFANA_API_HOST = 'grafanahost'
-        self.assertEqual(Settings.GRAFANA_API_HOST, 'grafanahost')
-
-    def test_get_cmd(self):
-        r, out, err = handle_option_command(
-            {'prefix': 'dashboard get-grafana-api-port'})
-        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'})
-        self.assertEqual(r, 0)
-        self.assertEqual(out, 'Option GRAFANA_API_PORT updated')
-        self.assertEqual(err, '')
-
-    def test_inv_cmd(self):
-        r, out, err = handle_option_command(
-            {'prefix': 'dashboard get-non-existent-option'})
-        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'})
-        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'})
-        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'")
diff --git a/src/pybind/mgr/dashboard_v2/tests/test_tcmu_iscsi.py b/src/pybind/mgr/dashboard_v2/tests/test_tcmu_iscsi.py
deleted file mode 100644 (file)
index 88077cb..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-from __future__ import absolute_import
-
-import cherrypy
-
-from .. import mgr
-from ..controllers.tcmu_iscsi import TcmuIscsi
-from .helper import ControllerTestCase
-
-mocked_servers = [{
-    'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)',
-    'hostname': 'ceph-dev',
-    'services': [{'id': 'a:b', 'type': 'tcmu-runner'}]
-}]
-
-mocked_metadata = {
-    'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)',
-    'pool_name': 'pool1',
-    'image_name': 'image1',
-    'image_id': '42',
-    'optimized_since': 100.0,
-}
-
-mocked_get_daemon_status = {
-    'lock_owner': 'true',
-}
-
-mocked_get_counter = {
-    'librbd-42-pool1-image1.lock_acquired_time': [[10000.0, 10000.0]],
-    'librbd-42-pool1-image1.rd': 43,
-    'librbd-42-pool1-image1.wr': 44,
-    'librbd-42-pool1-image1.rd_bytes': 45,
-    'librbd-42-pool1-image1.wr_bytes': 46,
-}
-
-mocked_get_rate = 47
-
-
-class TcmuIscsiControllerTest(ControllerTestCase):
-
-    @classmethod
-    def setup_server(cls):
-        mgr.list_servers.return_value = mocked_servers
-        mgr.get_metadata.return_value = mocked_metadata
-        mgr.get_daemon_status.return_value = mocked_get_daemon_status
-        mgr.get_counter.return_value = mocked_get_counter
-        mgr.get_rate.return_value = mocked_get_rate
-        mgr.url_prefix = ''
-        TcmuIscsi._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
-
-        cherrypy.tree.mount(TcmuIscsi(), "/api/test/tcmu")
-
-    def test_list(self):
-        self._get('/api/test/tcmu')
-        self.assertStatus(200)
-        self.assertJsonBody({
-            'daemons': [{
-                'server_hostname': 'ceph-dev',
-                'version': 'ceph version 13.0.0-5083- () mimic (dev)',
-                'optimized_paths': 1, 'non_optimized_paths': 0}],
-            'images': [{
-                'device_id': 'b',
-                'pool_name': 'pool1',
-                'name': 'image1',
-                'id': '42', 'optimized_paths': ['ceph-dev'],
-                'non_optimized_paths': [],
-                'optimized_since': 1e-05,
-                'stats': {'rd': 47, 'rd_bytes': 47, 'wr': 47, 'wr_bytes': 47},
-                'stats_history': {
-                    'rd': 43, 'wr': 44, 'rd_bytes': 45, 'wr_bytes': 46}
-            }]
-        })
diff --git a/src/pybind/mgr/dashboard_v2/tests/test_tools.py b/src/pybind/mgr/dashboard_v2/tests/test_tools.py
deleted file mode 100644 (file)
index ca4d904..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import cherrypy
-from cherrypy.lib.sessions import RamSession
-from mock import patch
-
-from .helper import ControllerTestCase
-from ..tools import RESTController
-
-
-# pylint: disable=W0613
-class FooResource(RESTController):
-    elems = []
-
-    def list(self, *vpath, **params):
-        return FooResource.elems
-
-    def create(self, data, *args, **kwargs):
-        FooResource.elems.append(data)
-        return data
-
-    def get(self, key, *args, **kwargs):
-        if args:
-            return {'detail': (key, args)}
-        return FooResource.elems[int(key)]
-
-    def delete(self, key):
-        del FooResource.elems[int(key)]
-
-    def bulk_delete(self):
-        FooResource.elems = []
-
-    def set(self, data, key):
-        FooResource.elems[int(key)] = data
-        return dict(key=key, **data)
-
-
-class FooArgs(RESTController):
-    @RESTController.args_from_json
-    def set(self, code, name):
-        return {'code': code, 'name': name}
-
-
-# pylint: disable=C0102
-class Root(object):
-    foo = FooResource()
-    fooargs = FooArgs()
-
-
-class RESTControllerTest(ControllerTestCase):
-
-    @classmethod
-    def setup_server(cls):
-        cherrypy.tree.mount(Root())
-
-    def test_empty(self):
-        self._delete("/foo")
-        self.assertStatus(204)
-        self._get("/foo")
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'application/json')
-        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', 'application/json')
-
-            self._get("/foo")
-            self.assertStatus('200 OK')
-            self.assertHeader('Content-Type', 'application/json')
-            self.assertJsonBody([data] * 5)
-
-            self._put('/foo/0', {'newdata': 'newdata'})
-            self.assertStatus('200 OK')
-            self.assertHeader('Content-Type', 'application/json')
-            self.assertJsonBody({'newdata': 'newdata', 'key': '0'})
-
-    def test_not_implemented(self):
-        self._put("/foo")
-        self.assertStatus(405)
-        body = self.jsonBody()
-        self.assertIsInstance(body, dict)
-        assert body['detail'] == 'Method not implemented.'
-        assert '405' in body['status']
-        assert 'traceback' in body
-
-    def test_args_from_json(self):
-        self._put("/fooargs/hello", {'name': 'world'})
-        self.assertJsonBody({'code': 'hello', 'name': 'world'})
-
-    def test_detail_route(self):
-        self._get('/foo/1/detail')
-        self.assertJsonBody({'detail': ['1', ['detail']]})
-
-        self._post('/foo/1/detail', 'post-data')
-        self.assertStatus(405)
diff --git a/src/pybind/mgr/dashboard_v2/tools.py b/src/pybind/mgr/dashboard_v2/tools.py
deleted file mode 100644 (file)
index f801370..0000000
+++ /dev/null
@@ -1,531 +0,0 @@
-# -*- coding: utf-8 -*-
-# pylint: disable=W0212
-from __future__ import absolute_import
-
-import collections
-import datetime
-import importlib
-import inspect
-import json
-import os
-import pkgutil
-import sys
-import time
-import threading
-
-import cherrypy
-
-from . import logger
-
-
-def ApiController(path):
-    def decorate(cls):
-        cls._cp_controller_ = True
-        cls._cp_path_ = path
-        config = {
-            'tools.sessions.on': True,
-            'tools.sessions.name': Session.NAME,
-            'tools.session_expire_at_browser_close.on': True
-        }
-        if not hasattr(cls, '_cp_config'):
-            cls._cp_config = {}
-        if 'tools.authenticate.on' not in cls._cp_config:
-            config['tools.authenticate.on'] = False
-        cls._cp_config.update(config)
-        return cls
-    return decorate
-
-
-def AuthRequired(enabled=True):
-    def decorate(cls):
-        if not hasattr(cls, '_cp_config'):
-            cls._cp_config = {
-                'tools.authenticate.on': enabled
-            }
-        else:
-            cls._cp_config['tools.authenticate.on'] = enabled
-        return cls
-    return decorate
-
-
-def load_controllers():
-    # setting sys.path properly when not running under the mgr
-    dashboard_dir = os.path.dirname(os.path.realpath(__file__))
-    mgr_dir = os.path.dirname(dashboard_dir)
-    if mgr_dir not in sys.path:
-        sys.path.append(mgr_dir)
-
-    controllers = []
-    ctrls_path = '{}/controllers'.format(dashboard_dir)
-    mods = [mod for _, mod, _ in pkgutil.iter_modules([ctrls_path])]
-    for mod_name in mods:
-        mod = importlib.import_module('.controllers.{}'.format(mod_name),
-                                      package='dashboard_v2')
-        for _, cls in mod.__dict__.items():
-            # Controllers MUST be derived from the class BaseController.
-            if inspect.isclass(cls) and issubclass(cls, BaseController) and \
-                    hasattr(cls, '_cp_controller_'):
-                controllers.append(cls)
-
-    return controllers
-
-
-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))
-
-
-class BaseController(object):
-    """
-    Base class for all controllers providing API endpoints.
-    """
-
-
-class RequestLoggingTool(cherrypy.Tool):
-    def __init__(self):
-        cherrypy.Tool.__init__(self, 'before_handler', self.request_begin,
-                               priority=95)
-
-    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 _get_user(self):
-        if hasattr(cherrypy.serving, 'session'):
-            return cherrypy.session.get(Session.USERNAME)
-        return None
-
-    def request_begin(self):
-        req = cherrypy.request
-        user = self._get_user()
-        if user:
-            logger.debug("[%s:%s] [%s] [%s] %s", req.remote.ip,
-                         req.remote.port, req.method, user, req.path_info)
-        else:
-            logger.debug("[%s:%s] [%s] %s", req.remote.ip,
-                         req.remote.port, req.method, req.path_info)
-
-    def request_error(self):
-        self._request_log(logger.error)
-        logger.error(cherrypy.response.body)
-
-    def request_end(self):
-        status = cherrypy.response.status[:3]
-        if status in ["401"]:
-            # log unauthorized accesses
-            self._request_log(logger.warning)
-        else:
-            self._request_log(logger.info)
-
-    def _format_bytes(self, num):
-        units = ['B', 'K', 'M', 'G']
-
-        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 = self._get_user()
-        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", req.remote.ip,
-                      req.remote.port, req.method, status,
-                      "{0:.3f}s".format(lat), length, req.path_info)
-
-
-# pylint: disable=too-many-instance-attributes
-class ViewCache(object):
-    VALUE_OK = 0
-    VALUE_STALE = 1
-    VALUE_NONE = 2
-    VALUE_EXCEPTION = 3
-
-    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):
-            try:
-                t0 = time.time()
-                val = self.fn(*self.args, **self.kwargs)
-                t1 = time.time()
-            except Exception as ex:
-                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.datetime.now()
-                    self._view.getter_thread = None
-                    self._view.exception = None
-
-            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()
-
-        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.datetime.now()
-                if self.value_when and now - self.value_when < datetime.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()
-
-                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
-                        return ViewCache.VALUE_EXCEPTION, self.exception
-                    return ViewCache.VALUE_OK, self.value
-                elif 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
-                return ViewCache.VALUE_NONE, None
-
-    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)
-        return wrapper
-
-
-class RESTController(BaseController):
-    """
-    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)
-    * delete(key)
-
-    Test with curl:
-
-    curl -H "Content-Type: application/json" -X POST \
-         -d '{"username":"xyz","password":"xyz"}'  http://127.0.0.1:8080/foo
-    curl http://127.0.0.1:8080/foo
-    curl http://127.0.0.1:8080/foo/0
-
-    """
-
-    def _not_implemented(self, is_sub_path):
-        methods = [method
-                   for ((method, _is_element), (meth, _))
-                   in self._method_mapping.items()
-                   if _is_element == is_sub_path is not None and hasattr(self, meth)]
-        cherrypy.response.headers['Allow'] = ','.join(methods)
-        raise cherrypy.HTTPError(405, 'Method not implemented.')
-
-    _method_mapping = {
-        ('GET', False): ('list', 200),
-        ('PUT', False): ('bulk_set', 200),
-        ('PATCH', False): ('bulk_set', 200),
-        ('POST', False): ('create', 201),
-        ('DELETE', False): ('bulk_delete', 204),
-        ('GET', True): ('get', 200),
-        ('PUT', True): ('set', 200),
-        ('PATCH', True): ('set', 200),
-        ('DELETE', True): ('delete', 204),
-    }
-
-    def _get_method(self, vpath):
-        is_sub_path = bool(len(vpath))
-        try:
-            method_name, status_code = self._method_mapping[
-                (cherrypy.request.method, is_sub_path)]
-        except KeyError:
-            self._not_implemented(is_sub_path)
-        method = getattr(self, method_name, None)
-        if not method:
-            self._not_implemented(is_sub_path)
-        return method, status_code
-
-    @cherrypy.expose
-    def default(self, *vpath, **params):
-        method, status_code = self._get_method(vpath)
-
-        if cherrypy.request.method not in ['GET', 'DELETE']:
-            method = RESTController._takes_json(method)
-
-        if cherrypy.request.method != 'DELETE':
-            method = RESTController._returns_json(method)
-
-        cherrypy.response.status = status_code
-
-        return method(*vpath, **params)
-
-    @staticmethod
-    def args_from_json(func):
-        func._args_from_json_ = True
-        return func
-
-    # pylint: disable=W1505
-    @staticmethod
-    def _takes_json(func):
-        def inner(*args, **kwargs):
-            content_length = int(cherrypy.request.headers['Content-Length'])
-            body = cherrypy.request.body.read(content_length)
-            if not body:
-                raise cherrypy.HTTPError(400, 'Empty body. Content-Length={}'
-                                         .format(content_length))
-            try:
-                data = json.loads(body.decode('utf-8'))
-            except Exception as e:
-                raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
-                                         .format(str(e)))
-            if hasattr(func, '_args_from_json_'):
-                if sys.version_info > (3, 0):
-                    f_args = list(inspect.signature(func).parameters.keys())
-                else:
-                    f_args = inspect.getargspec(func).args[1:]
-                n_args = []
-                for arg in args:
-                    n_args.append(arg)
-                for arg in f_args:
-                    if arg in data:
-                        n_args.append(data[arg])
-                        data.pop(arg)
-                kwargs.update(data)
-                return func(*n_args, **kwargs)
-
-            return func(data, *args, **kwargs)
-        return inner
-
-    @staticmethod
-    def _returns_json(func):
-        def inner(*args, **kwargs):
-            cherrypy.response.headers['Content-Type'] = 'application/json'
-            ret = func(*args, **kwargs)
-            return json.dumps(ret).encode('utf8')
-        return inner
-
-    @staticmethod
-    def split_vpath(vpath):
-        if not vpath:
-            return None, None
-        if len(vpath) == 1:
-            return vpath[0], None
-        return vpath[0], vpath[1]
-
-
-class Session(object):
-    """
-    This class contains all relevant settings related to cherrypy.session.
-    """
-    NAME = 'session_id'
-
-    # The keys used to store the information in the cherrypy.session.
-    USERNAME = '_username'
-    TS = '_ts'
-    EXPIRE_AT_BROWSER_CLOSE = '_expire_at_browser_close'
-
-    # The default values.
-    DEFAULT_EXPIRE = 1200.0
-
-
-class SessionExpireAtBrowserCloseTool(cherrypy.Tool):
-    """
-    A CherryPi Tool which takes care that the cookie does not expire
-    at browser close if the 'Keep me logged in' checkbox was selected
-    on the login page.
-    """
-    def __init__(self):
-        cherrypy.Tool.__init__(self, 'before_finalize', self._callback)
-
-    def _callback(self):
-        # Shall the cookie expire at browser close?
-        expire_at_browser_close = cherrypy.session.get(
-            Session.EXPIRE_AT_BROWSER_CLOSE, True)
-        logger.debug("expire at browser close: %s", expire_at_browser_close)
-        if expire_at_browser_close:
-            # Get the cookie and its name.
-            cookie = cherrypy.response.cookie
-            name = cherrypy.request.config.get(
-                'tools.sessions.name', Session.NAME)
-            # Make the cookie a session cookie by purging the
-            # fields 'expires' and 'max-age'.
-            logger.debug("expire at browser close: removing 'expires' and 'max-age'")
-            if name in cookie:
-                del cookie[name]['expires']
-                del cookie[name]['max-age']
-
-
-class NotificationQueue(threading.Thread):
-    _ALL_TYPES_ = '__ALL__'
-    _listeners = collections.defaultdict(set)
-    _lock = threading.Lock()
-    _cond = threading.Condition()
-    _queue = collections.deque()
-    _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()
-        logger.debug("starting notification queue")
-        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()
-        logger.debug("waiting for notification queue to finish")
-        instance.join()
-        logger.debug("notification queue stopped")
-
-    @classmethod
-    def register(cls, func, types=None):
-        """Registers function to listen for notifications
-
-        If the second parameter `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)
-            types (str|list): the single type to listen, or a list of types
-        """
-        with cls._lock:
-            if not types:
-                cls._listeners[cls._ALL_TYPES_].add(func)
-                return
-            if isinstance(types, str):
-                cls._listeners[types].add(func)
-            elif isinstance(types, list):
-                for typ in types:
-                    cls._listeners[typ].add(func)
-            else:
-                raise Exception("types param is neither a string nor a list")
-
-    @classmethod
-    def new_notification(cls, notify_type, notify_value):
-        cls._queue.append((notify_type, notify_value))
-        with cls._cond:
-            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_])
-            for listener in listeners:
-                listener(notify_value)
-
-    def run(self):
-        logger.debug("notification queue started")
-        while self._running:
-            private_buffer = []
-            logger.debug("NQ: processing queue: %s", len(self._queue))
-            try:
-                while True:
-                    private_buffer.append(self._queue.popleft())
-            except IndexError:
-                pass
-            self.notify_listeners(private_buffer)
-            with self._cond:
-                self._cond.wait(1.0)
-        # flush remaining events
-        logger.debug("NQ: flush remaining events: %s", len(self._queue))
-        self.notify_listeners(self._queue)
-        self._queue.clear()
-        logger.debug("notification queue finished")
diff --git a/src/pybind/mgr/dashboard_v2/tox.ini b/src/pybind/mgr/dashboard_v2/tox.ini
deleted file mode 100644 (file)
index 743a8a6..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-[tox]
-envlist = cov-init,py27,py3,cov-report,lint
-skipsdist = true
-
-[testenv]
-deps=-r{toxinidir}/requirements.txt
-setenv=
-    UNITTEST=true
-    WEBTEST_INTERACTIVE=false
-    COVERAGE_FILE= .coverage.{envname}
-    PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3:{toxinidir}/../../../../build/lib/cython_modules/lib.2
-    LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
-    PATH = {toxinidir}/../../../../build/bin:$PATH
-commands=
-    {envbindir}/py.test --cov=. --cov-report= --junitxml=junit.{envname}.xml --doctest-modules controllers/rbd.py tests/
-
-[testenv:cov-init]
-setenv =
-    COVERAGE_FILE = .coverage
-deps = coverage
-commands =
-    coverage erase
-
-[testenv:cov-report]
-setenv =
-    COVERAGE_FILE = .coverage
-deps = coverage
-commands =
-    coverage combine
-    coverage report
-    coverage xml
-
-[testenv:lint]
-setenv =
-    PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3:{toxinidir}/../../../../build/lib/cython_modules/lib.2
-    LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
-deps=-r{toxinidir}/requirements.txt
-commands=
-    pylint --rcfile=.pylintrc --jobs=5 . module.py tools.py controllers tests services
-    pycodestyle --max-line-length=100 --exclude=python2.7,.tox,venv,frontend --ignore=E402,E121,E123,E126,E226,E24,E704,W503 .
index 660158e62d37ac9dc8097952729d82830c381da6..eb3d82b98777337e8e11cb7939baed3b0777e043 100644 (file)
@@ -579,19 +579,19 @@ add_ceph_test(unittest_bufferlist.sh ${CMAKE_SOURCE_DIR}/src/unittest_bufferlist
 add_test(NAME run-tox-ceph-disk COMMAND bash ${CMAKE_SOURCE_DIR}/src/ceph-disk/run-tox.sh)
 add_test(NAME run-tox-ceph-detect-init COMMAND bash ${CMAKE_SOURCE_DIR}/src/ceph-detect-init/run-tox.sh)
 if(WITH_MGR)
-    add_test(NAME run-tox-mgr-dashboard_v2 COMMAND bash ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/run-tox.sh)
+    add_test(NAME run-tox-mgr-dashboard COMMAND bash ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/run-tox.sh)
 endif(WITH_MGR)
 
 set(CEPH_DISK_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/ceph-disk-virtualenv)
 set(CEPH_DETECT_INIT_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/ceph-detect-init-virtualenv)
 if(WITH_MGR)
-    set(MGR_DASHBOARD_V2_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/mgr-dashboard_v2-virtualenv)
+    set(MGR_DASHBOARD_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/mgr-dashboard-virtualenv)
 endif(WITH_MGR)
 
 set_property(TEST 
   run-tox-ceph-disk
   run-tox-ceph-detect-init
-  run-tox-mgr-dashboard_v2
+  run-tox-mgr-dashboard
   PROPERTY ENVIRONMENT
   CEPH_BUILD_DIR=${CMAKE_BINARY_DIR}
   CEPH_ROOT=${CMAKE_SOURCE_DIR}
@@ -600,7 +600,7 @@ set_property(TEST
   CEPH_BUILD_VIRTUALENV=${CEPH_BUILD_VIRTUALENV}
   CEPH_DISK_VIRTUALENV=${CEPH_DISK_VIRTUALENV}
   CEPH_DETECT_INIT_VIRTUALENV=${CEPH_DETECT_INIT_VIRTUALENV}
-  MGR_DASHBOARD_V2_VIRTUALENV=${MGR_DASHBOARD_V2_VIRTUALENV}
+  MGR_DASHBOARD_VIRTUALENV=${MGR_DASHBOARD_VIRTUALENV}
   LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/lib
   PATH=$ENV{PATH}:${CMAKE_RUNTIME_OUTPUT_DIRECTORY}:${CMAKE_SOURCE_DIR}/src
   PYTHONPATH=${CMAKE_SOURCE_DIR}/src/pybind
index 08b50288963a82b7da65c10055cf55c1fc7a8a91..c212b5419245c419ab1f67f144f24d5bad737edc 100644 (file)
@@ -1,10 +1,9 @@
 #scripts
-if(WITH_MGR_DASHBOARD_V2_FRONTEND)
-
+if(WITH_MGR_DASHBOARD_FRONTEND)
   if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
-    add_ceph_test(mgr-dashboard_v2-frontend-unittests ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/run-frontend-unittests.sh)
+    add_ceph_test(mgr-dashboard-frontend-unittests ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/run-frontend-unittests.sh)
   endif(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
 
-  add_ceph_test(mgr-dashboard_v2-smoke.sh ${CMAKE_CURRENT_SOURCE_DIR}/mgr-dashboard_v2-smoke.sh)
-endif(WITH_MGR_DASHBOARD_V2_FRONTEND)
+  add_ceph_test(mgr-dashboard-smoke.sh ${CMAKE_CURRENT_SOURCE_DIR}/mgr-dashboard-smoke.sh)
+endif(WITH_MGR_DASHBOARD_FRONTEND)
 
diff --git a/src/test/mgr/mgr-dashboard-smoke.sh b/src/test/mgr/mgr-dashboard-smoke.sh
new file mode 100755 (executable)
index 0000000..582909a
--- /dev/null
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+#
+# Copyright (C) 2014,2015,2017 Red Hat <contact@redhat.com>
+# Copyright (C) 2018 SUSE LLC
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Library Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program 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 Library Public License for more details.
+#
+source $(dirname $0)/../detect-build-env-vars.sh
+source $CEPH_ROOT/qa/standalone/ceph-helpers.sh
+
+function run() {
+    local dir=$1
+    shift
+
+    export CEPH_MON=127.0.0.1:7160  # git grep '\<7160\>' : there must be only one
+    export CEPH_ARGS
+    CEPH_ARGS+="--fsid=$(uuidgen) --auth-supported=none "
+    CEPH_ARGS+="--mon-initial-members=a --mon-host=$MON "
+    CEPH_ARGS+="--mgr-initial-modules=dashboard "
+    CEPH_ARGS+="--mon-host=$CEPH_MON"
+
+    setup $dir || return 1
+    TEST_dashboard $dir || return 1
+    teardown $dir || return 1
+}
+
+function TEST_dashboard() {
+    local dir=$1
+    shift
+
+    run_mon $dir a || return 1
+    timeout 30 ceph mon stat || return 1
+    ceph config-key set mgr/dashboard/x/server_port 7161
+    MGR_ARGS+="--mgr_module_path=${CEPH_ROOT}/src/pybind/mgr "
+    run_mgr $dir x ${MGR_ARGS} || return 1
+
+    tries=0
+    while [[ $tries < 30 ]] ; do
+        if [ $(ceph status -f json | jq .mgrmap.available) = "true" ]
+        then
+            break
+        fi
+        tries=$((tries+1))
+        sleep 1
+    done
+    ceph_adm tell mgr dashboard set-login-credentials admin admin
+
+    tries=0
+    while [[ $tries < 30 ]] ; do
+        if curl -c $dir/cookiefile -X POST -d '{"username":"admin","password":"admin"}' http://127.0.0.1:7161/api/auth
+        then
+            if curl -b $dir/cookiefile -s http://127.0.0.1:7161/api/summary | \
+                 jq '.health.overall_status' | grep HEALTH_
+            then
+                break
+            fi
+        fi
+        tries=$((tries+1))
+        sleep 0.5
+    done
+}
+
+main mgr-dashboard-smoke "$@"
+
+# Local Variables:
+# compile-command: "cd ../.. ; make -j4 TESTS=test/mgr/mgr-dashboard-smoke.sh check"
+# End:
diff --git a/src/test/mgr/mgr-dashboard_v2-smoke.sh b/src/test/mgr/mgr-dashboard_v2-smoke.sh
deleted file mode 100755 (executable)
index b0eb325..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (C) 2014,2015,2017 Red Hat <contact@redhat.com>
-# Copyright (C) 2018 SUSE LLC
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU Library Public License as published by
-# the Free Software Foundation; either version 2, or (at your option)
-# any later version.
-#
-# This program 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 Library Public License for more details.
-#
-source $(dirname $0)/../detect-build-env-vars.sh
-source $CEPH_ROOT/qa/standalone/ceph-helpers.sh
-
-function run() {
-    local dir=$1
-    shift
-
-    export CEPH_MON=127.0.0.1:7160  # git grep '\<7160\>' : there must be only one
-    export CEPH_ARGS
-    CEPH_ARGS+="--fsid=$(uuidgen) --auth-supported=none "
-    CEPH_ARGS+="--mon-initial-members=a --mon-host=$MON "
-    CEPH_ARGS+="--mgr-initial-modules=dashbaord_v2 "
-    CEPH_ARGS+="--mon-host=$CEPH_MON"
-
-    setup $dir || return 1
-    TEST_dashboardv2 $dir || return 1
-    teardown $dir || return 1
-}
-
-function TEST_dashboardv2() {
-    local dir=$1
-    shift
-
-    run_mon $dir a || return 1
-    timeout 30 ceph mon stat || return 1
-    ceph config-key set mgr/dashboard_v2/x/server_port 7161
-    MGR_ARGS+="--mgr_module_path=${CEPH_ROOT}/src/pybind/mgr "
-    run_mgr $dir x ${MGR_ARGS} || return 1
-
-    tries=0
-    while [[ $tries < 30 ]] ; do
-        if [ $(ceph status -f json | jq .mgrmap.available) = "true" ]
-        then
-            break
-        fi
-        tries=$((tries+1))
-        sleep 1
-    done
-    ceph_adm tell mgr dashboard set-login-credentials admin admin
-
-    tries=0
-    while [[ $tries < 30 ]] ; do
-        if curl -c $dir/cookiefile -X POST -d '{"username":"admin","password":"admin"}' http://127.0.0.1:7161/api/auth
-        then
-            if curl -b $dir/cookiefile -s http://127.0.0.1:7161/api/summary | \
-                 jq '.health.overall_status' | grep HEALTH_
-            then
-                break
-            fi
-        fi
-        tries=$((tries+1))
-        sleep 0.5
-    done
-}
-
-main mgr-dashboard_v2-smoke "$@"
-
-# Local Variables:
-# compile-command: "cd ../.. ; make -j4 TESTS=test/mgr/mgr-dashboard_v2-smoke.sh check"
-# End:
index c5fee8d84522627dfab0f93adfbc6feed671b28d..e74f3d61e26227bd6b7125ec72961326fabb46cb 100755 (executable)
@@ -136,7 +136,6 @@ VSTART_SEC="client.vstart.sh"
 
 MON_ADDR=""
 DASH_URLS=""
-DASH_V2_URLS=""
 RESTFUL_URLS=""
 
 conf_fn="$CEPH_CONF_PATH/ceph.conf"
@@ -503,7 +502,7 @@ $DAEMONOPTS
 $COSDSHORT
 $extra_conf
 [mon]
-        mgr initial modules = restful status balancer
+        mgr initial modules = dashboard restful status balancer
 $DAEMONOPTS
 $CMONDEBUG
 $extra_conf
@@ -649,20 +648,19 @@ start_mgr() {
         host = $HOSTNAME
 EOF
 
-       ceph_adm config-key set mgr/restful/$name/server_port $MGR_PORT
+       ceph_adm config-key set mgr/dashboard/$name/server_port $MGR_PORT
         if [ $mgr -eq 1 ]; then
-            RESTFUL_URLS="https://$IP:$MGR_PORT"
+            DASH_URLS="http://$IP:$MGR_PORT"
         else
-            RESTFUL_URLS+=", https://$IP:$MGR_PORT"
+            DASH_URLS+=", http://$IP:$MGR_PORT"
         fi
        MGR_PORT=$(($MGR_PORT + 1000))
 
-        # dashboard_v2
-       ceph_adm config-key set mgr/dashboard_v2/$name/server_port $MGR_PORT
+       ceph_adm config-key set mgr/restful/$name/server_port $MGR_PORT
         if [ $mgr -eq 1 ]; then
-            DASH_V2_URLS="http://$IP:$MGR_PORT"
+            RESTFUL_URLS="https://$IP:$MGR_PORT"
         else
-            DASH_V2_URLS+=", http://$IP:$MGR_PORT"
+            RESTFUL_URLS+=", https://$IP:$MGR_PORT"
         fi
        MGR_PORT=$(($MGR_PORT + 1000))
 
@@ -672,6 +670,10 @@ EOF
 
     # use tell mgr here because the first mgr might not have activated yet
     # to register the python module commands.
+
+    # setting login credentials for dashboard
+    ceph_adm tell mgr dashboard set-login-credentials admin admin
+
     if ceph_adm tell mgr restful create-self-signed-cert; then
         SF=`mktemp`
         ceph_adm restful create-key admin -o $SF
@@ -680,13 +682,6 @@ EOF
     else 
         echo MGR Restful is not working, perhaps the package is not installed?
     fi
-
-    # dashboard_v2
-    sleep 5  # when running with more than 1 mgrs, if we enable dashboard_v2
-             # immediately it will fail, so we just wait for a bit
-    ceph_adm mgr module enable dashboard_v2
-    # setting login credentials for dashboard_v2
-    ceph_adm tell mgr dashboard set-login-credentials admin admin
 }
 
 start_mds() {
@@ -1063,7 +1058,7 @@ fi
 echo "started.  stop.sh to stop.  see out/* (e.g. 'tail -f out/????') for debug output."
 
 echo ""
-echo "dashboard_v2 urls: $DASH_V2_URLS"
+echo "dashboard urls: $DASH_URLS"
 echo "  w/ user/pass: admin / admin"
 echo "restful urls: $RESTFUL_URLS"
 echo "  w/ user/pass: admin / $RESTFUL_SECRET"