]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add RGW user and bucket lists (read-only)
authorVolker Theile <vtheile@suse.com>
Tue, 6 Mar 2018 13:27:21 +0000 (14:27 +0100)
committerVolker Theile <vtheile@suse.com>
Thu, 19 Apr 2018 09:35:13 +0000 (11:35 +0200)
Signed-off-by: Volker Theile <vtheile@suse.com>
33 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/api/api.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts

index 1b3c15bb28540209f61bb3bf954f01301a1a9706..5c8e025abd99eeaf7177baddf0d9f84e772a72ba 100644 (file)
@@ -15,7 +15,9 @@ import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.compone
 import {
   PerformanceCounterComponent
 } from './ceph/performance-counter/performance-counter/performance-counter.component';
+import { RgwBucketListComponent } from './ceph/rgw/rgw-bucket-list/rgw-bucket-list.component';
 import { RgwDaemonListComponent } from './ceph/rgw/rgw-daemon-list/rgw-daemon-list.component';
+import { RgwUserListComponent } from './ceph/rgw/rgw-user-list/rgw-user-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';
@@ -27,10 +29,20 @@ const routes: Routes = [
   { path: 'login', component: LoginComponent },
   { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
   {
-    path: 'rgw',
+    path: 'rgw/daemon',
     component: RgwDaemonListComponent,
     canActivate: [AuthGuardService]
   },
+  {
+    path: 'rgw/user',
+    component: RgwUserListComponent,
+    canActivate: [AuthGuardService]
+  },
+  {
+    path: 'rgw/bucket',
+    component: RgwBucketListComponent,
+    canActivate: [AuthGuardService]
+  },
   { path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] },
   { path: 'block/rbd', component: RbdListComponent, canActivate: [AuthGuardService] },
   { path: 'rbd/add', component: RbdFormComponent, canActivate: [AuthGuardService] },
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html
new file mode 100644 (file)
index 0000000..306d6de
--- /dev/null
@@ -0,0 +1,101 @@
+<tabset *ngIf="selection.hasSingleSelection">
+  <tab i18n-heading heading="Details">
+    <div *ngIf="bucket">
+      <table class="table table-striped table-bordered">
+        <tbody>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Name</td>
+            <td class="col-sm-3">{{ bucket.bucket }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">ID</td>
+            <td>{{ bucket.id }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Owner</td>
+            <td>{{ bucket.owner }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Index type</td>
+            <td>{{ bucket.index_type }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Placement rule</td>
+            <td>{{ bucket.placement_rule }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Marker</td>
+            <td>{{ bucket.marker }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Maximum marker</td>
+            <td>{{ bucket.max_marker }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Version</td>
+            <td>{{ bucket.ver }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Master version</td>
+            <td>{{ bucket.master_ver }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Modification time</td>
+            <td>{{ bucket.mtime }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Zonegroup</td>
+            <td>{{ bucket.zonegroup }}</td>
+          </tr>
+        </tbody>
+      </table>
+
+      <!-- Bucket quota -->
+      <div *ngIf="bucket.bucket_quota">
+        <legend i18n>Bucket quota</legend>
+        <table class="table table-striped table-bordered">
+          <tbody>
+            <tr>
+              <td i18n
+                  class="bold col-sm-1">Enabled</td>
+              <td class="col-sm-3">{{ bucket.bucket_quota.enabled ? "Yes" : "No" }}</td>
+            </tr>
+            <tr>
+              <td i18n
+                  class="bold col-sm-1">Maximum size</td>
+              <td *ngIf="bucket.bucket_quota.max_size <= -1"
+                  i18n
+                  class="col-sm-3">Unlimited</td>
+              <td *ngIf="bucket.bucket_quota.max_size > -1"
+                  class="col-sm-3">
+                {{ bucket.bucket_quota.max_size | dimless }}
+              </td>
+            </tr>
+            <tr>
+              <td i18n
+                  class="bold col-sm-1">Maximum objects</td>
+              <td *ngIf="bucket.bucket_quota.max_objects <= -1"
+                  i18n
+                  class="col-sm-3">Unlimited</td>
+              <td *ngIf="bucket.bucket_quota.max_objects > -1"
+                  class="col-sm-3">
+                {{ bucket.bucket_quota.max_objects }}
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </tab>
+</tabset>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts
new file mode 100644 (file)
index 0000000..443ea95
--- /dev/null
@@ -0,0 +1,34 @@
+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 { RgwBucketDetailsComponent } from './rgw-bucket-details.component';
+
+describe('RgwBucketDetailsComponent', () => {
+  let component: RgwBucketDetailsComponent;
+  let fixture: ComponentFixture<RgwBucketDetailsComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ RgwBucketDetailsComponent ],
+      imports: [
+        SharedModule,
+        TabsModule.forRoot()
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwBucketDetailsComponent);
+    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-bucket-details/rgw-bucket-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts
new file mode 100644 (file)
index 0000000..bc8bc41
--- /dev/null
@@ -0,0 +1,22 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+
+@Component({
+  selector: 'cd-rgw-bucket-details',
+  templateUrl: './rgw-bucket-details.component.html',
+  styleUrls: ['./rgw-bucket-details.component.scss']
+})
+export class RgwBucketDetailsComponent implements OnChanges {
+  bucket: any;
+
+  @Input() selection: CdTableSelection;
+
+  constructor() {}
+
+  ngOnChanges() {
+    if (this.selection.hasSelection) {
+      this.bucket = this.selection.first();
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html
new file mode 100644 (file)
index 0000000..b7b5f3d
--- /dev/null
@@ -0,0 +1,21 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item">Object Gateway</li>
+    <li i18n
+        class="breadcrumb-item active"
+        aria-current="page">Buckets</li>
+  </ol>
+</nav>
+<cd-table [data]="buckets"
+          [columns]="columns"
+          columnMode="flex"
+          selectionType="single"
+          (updateSelection)="updateSelection($event)"
+          identifier="bucket"
+          (fetchData)="getBucketList()"
+          #table>
+  <cd-rgw-bucket-details cdTableDetail
+                         [selection]="selection">
+  </cd-rgw-bucket-details>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts
new file mode 100644 (file)
index 0000000..53d1f85
--- /dev/null
@@ -0,0 +1,54 @@
+import { HttpClientModule } from '@angular/common/http';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
+import { DataTableModule } from '../../../shared/datatable/datatable.module';
+import { SharedModule } from '../../../shared/shared.module';
+import { RgwBucketDetailsComponent } from '../rgw-bucket-details/rgw-bucket-details.component';
+import { RgwBucketListComponent } from './rgw-bucket-list.component';
+
+describe('RgwBucketListComponent', () => {
+  let component: RgwBucketListComponent;
+  let fixture: ComponentFixture<RgwBucketListComponent>;
+
+  const fakeService = {
+    list: () => {
+      return new Promise(function(resolve, reject) {
+        return [];
+      });
+    }
+  };
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        RgwBucketListComponent,
+        RgwBucketDetailsComponent
+      ],
+      imports: [
+        HttpClientModule,
+        RouterTestingModule,
+        BsDropdownModule.forRoot(),
+        TabsModule.forRoot(),
+        DataTableModule,
+        SharedModule
+      ],
+      providers: [{ provide: RgwBucketService, useValue: fakeService }]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwBucketListComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts
new file mode 100644 (file)
index 0000000..49e1242
--- /dev/null
@@ -0,0 +1,45 @@
+import { Component, ViewChild } from '@angular/core';
+
+import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
+import { TableComponent } from '../../../shared/datatable/table/table.component';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+
+@Component({
+  selector: 'cd-rgw-bucket-list',
+  templateUrl: './rgw-bucket-list.component.html',
+  styleUrls: ['./rgw-bucket-list.component.scss']
+})
+export class RgwBucketListComponent {
+  @ViewChild('table') table: TableComponent;
+
+  columns: CdTableColumn[] = [];
+  buckets: object[] = [];
+  selection: CdTableSelection = new CdTableSelection();
+
+  constructor(private rgwBucketService: RgwBucketService) {
+    this.columns = [
+      {
+        name: 'Name',
+        prop: 'bucket',
+        flexGrow: 1
+      },
+      {
+        name: 'Owner',
+        prop: 'owner',
+        flexGrow: 1
+      }
+    ];
+  }
+
+  getBucketList() {
+    this.rgwBucketService.list()
+      .subscribe((resp: object[]) => {
+        this.buckets = resp;
+      });
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+}
index e5686b056af4930876edf8bd3becf37ac1d23350..13fd3f0a88bf9140cd8ffffa2ca347fa9a8b7433 100644 (file)
@@ -35,9 +35,7 @@ describe('RgwDaemonDetailsComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(RgwDaemonDetailsComponent);
     component = fixture.componentInstance;
-
     component.selection = new CdTableSelection();
-
     fixture.detectChanges();
   });
 
index f3587a0a58a1e24327df02122e9ac8b00ccdaa4a..de24cd779899d53c0ad318d125212637747364a0 100644 (file)
@@ -29,8 +29,9 @@ export class RgwDaemonDetailsComponent implements OnChanges {
     if (_.isEmpty(this.serviceId)) {
       return;
     }
-    this.rgwDaemonService.get(this.serviceId).then(resp => {
-      this.metadata = resp['rgw_metadata'];
-    });
+    this.rgwDaemonService.get(this.serviceId)
+      .subscribe((resp) => {
+        this.metadata = resp['rgw_metadata'];
+      });
   }
 }
index 64b703fd98e4162231a668f59692959b7dbbf930..f480f5548d64e7e1d932d48f3244c12737936c62 100644 (file)
@@ -1,8 +1,10 @@
 <nav aria-label="breadcrumb">
   <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item">Object Gateway</li>
     <li i18n
         class="breadcrumb-item active"
-        aria-current="page">Object Gateway</li>
+        aria-current="page">Daemons</li>
   </ol>
 </nav>
 
index f4f08f4c528077facd5767ff678be4fb3579fcbe..f5dbd4785ee3faf17bbf4b38534ef6f41273fac0 100644 (file)
@@ -1,10 +1,8 @@
 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 { SharedModule } from '../../../shared/shared.module';
 import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
 import { RgwDaemonDetailsComponent } from '../rgw-daemon-details/rgw-daemon-details.component';
@@ -16,10 +14,11 @@ describe('RgwDaemonListComponent', () => {
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
-      declarations: [ RgwDaemonListComponent, RgwDaemonDetailsComponent ],
+      declarations: [
+        RgwDaemonListComponent,
+        RgwDaemonDetailsComponent
+      ],
       imports: [
-        DataTableModule,
-        HttpClientTestingModule,
         HttpClientModule,
         TabsModule.forRoot(),
         PerformanceCounterModule,
index affe3962cd8cda2e1682e3bde39acde40c43c65d..4f1d710b4d84e451eee21e6e30886ad681f6629d 100644 (file)
@@ -12,9 +12,9 @@ import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.p
 })
 export class RgwDaemonListComponent {
 
-  columns: Array<CdTableColumn> = [];
-  daemons: Array<object> = [];
-  selection = new CdTableSelection();
+  columns: CdTableColumn[] = [];
+  daemons: object[] = [];
+  selection: CdTableSelection = new CdTableSelection();
 
   constructor(private rgwDaemonService: RgwDaemonService,
               cephShortVersionPipe: CephShortVersionPipe) {
@@ -40,7 +40,7 @@ export class RgwDaemonListComponent {
 
   getDaemonList() {
     this.rgwDaemonService.list()
-      .then((resp) => {
+      .subscribe((resp: object[]) => {
         this.daemons = resp;
       });
   }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html
new file mode 100644 (file)
index 0000000..f05ff79
--- /dev/null
@@ -0,0 +1,125 @@
+<tabset *ngIf="selection.hasSingleSelection">
+  <tab i18n-heading heading="Details">
+    <div *ngIf="user">
+      <table class="table table-striped table-bordered">
+        <tbody>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Username</td>
+            <td class="col-sm-3">{{ user.user_id }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Full name</td>
+            <td class="col-sm-3">{{ user.display_name }}</td>
+          </tr>
+          <tr *ngIf="user.email.length">
+            <td i18n
+                class="bold col-sm-1">Email address</td>
+            <td class="col-sm-3">{{ user.email }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Suspended</td>
+            <td class="col-sm-3">{{ user.suspended ? "Yes" : "No" }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold col-sm-1">Maximum buckets</td>
+            <td class="col-sm-3">{{ user.max_buckets }}</td>
+          </tr>
+          <tr *ngIf="user.subusers && user.subusers.length">
+            <td i18n
+                class="bold col-sm-1">Subusers</td>
+            <td class="col-sm-3">
+              <div *ngFor="let subuser of user.subusers">
+                {{ subuser.id }} ({{ subuser.permissions }})
+              </div>
+            </td>
+          </tr>
+          <tr *ngIf="user.caps && user.caps.length">
+            <td i18n
+                class="bold col-sm-1">Capabilities</td>
+            <td class="col-sm-3">
+              <div *ngFor="let cap of user.caps">
+                {{ cap.type }} ({{ cap.perm }})
+              </div>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      <!-- User quota -->
+      <div *ngIf="user.user_quota">
+        <legend i18n>User quota</legend>
+        <table class="table table-striped table-bordered">
+          <tbody>
+            <tr>
+              <td i18n
+                  class="bold col-sm-1">Enabled</td>
+              <td class="col-sm-3">{{ user.user_quota.enabled ? "Yes" : "No" }}</td>
+            </tr>
+            <tr>
+              <td i18n
+                  class="bold col-sm-1">Maximum size</td>
+              <td *ngIf="user.user_quota.max_size <= -1"
+                  i18n
+                  class="col-sm-3">Unlimited</td>
+              <td *ngIf="user.user_quota.max_size > -1"
+                  class="col-sm-3">
+                {{ user.user_quota.max_size | dimlessBinary }}
+              </td>
+            </tr>
+            <tr>
+              <td i18n
+                  class="bold col-sm-1">Maximum objects</td>
+              <td *ngIf="user.user_quota.max_objects <= -1"
+                  i18n
+                  class="col-sm-3">Unlimited</td>
+              <td *ngIf="user.user_quota.max_objects > -1"
+                  class="col-sm-3">
+                {{ user.user_quota.max_objects }}
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+
+      <!-- Bucket quota -->
+      <div *ngIf="user.bucket_quota">
+        <legend i18n>Bucket quota</legend>
+        <table class="table table-striped table-bordered">
+          <tbody>
+            <tr>
+              <td i18n
+                  class="bold col-sm-1">Enabled</td>
+              <td class="col-sm-3">{{ user.bucket_quota.enabled ? "Yes" : "No" }}</td>
+            </tr>
+            <tr>
+              <td i18n
+                  class="bold col-sm-1">Maximum size</td>
+              <td *ngIf="user.bucket_quota.max_size <= -1"
+                  i18n
+                  class="col-sm-3">Unlimited</td>
+              <td *ngIf="user.bucket_quota.max_size > -1"
+                  class="col-sm-3">
+                {{ user.bucket_quota.max_size | dimlessBinary }}
+              </td>
+            </tr>
+            <tr>
+              <td i18n
+                  class="bold col-sm-1">Maximum objects</td>
+              <td *ngIf="user.bucket_quota.max_objects <= -1"
+                  i18n
+                  class="col-sm-3">Unlimited</td>
+              <td *ngIf="user.bucket_quota.max_objects > -1"
+                  class="col-sm-3">
+                {{ user.bucket_quota.max_objects }}
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </tab>
+</tabset>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts
new file mode 100644 (file)
index 0000000..938f0f4
--- /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 { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { SharedModule } from '../../../shared/shared.module';
+import { RgwUserDetailsComponent } from './rgw-user-details.component';
+
+describe('RgwUserDetailsComponent', () => {
+  let component: RgwUserDetailsComponent;
+  let fixture: ComponentFixture<RgwUserDetailsComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ RgwUserDetailsComponent ],
+      imports: [
+        HttpClientTestingModule,
+        HttpClientModule,
+        SharedModule,
+        TabsModule.forRoot()
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwUserDetailsComponent);
+    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-user-details/rgw-user-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts
new file mode 100644 (file)
index 0000000..1c21ce9
--- /dev/null
@@ -0,0 +1,37 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { RgwUserService } from '../../../shared/api/rgw-user.service';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+
+@Component({
+  selector: 'cd-rgw-user-details',
+  templateUrl: './rgw-user-details.component.html',
+  styleUrls: ['./rgw-user-details.component.scss']
+})
+export class RgwUserDetailsComponent implements OnChanges {
+  user: any;
+
+  @Input() selection: CdTableSelection;
+
+  constructor(private rgwUserService: RgwUserService) {}
+
+  ngOnChanges() {
+    if (this.selection.hasSelection) {
+      this.user = this.selection.first();
+
+      // Sort subusers and capabilities.
+      this.user.subusers = _.sortBy(this.user.subusers, 'id');
+      this.user.caps = _.sortBy(this.user.caps, 'type');
+
+      // Load the user/bucket quota of the selected user.
+      if (this.user.tenant === '') {
+        this.rgwUserService.getQuota(this.user.user_id)
+          .subscribe((resp: object) => {
+            _.extend(this.user, resp);
+          });
+      }
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
new file mode 100644 (file)
index 0000000..66434a0
--- /dev/null
@@ -0,0 +1,20 @@
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li i18n
+        class="breadcrumb-item">Object Gateway</li>
+    <li i18n
+        class="breadcrumb-item active"
+        aria-current="page">Users</li>
+  </ol>
+</nav>
+<cd-table [data]="users"
+          [columns]="columns"
+          columnMode="flex"
+          selectionType="single"
+          (updateSelection)="updateSelection($event)"
+          identifier="user_id"
+          (fetchData)="getUserList()">
+  <cd-rgw-user-details cdTableDetail
+                       [selection]="selection">
+  </cd-rgw-user-details>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts
new file mode 100644 (file)
index 0000000..1b0c18b
--- /dev/null
@@ -0,0 +1,40 @@
+import { HttpClientModule } from '@angular/common/http';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { RgwUserDetailsComponent } from '../rgw-user-details/rgw-user-details.component';
+import { RgwUserListComponent } from './rgw-user-list.component';
+
+describe('RgwUserListComponent', () => {
+  let component: RgwUserListComponent;
+  let fixture: ComponentFixture<RgwUserListComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        RgwUserListComponent,
+        RgwUserDetailsComponent
+      ],
+      imports: [
+        HttpClientModule,
+        BsDropdownModule.forRoot(),
+        TabsModule.forRoot(),
+        SharedModule
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwUserListComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts
new file mode 100644 (file)
index 0000000..14bb823
--- /dev/null
@@ -0,0 +1,60 @@
+import { Component } from '@angular/core';
+
+import { RgwUserService } from '../../../shared/api/rgw-user.service';
+import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+
+@Component({
+  selector: 'cd-rgw-user-list',
+  templateUrl: './rgw-user-list.component.html',
+  styleUrls: ['./rgw-user-list.component.scss']
+})
+export class RgwUserListComponent {
+
+  columns: CdTableColumn[] = [];
+  users: object[] = [];
+  selection: CdTableSelection = new CdTableSelection();
+
+  constructor(private rgwUserService: RgwUserService) {
+    this.columns = [
+      {
+        name: 'Username',
+        prop: 'user_id',
+        flexGrow: 1
+      },
+      {
+        name: 'Full name',
+        prop: 'display_name',
+        flexGrow: 1
+      },
+      {
+        name: 'Email address',
+        prop: 'email',
+        flexGrow: 1
+      },
+      {
+        name: 'Suspended',
+        prop: 'suspended',
+        flexGrow: 1,
+        cellTransformation: CellTemplate.checkIcon
+      },
+      {
+        name: 'Max. buckets',
+        prop: 'max_buckets',
+        flexGrow: 1
+      }
+    ];
+  }
+
+  getUserList() {
+    this.rgwUserService.list()
+      .subscribe((resp: object[]) => {
+        this.users = resp;
+      });
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+}
index 14577d9c71ff4cc83d320443e335767f2681deb1..6e5aaa5b9d0cdebd78fc157e61d60be04f4f42ec 100644 (file)
@@ -5,12 +5,18 @@ import { TabsModule } from 'ngx-bootstrap/tabs';
 
 import { SharedModule } from '../../shared/shared.module';
 import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
+import { RgwBucketDetailsComponent } from './rgw-bucket-details/rgw-bucket-details.component';
+import { RgwBucketListComponent } from './rgw-bucket-list/rgw-bucket-list.component';
 import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component';
 import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component';
+import { RgwUserDetailsComponent } from './rgw-user-details/rgw-user-details.component';
+import { RgwUserListComponent } from './rgw-user-list/rgw-user-list.component';
 
 @NgModule({
   entryComponents: [
-    RgwDaemonDetailsComponent
+    RgwDaemonDetailsComponent,
+    RgwBucketDetailsComponent,
+    RgwUserDetailsComponent
   ],
   imports: [
     CommonModule,
@@ -20,11 +26,19 @@ import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.compon
   ],
   exports: [
     RgwDaemonListComponent,
-    RgwDaemonDetailsComponent
+    RgwDaemonDetailsComponent,
+    RgwBucketListComponent,
+    RgwBucketDetailsComponent,
+    RgwUserListComponent,
+    RgwUserDetailsComponent
   ],
   declarations: [
     RgwDaemonListComponent,
-    RgwDaemonDetailsComponent
+    RgwDaemonDetailsComponent,
+    RgwBucketListComponent,
+    RgwBucketDetailsComponent,
+    RgwUserListComponent,
+    RgwUserDetailsComponent
   ]
 })
 export class RgwModule { }
index 5ed75f6296b0e408a6f4e91f4bbe80eac2d29c48..54cde66a9075ce26a2ae0933a4024859e4498346 100644 (file)
       -->
 
       <!-- 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=""
+      <li dropdown
+          routerLinkActive="active"
+          class="dropdown tc_menuitem tc_menuitem_rgw">
+        <a dropdownToggle
            class="dropdown-toggle"
            data-toggle="dropdown">
           <ng-container i18n>Object Gateway</ng-container>
         </a>
         <ul *dropdownMenu
             class="dropdown-menu">
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_rgw_daemons">
+            <a i18n
+               class="dropdown-item"
+               routerLink="/rgw/daemon">Daemons
+            </a>
+          </li>
           <li routerLinkActive="active"
               class="tc_submenuitem tc_submenuitem_rgw_users">
             <a i18n
                class="dropdown-item"
-               routerLink="/rgw-users">Users
+               routerLink="/rgw/user">Users
             </a>
           </li>
           <li routerLinkActive="active"
               class="tc_submenuitem tc_submenuitem_rgw_buckets">
             <a i18n
                class="dropdown-item"
-               routerLink="/rgw-buckets">Buckets
+               routerLink="/rgw/bucket">Buckets
             </a>
           </li>
         </ul>
       </li>
+
+      <!--
       <li routerLinkActive="active"
           class="tc_menuitem tc_submenuitem_settings">
         <a i18n
            routerLink="/settings">Settings
         </a>
-      </li> -->
+      </li>
+      -->
     </ul>
     <!-- /.navbar-primary -->
 
index 61b1c32e7561b3fa6641d0168563b077a8ddc987..26656cdd0a03a706c1ae6db8d7ee8dae7e128636 100644 (file)
@@ -11,7 +11,9 @@ import { OsdService } from './osd.service';
 import { PoolService } from './pool.service';
 import { RbdMirroringService } from './rbd-mirroring.service';
 import { RbdService } from './rbd.service';
+import { RgwBucketService } from './rgw-bucket.service';
 import { RgwDaemonService } from './rgw-daemon.service';
+import { RgwUserService } from './rgw-user.service';
 import { TablePerformanceCounterService } from './table-performance-counter.service';
 import { TcmuIscsiService } from './tcmu-iscsi.service';
 
@@ -29,7 +31,9 @@ import { TcmuIscsiService } from './tcmu-iscsi.service';
     PoolService,
     RbdService,
     RbdMirroringService,
+    RgwBucketService,
     RgwDaemonService,
+    RgwUserService,
     TablePerformanceCounterService,
     TcmuIscsiService
   ]
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
new file mode 100644 (file)
index 0000000..dadf2f1
--- /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 { RgwBucketService } from './rgw-bucket.service';
+
+describe('RgwBucketService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [RgwBucketService],
+      imports: [HttpClientTestingModule, HttpClientModule]
+    });
+  });
+
+  it(
+    'should be created',
+    inject([RgwBucketService], (service: RgwBucketService) => {
+      expect(service).toBeTruthy();
+    })
+  );
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
new file mode 100644 (file)
index 0000000..adcfc4a
--- /dev/null
@@ -0,0 +1,72 @@
+import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import 'rxjs/add/observable/forkJoin';
+import 'rxjs/add/observable/of';
+import { Observable } from 'rxjs/Observable';
+
+import * as _ from 'lodash';
+
+@Injectable()
+export class RgwBucketService {
+
+  private url = '/api/rgw/proxy/bucket';
+
+  constructor(private http: HttpClient) { }
+
+  list() {
+    return this.http.get(this.url)
+      .flatMap((buckets: string[]) => {
+        if (buckets.length > 0) {
+          return Observable.forkJoin(
+            buckets.map((bucket: string) => {
+              return this.get(bucket);
+            })
+          );
+        }
+        return Observable.of([]);
+      });
+  }
+
+  get(bucket: string) {
+    let params = new HttpParams();
+    params = params.append('bucket', bucket);
+    return this.http.get(this.url, { params: params });
+  }
+
+  create(bucket: string, uid: string) {
+    const body = JSON.stringify({
+      'bucket': bucket,
+      'uid': uid
+    });
+    return this.http.post(`/api/rgw/bucket`, body);
+  }
+
+  update(bucketId: string, bucket: string, uid: string) {
+    let params = new HttpParams();
+    params = params.append('bucket', bucket);
+    params = params.append('bucket-id', bucketId as string);
+    params = params.append('uid', uid);
+    return this.http.put(this.url, null, { params: params });
+  }
+
+  delete(bucket: string, purgeObjects = true) {
+    let params = new HttpParams();
+    params = params.append('bucket', bucket);
+    params = params.append('purge-objects', purgeObjects ? 'true' : 'false');
+    return this.http.delete(this.url, { params: params });
+  }
+
+  find(bucket: string) {
+    let params = new HttpParams();
+    params = params.append('bucket', bucket);
+    return this.http.get(this.url, { params: params })
+      .flatMap((resp: object | null) => {
+        // Make sure we have received valid data.
+        if ((null === resp) || (!_.isObjectLike(resp))) {
+          return Observable.of([]);
+        }
+        // Return an array to be able to support wildcard searching someday.
+        return Observable.of([resp]);
+      });
+  }
+}
index 907537ef2d9e51a38b10a3a468165da93a70c5f0..0e1ffba6000b4a70845ac30c0ab1b48262fe6e8f 100644 (file)
@@ -9,18 +9,10 @@ export class RgwDaemonService {
   constructor(private http: HttpClient) { }
 
   list() {
-    return this.http.get(this.url)
-      .toPromise()
-      .then((resp: any) => {
-        return resp;
-      });
+    return this.http.get(this.url);
   }
 
   get(id: string) {
-    return this.http.get(`${this.url}/${id}`)
-      .toPromise()
-      .then((resp: any) => {
-        return resp;
-      });
+    return this.http.get(`${this.url}/${id}`);
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts
new file mode 100644 (file)
index 0000000..2942eff
--- /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 { RgwUserService } from './rgw-user.service';
+
+describe('RgwUserService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [RgwUserService],
+      imports: [HttpClientTestingModule, HttpClientModule]
+    });
+  });
+
+  it(
+    'should be created',
+    inject([RgwUserService], (service: RgwUserService) => {
+      expect(service).toBeTruthy();
+    })
+  );
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
new file mode 100644 (file)
index 0000000..9585fdf
--- /dev/null
@@ -0,0 +1,43 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import 'rxjs/add/observable/forkJoin';
+import 'rxjs/add/observable/of';
+import { Observable } from 'rxjs/Observable';
+
+@Injectable()
+export class RgwUserService {
+
+  private url = '/api/rgw/proxy/user';
+
+  constructor(private http: HttpClient) { }
+
+  list() {
+    return this.enumerate()
+      .flatMap((uids: string[]) => {
+        if (uids.length > 0) {
+          return Observable.forkJoin(
+            uids.map((uid: string) => {
+              return this.get(uid);
+            })
+          );
+        }
+        return Observable.of([]);
+      });
+  }
+
+  enumerate() {
+    return this.http.get('/api/rgw/proxy/metadata/user');
+  }
+
+  get(uid: string) {
+    let params = new HttpParams();
+    params = params.append('uid', uid);
+    return this.http.get(this.url, { params: params });
+  }
+
+  getQuota(uid: string) {
+    let params = new HttpParams();
+    params = params.append('uid', uid);
+    return this.http.get(`${this.url}?quota`, {params: params});
+  }
+}
index 51dcb0b7f7df9895643df4b89ac69e1768477342..d9ecb65439a663b29efff3a53a3e7d4a8b311a2f 100644 (file)
   <a [routerLink]="[row.cdLink]">{{ value }}</a>
 </ng-template>
 
+<ng-template #checkIconTpl
+             let-value="value">
+  <i class="fa fa-check fa-fw"
+     [hidden]="!value"></i>
+</ng-template>
+
+
 <ng-template #perSecondTpl
              let-row="row"
              let-value="value">
index 7264799099a0eaf8b031efb0543c64e1c6b7264e..aa89fb4747a93095430e3759b1c0fb61a314bc0e 100644 (file)
@@ -36,6 +36,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   @ViewChild('tableCellBoldTpl') tableCellBoldTpl: TemplateRef<any>;
   @ViewChild('sparklineTpl') sparklineTpl: TemplateRef<any>;
   @ViewChild('routerLinkTpl') routerLinkTpl: TemplateRef<any>;
+  @ViewChild('checkIconTpl') checkIconTpl: TemplateRef<any>;
   @ViewChild('perSecondTpl') perSecondTpl: TemplateRef<any>;
   @ViewChild('executingTpl') executingTpl: TemplateRef<any>;
 
@@ -188,6 +189,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
 
   _addTemplates() {
     this.cellTemplates.bold = this.tableCellBoldTpl;
+    this.cellTemplates.checkIcon = this.checkIconTpl;
     this.cellTemplates.sparkline = this.sparklineTpl;
     this.cellTemplates.routerLink = this.routerLinkTpl;
     this.cellTemplates.perSecond = this.perSecondTpl;
index c02e7ff7de92f38cf4c0b54097ad0b0309d5c1f6..28740a53674de33a7f61eebc941b48b4e465ffbb 100644 (file)
@@ -2,6 +2,7 @@ export enum CellTemplate {
   bold = 'bold',
   sparkline = 'sparkline',
   perSecond = 'perSecond',
+  checkIcon = 'checkIcon',
   routerLink = 'routerLink',
   executing = 'executing'
 }