-<cd-modal [modalRef]="activeModal">
- <ng-container
- i18n="form title"
- class="modal-title"
- >Import Multi-site Token</ng-container
- >
+<cds-modal
+ size="lg"
+ [open]="open"
+ [hasScrollingContent]="true"
+ (overlaySelected)="closeModal()"
+>
+ <cds-modal-header (closeSelect)="closeModal()">
+ <h3
+ cdsModalHeaderHeading
+ i18n
+ >
+ Import multi-site tokens
+ </h3>
+ </cds-modal-header>
- <ng-container class="modal-content">
+ <div
+ cdsModalContent
+ [hasForm]="true"
+ >
<form
name="importTokenForm"
#frm="ngForm"
[formGroup]="importTokenForm"
+ novalidate
>
- <div class="modal-body">
- <cd-alert-panel
- type="info"
- spacingClass="mb-3"
- >
+ <div class="form-item">
+ <cd-alert-panel type="info">
<ul>
- <li>
+ <li i18n>
This feature allows you to configure a connection between your primary and secondary
Ceph clusters for data replication. By importing a token, you establish a link between
the clusters, enabling data synchronization.
</li>
- <li>
+ <li i18n>
To obtain the token, generate it from your primary Ceph cluster. This token includes
encoded information about the primary cluster's endpoint, access key, and secret key.
</li>
- <li>
+ <li i18n>
The secondary zone represents the destination cluster where your data will be
replicated.
</li>
</ul>
</cd-alert-panel>
- <legend i18n>Zone Details</legend>
- <div class="form-group row">
- <label
- class="cd-col-form-label required"
- for="realmToken"
- i18n
- >Token
- </label>
- <div class="cd-col-form-input">
- <input
- id="realmToken"
- name="realmToken"
- class="form-control"
- type="text"
- formControlName="realmToken"
- />
- <span
- class="invalid-feedback"
- *ngIf="importTokenForm.showError('realmToken', frm, 'required')"
- i18n
- >This field is required.</span
- >
- </div>
- </div>
- <div class="form-group row">
- <label
- class="cd-col-form-label required"
- for="zoneName"
- i18n
- >Secondary Zone Name</label
+ </div>
+ <legend
+ class="cd-header"
+ i18n
+ >
+ Zone Details
+ </legend>
+ <div class="form-item">
+ <cds-text-label
+ labelInputID="realmToken"
+ [invalid]="realmTokenRef.isInvalid"
+ [invalidText]="realmTokenError"
+ cdRequiredField="Token"
+ i18n
+ >Token
+ <input
+ cdsText
+ cdValidate
+ #realmTokenRef="cdValidate"
+ type="text"
+ id="realmToken"
+ name="realmToken"
+ formControlName="realmToken"
+ modal-primary-focus
+ [invalid]="realmTokenRef.isInvalid"
+ />
+ </cds-text-label>
+ <ng-template #realmTokenError>
+ <ng-container
+ *ngTemplateOutlet="
+ validationErrors;
+ context: { control: importTokenForm.get('realmToken') }
+ "
+ ></ng-container>
+ </ng-template>
+ </div>
+
+ <div class="form-item">
+ <cds-text-label
+ labelInputID="zoneName"
+ [invalid]="zoneNameRef.isInvalid"
+ [invalidText]="zoneNameError"
+ cdRequiredField="Secondary Zone Name"
+ i18n
+ >Secondary Zone Name
+ <input
+ cdsText
+ cdValidate
+ #zoneNameRef="cdValidate"
+ type="text"
+ placeholder="Zone name..."
+ id="zoneName"
+ name="zoneName"
+ formControlName="zoneName"
+ [invalid]="zoneNameRef.isInvalid"
+ />
+ </cds-text-label>
+ <ng-template #zoneNameError>
+ <ng-container
+ *ngTemplateOutlet="
+ validationErrors;
+ context: { control: importTokenForm.get('zoneName') }
+ "
+ ></ng-container>
+ </ng-template>
+ </div>
+
+ <div class="form-item">
+ <cds-checkbox
+ id="archive_zone"
+ formControlName="archive_zone"
+ >
+ <span i18n>Archive</span>
+ <cd-help-text i18n
+ >Enable archival storage to keep all object versions and protect data from deletion or
+ corruption.</cd-help-text
>
- <div class="cd-col-form-input">
- <input
- class="form-control"
- type="text"
- placeholder="Zone name..."
- id="zoneName"
- name="zoneName"
- formControlName="zoneName"
- />
- <span
- class="invalid-feedback"
- *ngIf="importTokenForm.showError('zoneName', frm, 'required')"
- i18n
- >This field is required.</span
- >
- <span
- class="invalid-feedback"
- *ngIf="importTokenForm.showError('zoneName', frm, 'uniqueName')"
- i18n
- >The chosen zone name is already in use.</span
- >
- </div>
- </div>
- <div class="form-group row">
- <div class="cd-col-form-offset">
- <div class="custom-control custom-checkbox">
- <input
- class="custom-control-input"
- id="archive_zone"
- type="checkbox"
- formControlName="archive_zone"
- />
- <label
- class="custom-control-label cds-ml-2"
- for="archive_zone"
- i18n
- >Archive</label
- >
- <cd-helper i18n>
- <span
- >Enable archival storage to keep all object versions and protect data from
- deletion or corruption.</span
- >
- </cd-helper>
- </div>
- </div>
- </div>
- <legend i18n>Service Details</legend>
- <div class="form-group row">
- <div class="cd-col-form-offset">
- <div class="custom-control custom-checkbox">
- <input
- class="custom-control-input"
- id="unmanaged"
- type="checkbox"
- formControlName="unmanaged"
- />
- <label
- class="custom-control-label cds-ml-2"
- for="unmanaged"
- i18n
- >Unmanaged</label
- >
- <cd-helper i18n
- >If set to true, the orchestrator will not start nor stop any daemon associated with
- this service. Placement and all other properties will be ignored.</cd-helper
- >
- </div>
- </div>
- </div>
+ </cds-checkbox>
+ </div>
- <!-- Placement -->
- <div
- *ngIf="!importTokenForm.controls.unmanaged.value"
- class="form-group row"
+ <legend
+ class="cd-header"
+ i18n
+ >
+ Service Details
+ </legend>
+ <div class="form-item">
+ <cds-checkbox
+ id="unmanaged"
+ formControlName="unmanaged"
>
- <label
- class="cd-col-form-label"
- for="placement"
- i18n
- >Placement</label
+ <span i18n>Unmanaged</span>
+ <cd-help-text i18n
+ >If set to true, the orchestrator will not start nor stop any daemon associated with
+ this service. Placement and all other properties will be ignored.</cd-help-text
>
- <div class="cd-col-form-input">
- <select
- id="placement"
- class="form-select"
+ </cds-checkbox>
+ </div>
+
+ <!-- Placement -->
+ @if (!importTokenForm.controls.unmanaged.value) {
+ <div
+ class="form-item"
+ cdsRow
+ >
+ <div cdsCol>
+ <cds-select
formControlName="placement"
+ label="Placement"
+ i18n-label
+ id="placement"
>
<option
i18n
>
Label
</option>
- </select>
+ </cds-select>
</div>
</div>
+ }
- <!-- Label -->
+ <!-- Label -->
+ @if (
+ !importTokenForm.controls.unmanaged.value &&
+ importTokenForm.controls.placement.value === 'label'
+ ) {
<div
- *ngIf="
- !importTokenForm.controls.unmanaged.value &&
- importTokenForm.controls.placement.value === 'label'
- "
- class="form-group row"
+ class="form-item"
+ cdsRow
>
- <label
- i18n
- class="cd-col-form-label"
- for="label"
- >Label</label
- >
- <div class="cd-col-form-input">
- <input
- id="label"
- class="form-control"
- type="text"
+ <div cdsCol>
+ <cds-combo-box
+ label="Label"
+ type="single"
formControlName="label"
- [ngbTypeahead]="searchLabels"
- (focus)="labelFocus.next($any($event).target.value)"
- (click)="labelClick.next($any($event).target.value)"
- />
- <span
- class="invalid-feedback"
- *ngIf="importTokenForm.showError('label', frm, 'required')"
- i18n
- >This field is required.</span
+ i18n-label
+ [items]="labels"
+ [appendInline]="true"
+ id="label"
+ [invalid]="importTokenForm.showError('label', frm)"
+ [invalidText]="labelError"
+ selectionFeedback="top-after-reopen"
+ placeholder="Select label..."
+ i18n-placeholder
>
+ <cds-dropdown-list></cds-dropdown-list>
+ </cds-combo-box>
+ <ng-template #labelError>
+ <ng-container
+ *ngTemplateOutlet="
+ validationErrors;
+ context: { control: importTokenForm.get('label') }
+ "
+ ></ng-container>
+ </ng-template>
</div>
</div>
+ }
- <!-- Hosts -->
+ <!-- Hosts -->
+ @if (
+ !importTokenForm.controls.unmanaged.value &&
+ importTokenForm.getValue('placement') === 'hosts'
+ ) {
<div
- *ngIf="
- !importTokenForm.controls.unmanaged.value &&
- importTokenForm.controls.placement.value === 'hosts'
- "
- class="form-group row"
+ class="form-item"
+ cdsRow
>
- <label
- class="cd-col-form-label"
- for="hosts"
- i18n
- >Hosts</label
- >
- <div class="cd-col-form-input">
- <cd-select-badges
+ <div cdsCol>
+ <cds-combo-box
+ label="Hosts"
+ type="multi"
+ formControlName="hosts"
id="hosts"
- [data]="importTokenForm.controls.hosts.value"
- [options]="hosts.options"
- [messages]="hosts.messages"
+ name="hosts"
+ [items]="hosts"
+ [appendInline]="true"
+ selectionFeedback="top-after-reopen"
+ (selected)="multiSelector($event)"
+ placeholder="Select hosts..."
+ i18n-placeholder
>
- </cd-select-badges>
+ <cds-dropdown-list></cds-dropdown-list>
+ </cds-combo-box>
</div>
</div>
+ }
- <!-- count -->
+ <!-- count -->
+ @if (!importTokenForm.controls.unmanaged.value) {
<div
- *ngIf="!importTokenForm.controls.unmanaged.value"
- class="form-group row"
+ class="form-item"
+ cdsRow
>
- <label
- class="cd-col-form-label"
- for="count"
- >
- <span i18n>Count</span>
- <cd-helper i18n>Only that number of daemons will be created.</cd-helper>
- </label>
- <div class="cd-col-form-input">
- <input
- id="count"
- class="form-control"
- type="number"
+ <div cdsCol>
+ <cds-number
+ cdValidate
+ #countRef="cdValidate"
formControlName="count"
- min="1"
- />
- <span
- class="invalid-feedback"
- *ngIf="importTokenForm.showError('count', frm, 'min')"
- i18n
- >The value must be at least 1.</span
- >
- <span
- class="invalid-feedback"
- *ngIf="importTokenForm.showError('count', frm, 'pattern')"
- i18n
- >The entered value needs to be a number.</span
- >
+ label="Count"
+ i18n-label
+ [min]="1"
+ [invalid]="countRef.isInvalid"
+ [invalidText]="countError"
+ [helperText]="countHelper"
+ ></cds-number>
+ <ng-template #countHelper>
+ <span i18n>Only that number of daemons will be created.</span>
+ </ng-template>
+ <ng-template #countError>
+ <ng-container
+ *ngTemplateOutlet="
+ validationErrors;
+ context: { control: importTokenForm.get('count') }
+ "
+ ></ng-container>
+ </ng-template>
</div>
</div>
+ }
- <!-- RGW -->
- <ng-container *ngIf="!importTokenForm.controls.unmanaged.value">
- <!-- rgw_frontend_port -->
- <div class="form-group row">
- <label
- i18n
- class="cd-col-form-label"
- for="rgw_frontend_port"
- >Port</label
- >
- <div class="cd-col-form-input">
- <input
- id="rgw_frontend_port"
- class="form-control"
- type="number"
- formControlName="rgw_frontend_port"
- min="1"
- max="65535"
- />
- <span
- class="invalid-feedback"
- *ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'pattern')"
- i18n
- >The entered value needs to be a number.</span
- >
- <span
- class="invalid-feedback"
- *ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'min')"
- i18n
- >The value must be at least 1.</span
- >
- <span
- class="invalid-feedback"
- *ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'max')"
- i18n
- >The value cannot exceed 65535.</span
- >
- </div>
+ <!-- RGW -->
+ <ng-container *ngIf="!importTokenForm.controls.unmanaged.value">
+ <!-- rgw_frontend_port -->
+ <div
+ class="form-item"
+ cdsRow
+ >
+ <div cdsCol>
+ <cds-number
+ cdValidate
+ #portRef="cdValidate"
+ formControlName="rgw_frontend_port"
+ label="Port"
+ i18n-label
+ [min]="1"
+ [max]="65535"
+ [invalid]="portRef.isInvalid"
+ [invalidText]="portError"
+ ></cds-number>
+ <ng-template #portError>
+ <ng-container
+ *ngTemplateOutlet="
+ validationErrors;
+ context: { control: importTokenForm.get('rgw_frontend_port') }
+ "
+ ></ng-container>
+ </ng-template>
</div>
- </ng-container>
- </div>
- <div class="modal-footer">
- <cd-form-button-panel
- (submitActionEvent)="onSubmit()"
- [submitText]="actionLabels.IMPORT"
- [form]="importTokenForm"
- ></cd-form-button-panel>
- </div>
+ </div>
+ </ng-container>
</form>
- </ng-container>
-</cd-modal>
+ </div>
+ <cd-form-button-panel
+ (submitActionEvent)="onSubmit()"
+ [submitText]="actionLabels.IMPORT"
+ [form]="importTokenForm"
+ [modalForm]="true"
+ ></cd-form-button-panel>
+
+ <ng-template
+ #validationErrors
+ let-control="control"
+ >
+ @if (control?.errors) {
+ @for (err of control.errors | keyvalue; track err.key) {
+ <span class="invalid-feedback">{{ INVALID_TEXTS[err.key] }}</span>
+ }
+ }
+ </ng-template>
+</cds-modal>
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
+import { BaseModal } from 'carbon-components-angular';
import { FormControl, Validators } from '@angular/forms';
-import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { ComboBoxItem } from '~/app/shared/models/combo-box.model';
+import { HostService } from '~/app/shared/api/host.service';
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
import { NotificationService } from '~/app/shared/services/notification.service';
import { RgwZone } from '../models/rgw-multisite';
import _ from 'lodash';
-import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
-import { HostService } from '~/app/shared/api/host.service';
-import { SelectOption } from '~/app/shared/components/select/select-option.model';
-import { Observable, Subject, merge } from 'rxjs';
-import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
@Component({
selector: 'cd-rgw-multisite-import',
styleUrls: ['./rgw-multisite-import.component.scss'],
standalone: false
})
-export class RgwMultisiteImportComponent implements OnInit {
+export class RgwMultisiteImportComponent extends BaseModal implements OnInit {
readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
readonly ipv4Rgx =
/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
- @ViewChild(NgbTypeahead, { static: false })
- typeahead: NgbTypeahead;
-
importTokenForm: CdFormGroup;
multisiteInfo: object[] = [];
zoneList: RgwZone[] = [];
zoneNames: string[];
- hosts: any;
- labels: string[];
- labelClick = new Subject<string>();
- labelFocus = new Subject<string>();
+ hosts: ComboBoxItem[] = [];
+ labels: ComboBoxItem[] = [];
+ selectedHosts: string[] = [];
+
+ INVALID_TEXTS = {
+ required: $localize`This field is required.`,
+ uniqueName: $localize`The chosen zone name is already in use.`,
+ min: $localize`The value must be at least 1.`,
+ max: $localize`The value cannot exceed 65535.`,
+ pattern: $localize`The entered value needs to be a number.`
+ };
constructor(
public activeModal: NgbActiveModal,
public actionLabels: ActionLabelsI18n,
public notificationService: NotificationService
) {
- this.hosts = {
- options: [],
- messages: new SelectMessages({
- empty: $localize`There are no hosts.`,
- filter: $localize`Filter hosts`
- })
- };
+ super();
this.createForm();
}
ngOnInit(): void {
return zone['name'];
});
this.hostService.getAllHosts().subscribe((resp: object[]) => {
- const options: SelectOption[] = [];
+ const options: ComboBoxItem[] = [];
_.forEach(resp, (host: object) => {
if (_.get(host, 'sources.orchestrator', false)) {
- const option = new SelectOption(false, _.get(host, 'hostname'), '');
- options.push(option);
+ const hostname = _.get(host, 'hostname');
+ options.push({ content: hostname, selected: false, name: hostname });
}
});
- this.hosts.options = [...options];
+
+ this.hosts = [...options];
});
this.hostService.getLabels().subscribe((resp: string[]) => {
- this.labels = resp;
+ this.labels = resp.map((label) => {
+ return {
+ content: label,
+ selected: false,
+ name: label
+ };
+ });
});
}
]
}),
rgw_frontend_port: new FormControl(null, {
- validators: [Validators.required, Validators.pattern('^[0-9]*$')]
+ validators: [
+ CdValidators.requiredIf({
+ unmanaged: false
+ }),
+ Validators.pattern('^[0-9]*$')
+ ]
}),
placement: new FormControl('hosts'),
label: new FormControl(null, [
});
}
+ multiSelector(event: ComboBoxItem[]) {
+ this.selectedHosts = event.map((host: ComboBoxItem) => host.content);
+ }
+
onSubmit() {
const values = this.importTokenForm.value;
const placementSpec: object = {
if (!values['unmanaged']) {
switch (values['placement']) {
case 'hosts':
- if (values['hosts'].length > 0) {
- placementSpec['placement']['hosts'] = values['hosts'];
+ if (this.selectedHosts.length > 0) {
+ placementSpec['placement']['hosts'] = this.selectedHosts;
}
break;
case 'label':
- placementSpec['placement']['label'] = values['label'];
+ if (!_.isEmpty(values['label'])) {
+ placementSpec['placement']['label'] = values['label']?.content;
+ }
break;
}
if (_.isNumber(values['count']) && values['count'] > 0) {
placementSpec['placement']['count'] = values['count'];
}
}
+
this.rgwRealmService
.importRealmToken(
values['realmToken'],
NotificationType.success,
$localize`Realm token import successfull`
);
- this.activeModal.close();
+ this.closeModal();
},
() => {
this.importTokenForm.setErrors({ cdSubmitButton: true });
}
);
}
-
- searchLabels = (text$: Observable<string>) => {
- return merge(
- text$.pipe(debounceTime(200), distinctUntilChanged()),
- this.labelFocus,
- this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
- ).pipe(
- map((value) =>
- this.labels
- .filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
- .slice(0, 10)
- )
- );
- };
}