Skip to content

Commit 1e00acf

Browse files
authored
Merge pull request ceph#65735 from rhcs-dashboard/input-string-list
mgr/dashboard: add text-label-list component Reviewed-by: Afreen Misbah <[email protected]> Reviewed-by: Nizamudeen A <[email protected]>
2 parents 116a414 + 55efded commit 1e00acf

File tree

8 files changed

+226
-21
lines changed

8 files changed

+226
-21
lines changed

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import { MultiClusterFormComponent } from './multi-cluster/multi-cluster-form/mu
8484
import { MultiClusterListComponent } from './multi-cluster/multi-cluster-list/multi-cluster-list.component';
8585
import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
8686
import { MultiClusterDetailsComponent } from './multi-cluster/multi-cluster-details/multi-cluster-details.component';
87+
import { TextLabelListComponent } from '~/app/shared/components/text-label-list/text-label-list.component';
8788

8889
@NgModule({
8990
imports: [
@@ -117,7 +118,8 @@ import { MultiClusterDetailsComponent } from './multi-cluster/multi-cluster-deta
117118
ListModule,
118119
ToggletipModule,
119120
IconModule,
120-
TagModule
121+
TagModule,
122+
TextLabelListComponent
121123
],
122124
declarations: [
123125
MonitorComponent,

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -556,24 +556,12 @@
556556
</div>
557557
</div>
558558

559-
<div class="form-group row">
560-
<label class="cd-col-form-label"
561-
for="custom_dns">
562-
<span i18n>Custom DNS</span>
563-
<cd-helper i18n>
564-
<span>Comma separated list of DNSs.</span>
565-
<br>
566-
<span>A list of IP addresses that will be used as the DNS servers for a Samba container.</span>
567-
</cd-helper>
568-
</label>
569-
<div class="cd-col-form-input">
570-
<input id="custom_dns"
571-
class="form-control"
572-
type="text"
573-
formControlName="custom_dns"
574-
placeholder="192.168.76.204"
575-
i18n-placeholder>
576-
</div>
559+
<div class="form-item">
560+
<cd-text-label-list formControlName="custom_dns"
561+
label="Custom DNS"
562+
helperText="IP addresses that will be used as the DNS servers for a Samba container."
563+
placeholder="192.168.76.204">
564+
</cd-text-label-list>
577565
</div>
578566

579567
<div class="form-group row">

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,10 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
539539
unmanaged: false,
540540
service_id: 'foo',
541541
cluster_id: 'cluster_foo',
542-
config_uri: 'rados://.smb/foo/scc.toml'
542+
config_uri: 'rados://.smb/foo/scc.toml',
543+
custom_dns: null,
544+
join_sources: undefined,
545+
user_sources: undefined
543546
});
544547
});
545548
});

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
12121212
(serviceSpec['features'] = serviceSpec['features'] || []).push(feature);
12131213
}
12141214
}
1215-
serviceSpec['custom_dns'] = values['custom_dns']?.trim();
1215+
serviceSpec['custom_dns'] = values['custom_dns'];
12161216
serviceSpec['join_sources'] = values['join_sources']?.trim();
12171217
serviceSpec['user_sources'] = values['user_sources']?.trim();
12181218
serviceSpec['include_ceph_users'] = values['include_ceph_users']?.trim();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@for (value of values; track $index) {
2+
<cds-text-label [helperText]="helperText && $index === values.length - 1 ? helperText : null"
3+
i18n-helperText
4+
i18n>
5+
<div class="cds-input-group">
6+
<input cdsText
7+
type="text"
8+
[placeholder]="placeholder"
9+
[value]="value"
10+
(input)="onInputChange($index, $event.target.value)" />
11+
@if ($index > 0 ) {
12+
<cds-icon-button kind="ghost"
13+
(click)="deleteInput($index)"
14+
size="md">
15+
<cd-icon type="trash"></cd-icon>
16+
</cds-icon-button>
17+
}
18+
</div>
19+
{{$index === 0 ? label : ''}}
20+
</cds-text-label>
21+
}

src/pybind/mgr/dashboard/frontend/src/app/shared/components/text-label-list/text-label-list.component.scss

