Skip to content

Commit 9bc2c79

Browse files
authored
Merge pull request ceph#54305 from rhcs-dashboard/add-tags
mgr/dashboard: add tags to edit bucket Reviewed-by: Avan Thakkar <[email protected]> Reviewed-by: Ankush Behl <[email protected]> Reviewed-by: Nizamudeen A <[email protected]>
2 parents a42e286 + 9481b7e commit 9bc2c79

File tree

13 files changed

+347
-12
lines changed

13 files changed

+347
-12
lines changed

src/pybind/mgr/dashboard/controllers/rgw.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ def _get_policy(self, bucket: str):
290290
rgw_client = RgwClient.admin_instance()
291291
return rgw_client.get_bucket_policy(bucket)
292292

293+
def _set_tags(self, bucket_name, tags, daemon_name, owner):
294+
rgw_client = RgwClient.instance(owner, daemon_name)
295+
return rgw_client.set_tags(bucket_name, tags)
296+
293297
@staticmethod
294298
def strip_tenant_from_bucket_name(bucket_name):
295299
# type (str) -> str
@@ -355,7 +359,7 @@ def create(self, bucket, uid, zonegroup=None, placement_target=None,
355359
lock_enabled='false', lock_mode=None,
356360
lock_retention_period_days=None,
357361
lock_retention_period_years=None, encryption_state='false',
358-
encryption_type=None, key_id=None, daemon_name=None):
362+
encryption_type=None, key_id=None, tags=None, daemon_name=None):
359363
lock_enabled = str_to_bool(lock_enabled)
360364
encryption_state = str_to_bool(encryption_state)
361365
try:
@@ -371,6 +375,9 @@ def create(self, bucket, uid, zonegroup=None, placement_target=None,
371375
if encryption_state:
372376
self._set_encryption(bucket, encryption_type, key_id, daemon_name, uid)
373377

378+
if tags:
379+
self._set_tags(bucket, tags, daemon_name, uid)
380+
374381
return result
375382
except RequestException as e: # pragma: no cover - handling is too obvious
376383
raise DashboardException(e, http_status_code=500, component='rgw')
@@ -380,7 +387,7 @@ def set(self, bucket, bucket_id, uid, versioning_state=None,
380387
encryption_state='false', encryption_type=None, key_id=None,
381388
mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
382389
lock_mode=None, lock_retention_period_days=None,
383-
lock_retention_period_years=None, daemon_name=None):
390+
lock_retention_period_years=None, tags=None, daemon_name=None):
384391
encryption_state = str_to_bool(encryption_state)
385392
# When linking a non-tenant-user owned bucket to a tenanted user, we
386393
# need to prefix bucket name with '/'. e.g. photos -> /photos
@@ -420,6 +427,8 @@ def set(self, bucket, bucket_id, uid, versioning_state=None,
420427
self._set_encryption(bucket_name, encryption_type, key_id, daemon_name, uid)
421428
if encryption_status['Status'] == 'Enabled' and (not encryption_state):
422429
self._delete_encryption(bucket_name, daemon_name, uid)
430+
if tags:
431+
self._set_tags(bucket_name, tags, daemon_name, uid)
423432
return self._append_bid(result)
424433

425434
def delete(self, bucket, purge_objects='true', daemon_name=None):
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<cd-modal [modalRef]="activeModal">
2+
<span class="modal-title"
3+
i18n>{{ getMode() }} Tag</span>
4+
5+
<ng-container class="modal-content">
6+
<form class="form"
7+
#formDir="ngForm"
8+
[formGroup]="form">
9+
<div class="modal-body">
10+
<!-- Key -->
11+
<div class="form-group row">
12+
<label class="cd-col-form-label required"
13+
for="key"
14+
i18n>Key</label>
15+
<div class="cd-col-form-input">
16+
<input type="text"
17+
class="form-control"
18+
formControlName="key"
19+
id="key">
20+
<span class="invalid-feedback"
21+
*ngIf="form.showError('key', formDir, 'required')"
22+
i18n>This field is required.</span>
23+
<span class="invalid-feedback"
24+
*ngIf="form.showError('key', formDir, 'unique')"
25+
i18n>This key must be unique.</span>
26+
<span class="invalid-feedback"
27+
*ngIf="form.showError('key', formDir, 'maxLength')"
28+
i18n>Length of the key must be maximum of 128 characters</span>
29+
</div>
30+
</div>
31+
32+
<!-- Value -->
33+
<div class="form-group row">
34+
<label class="cd-col-form-label required"
35+
for="value"
36+
i18n>Value</label>
37+
<div class="cd-col-form-input">
38+
<input id="value"
39+
class="form-control"
40+
type="text"
41+
formControlName="value">
42+
<span *ngIf="form.showError('value', formDir, 'required')"
43+
class="invalid-feedback"
44+
i18n>This field is required.</span>
45+
<span class="invalid-feedback"
46+
*ngIf="form.showError('value', formDir, 'maxLength')"
47+
i18n>Length of the value must be a maximum of 128 characters</span>
48+
</div>
49+
</div>
50+
</div>
51+
52+
<div class="modal-footer">
53+
<cd-form-button-panel (submitActionEvent)="onSubmit()"
54+
[form]="form"
55+
[submitText]="getMode()"></cd-form-button-panel>
56+
</div>
57+
</form>
58+
</ng-container>
59+
</cd-modal>

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/bucket-tag-modal/bucket-tag-modal.component.scss

Whitespace-only changes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { BucketTagModalComponent } from './bucket-tag-modal.component';
4+
import { HttpClientTestingModule } from '@angular/common/http/testing';
5+
import { ReactiveFormsModule } from '@angular/forms';
6+
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
7+
8+
describe('BucketTagModalComponent', () => {
9+
let component: BucketTagModalComponent;
10+
let fixture: ComponentFixture<BucketTagModalComponent>;
11+
12+
beforeEach(async () => {
13+
await TestBed.configureTestingModule({
14+
declarations: [BucketTagModalComponent],
15+
imports: [HttpClientTestingModule, ReactiveFormsModule],
16+
providers: [NgbActiveModal]
17+
}).compileComponents();
18+
19+
fixture = TestBed.createComponent(BucketTagModalComponent);
20+
component = fixture.componentInstance;
21+
fixture.detectChanges();
22+
});
23+
24+
it('should create', () => {
25+
expect(component).toBeTruthy();
26+
});
27+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Component, EventEmitter, Output } from '@angular/core';
2+
import { Validators } from '@angular/forms';
3+
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
4+
import _ from 'lodash';
5+
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
6+
import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
7+
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
8+
import { CdValidators } from '~/app/shared/forms/cd-validators';
9+
10+
@Component({
11+
selector: 'cd-bucket-tag-modal',
12+
templateUrl: './bucket-tag-modal.component.html',
13+
styleUrls: ['./bucket-tag-modal.component.scss']
14+
})
15+
export class BucketTagModalComponent {
16+
@Output()
17+
submitAction = new EventEmitter();
18+
19+
form: CdFormGroup;
20+
editMode = false;
21+
currentKeyTags: string[];
22+
storedKey: string;
23+
24+
constructor(
25+
private formBuilder: CdFormBuilder,
26+
public activeModal: NgbActiveModal,
27+
public actionLabels: ActionLabelsI18n
28+
) {
29+
this.createForm();
30+
}
31+
32+
private createForm() {
33+
this.form = this.formBuilder.group({
34+
key: [
35+
null,
36+
[
37+
Validators.required,
38+
CdValidators.custom('unique', (value: string) => {
39+
if (_.isEmpty(value) && !this.currentKeyTags) {
40+
return false;
41+
}
42+
return this.storedKey !== value && this.currentKeyTags.includes(value);
43+
}),
44+
CdValidators.custom('maxLength', (value: string) => {
45+
if (_.isEmpty(value)) return false;
46+
return value.length > 128;
47+
})
48+
]
49+
],
50+
value: [
51+
null,
52+
[
53+
Validators.required,
54+
CdValidators.custom('maxLength', (value: string) => {
55+
if (_.isEmpty(value)) return false;
56+
return value.length > 128;
57+
})
58+
]
59+
]
60+
});
61+
}
62+
63+
onSubmit() {
64+
this.submitAction.emit(this.form.value);
65+
this.activeModal.close();
66+
}
67+
68+
getMode() {
69+
return this.editMode ? this.actionLabels.EDIT : this.actionLabels.ADD;
70+
}
71+
72+
fillForm(tag: Record<string, string>) {
73+
this.form.setValue(tag);
74+
}
75+
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@
100100
</ng-container>
101101
</tbody>
102102
</table>
103+
104+
<!-- Tags -->
105+
<ng-container *ngIf="selection.tagset">
106+
<legend i18n>Tags</legend>
107+
<table class="table table-striped table-bordered">
108+
<tbody>
109+
<tr *ngFor="let tag of selection.tagset | keyvalue">
110+
<td i18n
111+
class="bold w-25">{{tag.key}}</td>
112+
<td class="w-75">{{ tag.value }}</td>
113+
</tr>
114+
</tbody>
115+
</table>
116+
</ng-container>
117+
103118
</ng-template>
104119
</ng-container>
105120

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,32 @@
385385
</div>
386386
</fieldset>
387387

388+
<!-- Tags -->
389+
<legend class="cd-header"
390+
i18n>Tags
391+
<cd-helper>Tagging gives you a way to categorize storage</cd-helper>
392+
</legend>
393+
<span *ngFor="let tag of tags; let i=index;">
394+
<ng-container *ngTemplateOutlet="tagTpl; context:{index: i, tag: tag}"></ng-container>
395+
</span>
396+
397+
<div class="row">
398+
<div class="col-12">
399+
<strong *ngIf="tags.length > 19"
400+
class="text-warning"
401+
i18n>Maximum of 20 tags reached</strong>
402+
<button type="button"
403+
id="add-tag"
404+
class="btn btn-light float-end my-3"
405+
[disabled]="tags.length > 19"
406+
(click)="showTagModal()">
407+
<i [ngClass]="[icons.add]"></i>
408+
<ng-container i18n>Add tag</ng-container>
409+
</button>
410+
</div>
411+
</div>
412+
413+
388414
</div>
389415
<div class="card-footer">
390416
<cd-form-button-panel (submitActionEvent)="submit()"
@@ -395,3 +421,37 @@
395421
</div>
396422
</form>
397423
</div>
424+
425+
<ng-template #tagTpl
426+
let-tag="tag"
427+
let-index="index">
428+
<div class="input-group my-2">
429+
<ng-container *ngFor="let config of tagConfig">
430+
<input type="text"
431+
id="tag-{{config.attribute}}-{{index}}"
432+
class="form-control"
433+
[ngbTooltip]="config.attribute"
434+
[value]="tag[config.attribute]"
435+
disabled
436+
readonly>
437+
</ng-container>
438+
439+
<!-- Tag actions -->
440+
<button type="button"
441+
class="btn btn-light"
442+
id="tag-edit-{{index}}"
443+
i18n-ngbTooltip
444+
ngbTooltip="Edit"
445+
(click)="showTagModal(index)">
446+
<i [ngClass]="[icons.edit]"></i>
447+
</button>
448+
<button type="button"
449+
class="btn btn-light"
450+
id="tag-delete-{{index}}"
451+
i18n-ngbTooltip
452+
ngbTooltip="Delete"
453+
(click)="deleteTag(index)">
454+
<i [ngClass]="[icons.trash]"></i>
455+
</button>
456+
</div>
457+
</ng-template>

0 commit comments

Comments
 (0)