Skip to content

Commit 717c1c5

Browse files
author
Dnyaneshwari
committed
mgr/dashboard: Storage Class Management
Fixes: https://tracker.ceph.com/issues/69606 Signed-off-by: Dnyaneshwari Talwekar <[email protected]>
1 parent be95b64 commit 717c1c5

12 files changed

+408
-5
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export interface ZoneGroupDetails {
2+
default_zonegroup: string;
3+
zonegroups: ZoneGroup[];
4+
}
5+
6+
export interface StorageClass {
7+
storage_class: string;
8+
endpoint: string;
9+
region: string;
10+
placement_target: string;
11+
}
12+
13+
export interface StorageClassDetails {
14+
target_path: string;
15+
access_key: string;
16+
secret: string;
17+
multipart_min_part_size: number;
18+
multipart_sync_threshold: number;
19+
host_style: string;
20+
}
21+
export interface ZoneGroup {
22+
name: string;
23+
placement_targets: Target[];
24+
}
25+
26+
export interface S3Details {
27+
endpoint: string;
28+
access_key: string;
29+
storage_class: string;
30+
target_path: string;
31+
target_storage_class: string;
32+
region: string;
33+
secret: string;
34+
multipart_min_part_size: number;
35+
multipart_sync_threshold: number;
36+
host_style: boolean;
37+
}
38+
39+
export interface TierTarget {
40+
val: {
41+
storage_class: string;
42+
tier_type: string;
43+
s3: S3Details;
44+
};
45+
}
46+
47+
export interface Target {
48+
name: string;
49+
tier_targets: TierTarget[];
50+
}
51+
52+
export const CLOUD_TIER = 'cloud-s3';
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<ng-container *ngIf="!!selection">
2+
<cds-tabs type="contained"
3+
theme="light">
4+
<cds-tab heading="Details">
5+
<table
6+
class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
7+
data-testid="rgw-storage-details"
8+
>
9+
<tbody>
10+
<tr>
11+
<td class="bold">
12+
Target Path
13+
<cd-helper class="text-pre-wrap">
14+
<span i18n>
15+
The target path specifies a prefix to which the source bucket-name/object-name is
16+
appended.
17+
</span>
18+
</cd-helper>
19+
</td>
20+
<td>{{ selection?.target_path }}</td>
21+
</tr>
22+
<tr>
23+
<td class="bold">
24+
Access key
25+
<cd-helper class="text-pre-wrap">
26+
<span i18n>
27+
Access key is the remote cloud S3 access key used for a specific connection.
28+
</span>
29+
</cd-helper>
30+
</td>
31+
<td>
32+
<div cdsCol
33+
[columnNumbers]="{ md: 4 }"
34+
class="d-flex">
35+
<input
36+
cdsPassword
37+
type="password"
38+
readonly
39+
id="access_key"
40+
[value]="selection?.access_key"
41+
/>
42+
<button type="button"
43+
class="btn btn-light"
44+
cdPasswordButton="access_key"></button>
45+
<cd-copy-2-clipboard-button source="access_key"> </cd-copy-2-clipboard-button>
46+
</div>
47+
</td>
48+
</tr>
49+
<tr>
50+
<td class="bold">
51+
Secret key
52+
<cd-helper class="text-pre-wrap">
53+
<span i18n> Secret is the secret key for the remote cloud S3 service. </span>
54+
</cd-helper>
55+
</td>
56+
<td>
57+
<div cdsCol
58+
[columnNumbers]="{ md: 4 }"
59+
class="d-flex">
60+
<input
61+
cdsPassword
62+
type="password"
63+
readonly
64+
id="secret"
65+
[value]="selection?.secret"
66+
/>
67+
<button type="button"
68+
class="btn btn-light"
69+
cdPasswordButton="secret"></button>
70+
<cd-copy-2-clipboard-button source="secret"> </cd-copy-2-clipboard-button>
71+
</div>
72+
</td>
73+
</tr>
74+
<tr>
75+
<td
76+
class="bold">
77+
Host Style
78+
<cd-helper class="text-pre-wrap">
79+
<span i18n>The URL format for accessing the remote S3 endpoint: 'Path' for a path-based URL
80+
or 'Virtual' for a domain-based URL.</span>
81+
</cd-helper>
82+
</td>
83+
<td>{{ selection?.host_style }}</td>
84+
</tr>
85+
<tr>
86+
<td
87+
class="bold">
88+
Multipart Minimum Part Size
89+
<cd-helper class="text-pre-wrap">
90+
<span i18n>
91+
Minimum parts size to use when transitioning objects using multipart upload.
92+
</span>
93+
</cd-helper>
94+
</td>
95+
<td>{{ selection?.multipart_min_part_size }}</td>
96+
</tr>
97+
<tr>
98+
<td
99+
class="bold">
100+
Multipart Sync Threshold
101+
<cd-helper class="text-pre-wrap">
102+
<span i18n>
103+
Objects this size or larger will be transitioned to the cloud using multipart
104+
upload.
105+
</span>
106+
</cd-helper>
107+
</td>
108+
<td>{{ selection?.multipart_sync_threshold }}</td>
109+
</tr>
110+
</tbody>
111+
</table>
112+
</cds-tab>
113+
</cds-tabs>
114+
</ng-container>

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.scss

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { RgwStorageClassDetailsComponent } from './rgw-storage-class-details.component';
4+
import { StorageClassDetails } from '../models/rgw-storage-class.model';
5+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
6+
import { SharedModule } from '~/app/shared/shared.module';
7+
import { HttpClientTestingModule } from '@angular/common/http/testing';
8+
import { RouterTestingModule } from '@angular/router/testing';
9+
10+
describe('RgwStorageClassDetailsComponent', () => {
11+
let component: RgwStorageClassDetailsComponent;
12+
let fixture: ComponentFixture<RgwStorageClassDetailsComponent>;
13+
14+
beforeEach(async () => {
15+
await TestBed.configureTestingModule({
16+
imports: [
17+
BrowserAnimationsModule,
18+
SharedModule,
19+
HttpClientTestingModule,
20+
RouterTestingModule
21+
],
22+
declarations: [RgwStorageClassDetailsComponent]
23+
}).compileComponents();
24+
25+
fixture = TestBed.createComponent(RgwStorageClassDetailsComponent);
26+
component = fixture.componentInstance;
27+
fixture.detectChanges();
28+
});
29+
30+
it('should create', () => {
31+
expect(component).toBeTruthy();
32+
});
33+
34+
it('should update storageDetails when selection input changes', () => {
35+
const mockSelection: StorageClassDetails = {
36+
access_key: 'TestAccessKey',
37+
secret: 'TestSecret',
38+
target_path: '/test/path',
39+
multipart_min_part_size: 100,
40+
multipart_sync_threshold: 200,
41+
host_style: 'path'
42+
};
43+
component.selection = mockSelection;
44+
component.ngOnChanges();
45+
expect(component.storageDetails).toEqual(mockSelection);
46+
});
47+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Component, Input, OnChanges } from '@angular/core';
2+
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
3+
import { StorageClassDetails } from '../models/rgw-storage-class.model';
4+
5+
@Component({
6+
selector: 'cd-rgw-storage-class-details',
7+
templateUrl: './rgw-storage-class-details.component.html',
8+
styleUrls: ['./rgw-storage-class-details.component.scss']
9+
})
10+
export class RgwStorageClassDetailsComponent implements OnChanges {
11+
@Input()
12+
selection: StorageClassDetails;
13+
columns: CdTableColumn[] = [];
14+
storageDetails: StorageClassDetails;
15+
16+
ngOnChanges() {
17+
if (this.selection) {
18+
this.storageDetails = {
19+
access_key: this.selection.access_key,
20+
secret: this.selection.secret,
21+
target_path: this.selection.target_path,
22+
multipart_min_part_size: this.selection.multipart_min_part_size,
23+
multipart_sync_threshold: this.selection.multipart_sync_threshold,
24+
host_style: this.selection.host_style
25+
};
26+
}
27+
}
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<cd-table
2+
[data]="storageClassList"
3+
columnMode="flex"
4+
[columns]="columns"
5+
(fetchData)="loadStorageClass()"
6+
selectionType="single"
7+
[hasDetails]="true"
8+
(setExpandedRow)="setExpandedRow($event)"
9+
(updateSelection)="updateSelection($event)"
10+
>
11+
<cd-rgw-storage-class-details *cdTableDetail
12+
[selection]="expandedRow">
13+
</cd-rgw-storage-class-details>
14+
</cd-table>

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.scss

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { RgwStorageClassListComponent } from './rgw-storage-class-list.component';
4+
import { HttpClientTestingModule } from '@angular/common/http/testing';
5+
6+
describe('RgwStorageClassListComponent', () => {
7+
let component: RgwStorageClassListComponent;
8+
let fixture: ComponentFixture<RgwStorageClassListComponent>;
9+
10+
beforeEach(async () => {
11+
await TestBed.configureTestingModule({
12+
imports: [HttpClientTestingModule],
13+
declarations: [RgwStorageClassListComponent]
14+
}).compileComponents();
15+
16+
fixture = TestBed.createComponent(RgwStorageClassListComponent);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
});
20+
21+
it('should create', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { CdTableAction } from '~/app/shared/models/cd-table-action';
3+
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
4+
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
5+
6+
import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
7+
import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
8+
import {
9+
StorageClass,
10+
CLOUD_TIER,
11+
ZoneGroup,
12+
TierTarget,
13+
Target,
14+
ZoneGroupDetails
15+
} from '../models/rgw-storage-class.model';
16+
17+
@Component({
18+
selector: 'cd-rgw-storage-class-list',
19+
templateUrl: './rgw-storage-class-list.component.html',
20+
styleUrls: ['./rgw-storage-class-list.component.scss']
21+
})
22+
export class RgwStorageClassListComponent extends ListWithDetails implements OnInit {
23+
columns: CdTableColumn[];
24+
selection = new CdTableSelection();
25+
tableActions: CdTableAction[];
26+
storageClassList: StorageClass[] = [];
27+
28+
constructor(private rgwZonegroupService: RgwZonegroupService) {
29+
super();
30+
}
31+
32+
ngOnInit() {
33+
this.columns = [
34+
{
35+
name: $localize`Zone Group`,
36+
prop: 'zonegroup_name',
37+
flexGrow: 2
38+
},
39+
{
40+
name: $localize`Placement Target`,
41+
prop: 'placement_target',
42+
flexGrow: 2
43+
},
44+
{
45+
name: $localize`Storage Class`,
46+
prop: 'storage_class',
47+
flexGrow: 2
48+
},
49+
{
50+
name: $localize`Target Region`,
51+
prop: 'region',
52+
flexGrow: 2
53+
},
54+
{
55+
name: $localize`Target Endpoint`,
56+
prop: 'endpoint',
57+
flexGrow: 2
58+
}
59+
];
60+
}
61+
62+
loadStorageClass(): Promise<void> {
63+
return new Promise((resolve, reject) => {
64+
this.rgwZonegroupService.getAllZonegroupsInfo().subscribe(
65+
(data: ZoneGroupDetails) => {
66+
this.storageClassList = [];
67+
68+
const tierObj = data.zonegroups.flatMap((zoneGroup: ZoneGroup) =>
69+
zoneGroup.placement_targets
70+
.filter((target: Target) => target.tier_targets)
71+
.flatMap((target: Target) =>
72+
target.tier_targets
73+
.filter((tierTarget: TierTarget) => tierTarget.val.tier_type === CLOUD_TIER)
74+
.map((tierTarget: TierTarget) => {
75+
return this.getTierTargets(tierTarget, zoneGroup.name, target.name);
76+
})
77+
)
78+
);
79+
this.storageClassList.push(...tierObj);
80+
resolve();
81+
},
82+
(error) => {
83+
reject(error);
84+
}
85+
);
86+
});
87+
}
88+
89+
getTierTargets(tierTarget: TierTarget, zoneGroup: string, targetName: string) {
90+
if (tierTarget.val.tier_type !== CLOUD_TIER) return null;
91+
return {
92+
zonegroup_name: zoneGroup,
93+
placement_target: targetName,
94+
storage_class: tierTarget.val.storage_class,
95+
...tierTarget.val.s3
96+
};
97+
}
98+
99+
updateSelection(selection: CdTableSelection) {
100+
this.selection = selection;
101+
}
102+
103+
setExpandedRow(expandedRow: any) {
104+
super.setExpandedRow(expandedRow);
105+
}
106+
}

0 commit comments

Comments
 (0)