Whitespace-only changes.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { TextLabelListComponent } from './text-label-list.component';
3+
import { By } from '@angular/platform-browser';
4+
5+
describe('TextLabelListComponent', () => {
6+
let component: TextLabelListComponent;
7+
let fixture: ComponentFixture<TextLabelListComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [TextLabelListComponent]
12+
}).compileComponents();
13+
14+
fixture = TestBed.createComponent(TextLabelListComponent);
15+
component = fixture.componentInstance;
16+
component.registerOnChange(jasmine.createSpy('onChange'));
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
24+
it('should call writeValue and render values', () => {
25+
component.writeValue(['foo', 'bar']);
26+
fixture.detectChanges();
27+
28+
const inputs = fixture.debugElement.queryAll(By.css('cds-text-label input'));
29+
expect(inputs.length).toBe(3);
30+
expect(inputs[0].nativeElement.value).toBe('foo');
31+
expect(inputs[1].nativeElement.value).toBe('bar');
32+
expect(inputs[2].nativeElement.value).toBe('');
33+
});
34+
35+
it('should call writeValue empty and render one', () => {
36+
component.writeValue([]);
37+
fixture.detectChanges();
38+
39+
const inputs = fixture.debugElement.queryAll(By.css('cds-text-label input'));
40+
expect(inputs.length).toBe(1);
41+
expect(inputs[0].nativeElement.value).toBe('');
42+
});
43+
44+
it('should call onTouch on input changes', () => {
45+
spyOn(component as any, 'onTouched');
46+
47+
component.onInputChange(0, 'foo');
48+
49+
expect((component as any).onTouched).toHaveBeenCalled();
50+
});
51+
52+
it('should update the value at the given index', () => {
53+
component.writeValue(['foo', '']);
54+
component.onInputChange(2, 'bar');
55+
56+
expect(component.values[2]).toBe('bar');
57+
});
58+
59+
it('should return non empty values on input changes', () => {
60+
component.writeValue(['foo', 'bar', '']);
61+
component.onInputChange(3, 'test');
62+
fixture.detectChanges();
63+
64+
const inputs = fixture.debugElement.queryAll(By.css('cds-text-label input'));
65+
expect(inputs.length).toBe(5);
66+
expect((component as any).onChange).toHaveBeenCalledWith(['foo', 'bar', 'test']);
67+
});
68+
69+
it('should remove the item at the given index', () => {
70+
component.writeValue(['foo', 'bar']);
71+
component.deleteInput(0);
72+
73+
expect(component['values']).toEqual(['bar', '']);
74+
expect(component['onChange']).toHaveBeenCalledWith(['bar']);
75+
});
76+
77+
it('should add an empty input if all items are deleted', () => {
78+
component.writeValue(['foo']);
79+
component.deleteInput(0);
80+
81+
expect(component['values']).toEqual(['']);
82+
expect(component['onChange']).toHaveBeenCalledWith([]);
83+
});
84+
85+
it('should update values correctly on deletion', () => {
86+
component.writeValue(['foo', 'bar', 'test']);
87+
component.deleteInput(1);
88+
89+
expect(component['values']).toEqual(['foo', 'test', '']);
90+
expect(component['onChange']).toHaveBeenCalledWith(['foo', 'test']);
91+
});
92+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { Component, Input } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
4+
import { ButtonModule, GridModule, IconModule, InputModule } from 'carbon-components-angular';
5+
import { ComponentsModule } from '../components.module';
6+
7+
@Component({
8+
selector: 'cd-text-label-list',
9+
standalone: true,
10+
imports: [
11+
CommonModule,
12+
ReactiveFormsModule,
13+
InputModule,
14+
IconModule,
15+
ButtonModule,
16+
GridModule,
17+
ComponentsModule
18+
],
19+
templateUrl: './text-label-list.component.html',
20+
styleUrl: './text-label-list.component.scss',
21+
providers: [
22+
{
23+
provide: NG_VALUE_ACCESSOR,
24+
useExisting: TextLabelListComponent,
25+
multi: true
26+
}
27+
]
28+
})
29+
export class TextLabelListComponent implements ControlValueAccessor {
30+
// Label for the text list.
31+
@Input() label: string = '';
32+
// Optional helper text that appears under the label.
33+
@Input() helperText: string = '';
34+
// Value displayed if no item is present.
35+
@Input() placeholder: string = '';
36+
37+
values: string[] = [''];
38+
disabled: boolean = false;
39+
touched: boolean = false;
40+
41+
private onChange: (value: string[]) => void = () => {};
42+
private onTouched: () => void = () => {};
43+
44+
private _isRemovingChar(originalValue: string, value: string): boolean {
45+
return originalValue.slice(0, -1) === value;
46+
}
47+
48+
writeValue(value: string[]): void {
49+
this.values = value?.length ? [...value, ''] : [''];
50+
}
51+
52+
registerOnChange(onChange: (value: string[]) => void): void {
53+
this.onChange = onChange;
54+
}
55+
56+
registerOnTouched(onTouched: () => void): void {
57+
this.onTouched = onTouched;
58+
}
59+
60+
setDisabledState(disabled: boolean): void {
61+
this.disabled = disabled;
62+
}
63+
64+
onInputChange(index: number, value: string) {
65+
const originalValue = this.values[index];
66+
this.values[index] = value;
67+
68+
if (
69+
!this._isRemovingChar(originalValue, value) &&
70+
index === this.values.length - 1 &&
71+
value.trim() !== ''
72+
) {
73+
this.values.push('');
74+
}
75+
76+
const nonEmptyValues = this.values.filter((v) => v.trim() !== '');
77+
this.onChange(nonEmptyValues.length ? nonEmptyValues : []);
78+
79+
this.markAsTouched();
80+
}
81+
82+
markAsTouched() {
83+
if (!this.touched) {
84+
this.onTouched();
85+
this.touched = true;
86+
}
87+
}
88+
89+
deleteInput(index: number) {
90+
this.values.splice(index, 1);
91+
92+
if (this.values.length === 0) {
93+
this.values.push('');
94+
}
95+
96+
const nonEmpty = this.values.filter((v) => v.trim() !== '');
97+
this.onChange(nonEmpty.length ? nonEmpty : []);
98+
}
99+
}

0 commit comments

Comments
 (0)