-import { Component, OnInit, TemplateRef, ViewChild, inject } from '@angular/core';
+import {
+ Component,
+ OnInit,
+ TemplateRef,
+ ViewChild,
+ inject,
+ Output,
+ EventEmitter
+} from '@angular/core';
import { CephfsService } from '~/app/shared/api/cephfs.service';
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
filesystems$: Observable<FilesystemRow[]> = of([]);
selection = new CdTableSelection();
icons = Icons;
+ @Output() filesystemSelected = new EventEmitter<FilesystemRow | null>();
mdsStatusLabels: Record<MdsStatus, string> = {
Active: $localize`Active`,
Warning: $localize`Warning`,
updateSelection(selection: CdTableSelection) {
this.selection = selection;
+ const selectedRow = typeof selection?.first === 'function' ? selection.first() : null;
+ this.filesystemSelected.emit(selectedRow as FilesystemRow | null);
}
}
--- /dev/null
+<div class="scroll-overflow">
+ <div
+ [cdsStack]="'vertical'"
+ gap="3">
+ <div
+ class="cds--type-heading-03"
+ i18n>Create or select entity</div>
+ <p
+ class="cds--type-body-01"
+ i18n>
+ Choose an existing CephFS mirroring entity or create a new one to be used for mirroring.
+ </p>
+ </div>
+ <cds-tile>
+ <div>
+ <cds-radio-group [(ngModel)]="isCreatingNewEntity">
+ <cds-radio
+ [value]="true"
+ (click)="onCreateEntitySelected()"
+ i18n> Create new entity </cds-radio>
+ <cds-radio
+ [value]="false"
+ (click)="onExistingEntitySelected()"
+ i18n> Use existing entity </cds-radio>
+ </cds-radio-group>
+ </div>
+
+ @if (isCreatingNewEntity) {
+ @if (showCreateRequirementsWarning) {
+ <cd-alert-panel
+ type="warning"
+ class="cds-ml-3 cds-mr-3"
+ dismissible="true"
+ (dismissed)="onDismissCreateRequirementsWarning()">
+ <div
+ [cdsStack]="'vertical'"
+ gap="2">
+ <div
+ class="cds--type-heading-compact-01"
+ i18n>Mirroring entity requirements</div>
+ <div
+ class="cds--type-body-compact-01"
+ i18n>
+ This entity will be used by the mirroring service to manage snapshots and replication.
+ It will require the proper permissions to replicate data.
+ </div>
+ </div>
+ </cd-alert-panel>
+ }
+
+ @if (showCreateCapabilitiesInfo) {
+ <cd-alert-panel
+ type="info"
+ dismissible="true"
+ (dismissed)="onDismissCreateCapabilitiesInfo()">
+ <div
+ [cdsStack]="'vertical'"
+ gap="1">
+ <div
+ class="cds--type-heading-compact-01"
+ i18n>Capabilities added automatically.</div>
+ <div
+ class="cds--type-body-compact-01 requirements-list"
+ i18n>
+ If you create a new entity, the following capabilities will be assigned automatically:
+ </div>
+ <ul class="list-disc">
+ @for (cap of capabilities; track cap.name) {
+ <li>
+ {{ cap.name }}: {{ cap.permission }}
+ </li>
+ }
+ </ul>
+ </div>
+ </cd-alert-panel>
+ }
+
+ <form
+ name="entityForm"
+ #formDir="ngForm"
+ [formGroup]="entityForm"
+ novalidate>
+ <div
+ [cdsStack]="'vertical'"
+ gap="5"
+ class="form-item form-item-append cds-mt-5">
+ <cds-text-label
+ labelInputID="user_entity"
+ i18n
+ cdRequiredField="Ceph entity"
+ [helperText]="userEntityHelperText"
+ [invalid]="!entityForm.controls['user_entity'].valid && (entityForm.controls['user_entity'].dirty)"
+ [invalidText]="userEntityError"
+ i18n-invalidText>
+ Ceph entity
+ <div
+ [cdsStack]="'horizontal'"
+ gap="0">
+ <input
+ cdsText
+ value="client."
+ readonly
+ name="client_prefix" />
+ <input
+ cdsText
+ name="user_entity"
+ formControlName="user_entity"
+ [invalid]="!entityForm.controls['user_entity'].valid && (entityForm.controls['user_entity'].dirty)" />
+ </div>
+ </cds-text-label>
+
+ <ng-template #userEntityError>
+ @if (entityForm.showError('user_entity', formDir, 'required')) {
+ <span
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ }
+ @if (entityForm.showError('user_entity', formDir, 'forbiddenClientPrefix')) {
+ <span
+ class="invalid-feedback"
+ i18n>Do not include 'client.' prefix. Enter only the entity suffix.</span>
+ }
+ </ng-template>
+ </div>
+ <div>
+ <cd-form-button-panel
+ (submitActionEvent)="submitAction()"
+ [form]="entityForm"
+ submitText="Create entity"
+ i18n-submitText
+ [showCancel]="false">
+ </cd-form-button-panel>
+ </div>
+ </form>
+ }
+
+ @if (!isCreatingNewEntity && showSelectRequirementsWarning) {
+ <cd-alert-panel
+ type="warning"
+ class="cds-ml-3 cds-mr-3"
+ dismissible="true"
+ (dismissed)="onDismissSelectRequirementsWarning()">
+ <div
+ [cdsStack]="'vertical'"
+ gap="2">
+ <div
+ class="cds--type-heading-compact-01"
+ i18n>Mirroring entity requirements</div>
+ <div
+ class="cds--type-body-compact-01"
+ i18n>
+ This selected entity will be used by the mirroring service to manage snapshots and
+ replication. It must have rwps capabilities on MDS, MON, and OSD.
+ </div>
+ </div>
+ </cd-alert-panel>
+ @if (showSelectEntityInfo) {
+ <cd-alert-panel
+ type="info"
+ class="cds-mb-3"
+ dismissible="true"
+ (dismissed)="onDismissSelectEntityInfo()">
+ <div
+ [cdsStack]="'vertical'"
+ gap="2">
+ <div
+ class="cds--type-heading-01"
+ i18n>Entity selection note</div>
+ <div
+ class="cds--type-body-compact-01"
+ i18n>
+ Only entities with valid rwps capabilities can be used for mirroring.
+ </div>
+ </div>
+ </cd-alert-panel>
+ }
+ }
+
+ @let entities = (entities$ | async);
+ @if (!isCreatingNewEntity && entities) {
+ <cd-table
+ #table
+ [data]="entities"
+ [columns]="columns"
+ columnMode="flex"
+ selectionType="singleRadio"
+ (updateSelection)="updateSelection($event)"
+ (fetchData)="loadEntities($event)">
+ </cd-table>
+ }
+ </cds-tile>
+</div>
+
--- /dev/null
+@use '@carbon/layout';
+
+.scroll-overflow {
+ max-height: 70vh;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.requirements-list {
+ list-style-type: disc;
+ padding-left: var(--cds-spacing-05);
+ margin-left: layout.$spacing-02;
+}
+
+.list-disc {
+ list-style-type: disc;
+}
--- /dev/null
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { of } from 'rxjs';
+
+import { CephfsMirroringEntityComponent } from './cephfs-mirroring-entity.component';
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { ClusterService } from '~/app/shared/api/cluster.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+
+describe('CephfsMirroringEntityComponent', () => {
+ let component: CephfsMirroringEntityComponent;
+ let fixture: ComponentFixture<CephfsMirroringEntityComponent>;
+
+ let clusterServiceMock: any;
+ let cephfsServiceMock: any;
+ let taskWrapperServiceMock: any;
+
+ beforeEach(async () => {
+ clusterServiceMock = {
+ listUser: jest.fn(),
+ createUser: jest.fn()
+ };
+
+ cephfsServiceMock = {
+ setAuth: jest.fn()
+ };
+
+ taskWrapperServiceMock = {
+ wrapTaskAroundCall: jest.fn()
+ };
+
+ await TestBed.configureTestingModule({
+ declarations: [CephfsMirroringEntityComponent],
+ imports: [ReactiveFormsModule],
+ providers: [
+ CdFormBuilder,
+ { provide: ClusterService, useValue: clusterServiceMock },
+ { provide: CephfsService, useValue: cephfsServiceMock },
+ { provide: TaskWrapperService, useValue: taskWrapperServiceMock }
+ ]
+ })
+ .overrideComponent(CephfsMirroringEntityComponent, {
+ set: { template: '' }
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(CephfsMirroringEntityComponent);
+ component = fixture.componentInstance;
+
+ component.selectedFilesystem = { name: 'myfs' } as any;
+
+ clusterServiceMock.listUser.mockReturnValue(of([]));
+
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should create component', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should initialize form with required + noClientPrefix validator', () => {
+ const control = component.entityForm.get('user_entity');
+
+ control?.setValue('');
+ expect(control?.valid).toBeFalsy();
+
+ control?.setValue('client.test');
+ expect(control?.errors?.['forbiddenClientPrefix']).toBeTruthy();
+
+ control?.setValue('validuser');
+ expect(control?.valid).toBeTruthy();
+ });
+
+ it('should filter entities based on fsname', fakeAsync(() => {
+ clusterServiceMock.listUser.mockReturnValue(
+ of([
+ {
+ entity: 'client.valid',
+ caps: { mds: 'allow rw fsname=myfs' }
+ },
+ {
+ entity: 'client.invalid',
+ caps: { mds: 'allow rw fsname=otherfs' }
+ },
+ {
+ entity: 'osd.something',
+ caps: { mds: 'allow rw fsname=myfs' }
+ }
+ ])
+ );
+
+ component.loadEntities();
+ tick();
+
+ component.entities$.subscribe((rows) => {
+ expect(rows.length).toBe(1);
+ expect(rows[0].entity).toBe('client.valid');
+ expect(rows[0].mdsCaps).toContain('fsname=myfs');
+ });
+ }));
+
+ it('should not submit if form invalid', () => {
+ component.entityForm.get('user_entity')?.setValue('');
+
+ component.submitAction();
+
+ expect(clusterServiceMock.createUser).not.toHaveBeenCalled();
+ expect(component.isSubmitting).toBeFalsy();
+ });
+
+ it('should create entity and set auth successfully', fakeAsync(() => {
+ component.entityForm.get('user_entity')?.setValue('newuser');
+
+ clusterServiceMock.createUser.mockReturnValue(of({}));
+ taskWrapperServiceMock.wrapTaskAroundCall.mockReturnValue(of({}));
+ cephfsServiceMock.setAuth.mockReturnValue(of({}));
+
+ const emitSpy = jest.spyOn(component.entitySelected, 'emit');
+
+ component.submitAction();
+ tick();
+
+ expect(clusterServiceMock.createUser).toHaveBeenCalledWith(
+ expect.objectContaining({
+ user_entity: 'client.newuser'
+ })
+ );
+
+ expect(cephfsServiceMock.setAuth).toHaveBeenCalledWith('myfs', 'newuser', ['/', 'rwps'], false);
+
+ expect(emitSpy).toHaveBeenCalledWith('client.newuser');
+ expect(component.isSubmitting).toBeFalsy();
+ expect(component.isCreatingNewEntity).toBeFalsy();
+ }));
+
+ it('should emit selected entity on updateSelection', () => {
+ const selection = new CdTableSelection();
+ selection.selected = [{ entity: 'client.test' }];
+
+ const emitSpy = jest.spyOn(component.entitySelected, 'emit');
+
+ component.updateSelection(selection);
+
+ expect(emitSpy).toHaveBeenCalledWith('client.test');
+ });
+
+ it('should emit null when selection empty', () => {
+ const selection = new CdTableSelection();
+ selection.selected = [];
+
+ const emitSpy = jest.spyOn(component.entitySelected, 'emit');
+
+ component.updateSelection(selection);
+
+ expect(emitSpy).toHaveBeenCalledWith(null);
+ });
+
+ it('should toggle create/existing states correctly', () => {
+ component.onExistingEntitySelected();
+ expect(component.isCreatingNewEntity).toBeFalsy();
+
+ component.onCreateEntitySelected();
+ expect(component.isCreatingNewEntity).toBeTruthy();
+ });
+
+ it('should dismiss warnings correctly', () => {
+ component.onDismissCreateRequirementsWarning();
+ expect(component.showCreateRequirementsWarning).toBeFalsy();
+
+ component.onDismissCreateCapabilitiesInfo();
+ expect(component.showCreateCapabilitiesInfo).toBeFalsy();
+
+ component.onDismissSelectRequirementsWarning();
+ expect(component.showSelectRequirementsWarning).toBeFalsy();
+
+ component.onDismissSelectEntityInfo();
+ expect(component.showSelectEntityInfo).toBeFalsy();
+ });
+});
--- /dev/null
+import { Component, OnInit, Output, EventEmitter, Input, inject } from '@angular/core';
+import { BehaviorSubject, Observable, of } from 'rxjs';
+import { catchError, map, switchMap, defaultIfEmpty } from 'rxjs/operators';
+
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { ClusterService } from '~/app/shared/api/cluster.service';
+
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { Validators, AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { FilesystemRow, MirroringEntityRow } from '~/app/shared/models/cephfs.model';
+import { CephAuthUser } from '~/app/shared/models/cluster.model';
+
+@Component({
+ selector: 'cd-cephfs-mirroring-entity',
+ templateUrl: './cephfs-mirroring-entity.component.html',
+ styleUrls: ['./cephfs-mirroring-entity.component.scss'],
+ standalone: false
+})
+export class CephfsMirroringEntityComponent extends CdForm implements OnInit {
+ columns: CdTableColumn[];
+ selection = new CdTableSelection();
+
+ subject$ = new BehaviorSubject<void>(undefined);
+ entities$: Observable<MirroringEntityRow[]>;
+ context: CdTableFetchDataContext;
+ capabilities = [
+ { name: $localize`MDS`, permission: 'rwps' },
+ { name: $localize`MON`, permission: 'rwps' },
+ { name: $localize`OSD`, permission: 'rwps' }
+ ];
+
+ isCreatingNewEntity = true;
+ showCreateRequirementsWarning = true;
+ showCreateCapabilitiesInfo = true;
+ showSelectRequirementsWarning = true;
+ showSelectEntityInfo = true;
+
+ entityForm: CdFormGroup;
+
+ readonly userEntityHelperText = $localize`Ceph Authentication entity used by mirroring.`;
+
+ @Input() selectedFilesystem: FilesystemRow | null = null;
+ @Output() entitySelected = new EventEmitter<string | null>();
+ isSubmitting: boolean = false;
+
+ private cephfsService = inject(CephfsService);
+ private clusterService = inject(ClusterService);
+ private taskWrapperService = inject(TaskWrapperService);
+ private formBuilder = inject(CdFormBuilder);
+
+ ngOnInit(): void {
+ const noClientPrefix: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
+ const value = (control.value ?? '').toString().trim();
+ if (!value) return null;
+ return value.startsWith('client.') ? { forbiddenClientPrefix: true } : null;
+ };
+
+ this.entityForm = this.formBuilder.group({
+ user_entity: ['', [Validators.required, noClientPrefix]]
+ });
+
+ this.columns = [
+ {
+ name: $localize`Entity ID`,
+ prop: 'entity',
+ flexGrow: 2
+ },
+ {
+ name: $localize`MDS capabilities`,
+ prop: 'mdsCaps',
+ flexGrow: 1.5
+ },
+ {
+ name: $localize`MON capabilities`,
+ prop: 'monCaps',
+ flexGrow: 1.5
+ },
+ {
+ name: $localize`OSD capabilities`,
+ prop: 'osdCaps',
+ flexGrow: 1.5
+ }
+ ];
+
+ this.entities$ = this.subject$.pipe(
+ switchMap(() =>
+ this.clusterService.listUser().pipe(
+ switchMap((users) => {
+ const typedUsers = (users as CephAuthUser[]) || [];
+ const filteredEntities = typedUsers.filter((entity) => {
+ if (entity.entity?.startsWith('client.')) {
+ const caps = entity.caps || {};
+ const mdsCaps = caps.mds || '-';
+
+ const fsName = this.selectedFilesystem?.name || '';
+ const isValid = mdsCaps.includes(`fsname=${fsName}`);
+
+ return isValid;
+ }
+ return false;
+ });
+
+ const rows: MirroringEntityRow[] = filteredEntities.map((entity) => {
+ const caps = entity.caps || {};
+ const mdsCaps = caps.mds || '-';
+ const monCaps = caps.mon || '-';
+ const osdCaps = caps.osd || '-';
+
+ return {
+ entity: entity.entity,
+ mdsCaps,
+ monCaps,
+ osdCaps
+ };
+ });
+
+ return of(rows);
+ }),
+ catchError(() => {
+ this.context?.error();
+ return of([]);
+ })
+ )
+ )
+ );
+
+ this.loadEntities();
+ }
+
+ submitAction(): void {
+ if (!this.entityForm.valid) {
+ this.entityForm.markAllAsTouched();
+ return;
+ }
+
+ const clientEntity = (this.entityForm.get('user_entity')?.value || '').toString().trim();
+ const fullEntity = `client.${clientEntity}`;
+ const fsName = this.selectedFilesystem?.name;
+
+ const payload = {
+ user_entity: fullEntity,
+ capabilities: [
+ { entity: 'mds', cap: 'allow *' },
+ { entity: 'mgr', cap: 'allow *' },
+ { entity: 'mon', cap: 'allow *' },
+ { entity: 'osd', cap: 'allow *' }
+ ]
+ };
+
+ this.isSubmitting = true;
+
+ this.taskWrapperService
+ .wrapTaskAroundCall({
+ task: new FinishedTask(`ceph-user/create`, {
+ userEntity: fullEntity,
+ fsName: fsName
+ }),
+ call: this.clusterService.createUser(payload).pipe(
+ map((res) => {
+ return { ...(res as Record<string, unknown>), __taskCompleted: true };
+ })
+ )
+ })
+ .pipe(
+ defaultIfEmpty(null),
+ switchMap(() => {
+ if (fsName) {
+ return this.cephfsService.setAuth(fsName, clientEntity, ['/', 'rwps'], false);
+ }
+ return of(null);
+ })
+ )
+ .subscribe({
+ complete: () => {
+ this.isSubmitting = false;
+ this.entityForm.reset();
+ this.handleEntityCreated(fullEntity);
+ }
+ });
+ }
+
+ private handleEntityCreated(entityId: string) {
+ this.loadEntities(this.context);
+ this.entitySelected.emit(entityId);
+ this.isCreatingNewEntity = false;
+ }
+
+ loadEntities(context?: CdTableFetchDataContext) {
+ this.context = context;
+ this.subject$.next();
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ const selectedRow = selection?.first();
+ this.entitySelected.emit(selectedRow ? selectedRow.entity : null);
+ }
+
+ onCreateEntitySelected() {
+ this.isCreatingNewEntity = true;
+ this.showCreateRequirementsWarning = true;
+ this.showCreateCapabilitiesInfo = true;
+ }
+
+ onExistingEntitySelected() {
+ this.isCreatingNewEntity = false;
+ this.showSelectRequirementsWarning = true;
+ this.showSelectEntityInfo = true;
+ }
+
+ onDismissCreateRequirementsWarning() {
+ this.showCreateRequirementsWarning = false;
+ }
+
+ onDismissCreateCapabilitiesInfo() {
+ this.showCreateCapabilitiesInfo = false;
+ }
+
+ onDismissSelectRequirementsWarning() {
+ this.showSelectRequirementsWarning = false;
+ }
+
+ onDismissSelectEntityInfo() {
+ this.showSelectEntityInfo = false;
+ }
+}
<div>
<div class="cds--type-heading-03"
i18n>Choose mirror peer role</div>
- <p i18n>Select how the cluster will participate in the CephFS Mirroring relationship.</p>
+ <p i18n>Select how the cluster will participate in the CephFS mirroring relationship.</p>
</div>
<div cdsStack="horizontal">
dismissible="true"
(dismissed)="showMessage = false">
<div>
- <div class="cds--type-heading-compact-01 cds-mb-2"
- i18n>About Remote Peer Setup</div>
- <div class="cds--type-body-compact-01 cds-mb-3"
- i18n>
+ <div
+ class="cds--type-heading-compact-01 cds-mb-2"
+ i18n>About remote peer setup</div>
+ <div
+ class="cds--type-body-compact-01 cds-mb-3"
+ i18n>
As a remote peer, this cluster prepares to receive mirrored data from an initiating
cluster. The setup includes environment validation, enabling filesystem mirroring,
creating required Ceph users, and generating a bootstrap token.
</div>
- <div class="cds--type-heading-compact-01 cds-mb-1 cds-mt-6"
- i18n>What happens next:</div>
+ <div
+ class="cds--type-heading-compact-01 cds-mb-1 cds-mt-6"
+ i18n>What happens next:</div>
<ul class="list-disc cds-ml-5 cds--type-body-compact-01">
<li i18n>Environment validation</li>
<li i18n>Ceph user creation</li>
<!-- Step 1 -->
<cd-tearsheet-step>
- <cd-cephfs-filesystem-selector>
+ <cd-cephfs-filesystem-selector (filesystemSelected)="onFilesystemSelected($event)">
</cd-cephfs-filesystem-selector>
</cd-tearsheet-step>
<!-- Step 2 -->
<cd-tearsheet-step>
- <div>Test 2</div>
+ <cd-cephfs-mirroring-entity [selectedFilesystem]="selectedFilesystem"
+ (entitySelected)="onEntitySelected($event)">
+ </cd-cephfs-mirroring-entity>
</cd-tearsheet-step>
<!-- Step 3 -->
<cd-tearsheet-step>
<div>
- <p>Test3</p>
+ Test 3
</div>
</cd-tearsheet-step>
</cd-tearsheet>
import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
import { WizardStepModel } from '~/app/shared/models/wizard-steps';
import { FormBuilder, FormGroup } from '@angular/forms';
+import { FilesystemRow } from '~/app/shared/models/cephfs.model';
@Component({
selector: 'cd-cephfs-mirroring-wizard',
templateUrl: './cephfs-mirroring-wizard.component.html',
description: string = $localize`Configure a new mirroring relationship between clusters`;
form: FormGroup;
showMessage: boolean = true;
+ selectedFilesystem: FilesystemRow | null = null;
+ selectedEntity: string | null = null;
LOCAL_ROLE = LOCAL_ROLE;
REMOTE_ROLE = REMOTE_ROLE;
const stepsData = this.wizardStepsService.steps$.value;
this.steps = STEP_TITLES_MIRRORING_CONFIGURED.map((title, index) => ({
label: title,
- onClick: () => this.goToStep(stepsData[index])
+ onClick: () => this.goToStep(stepsData[index]),
+ invalid: true
}));
}
+ onFilesystemSelected(filesystem: FilesystemRow) {
+ this.selectedFilesystem = filesystem;
+ if (this.steps[1]) {
+ this.steps[1].invalid = !filesystem;
+ }
+ }
+
+ onEntitySelected(entity: string) {
+ this.selectedEntity = entity;
+ if (this.steps[2]) {
+ this.steps[2].invalid = !entity;
+ }
+ }
+
goToStep(step: WizardStepModel) {
if (step) {
this.wizardStepsService.setCurrentStep(step);
onLocalRoleChange() {
this.form.patchValue({ localRole: LOCAL_ROLE, remoteRole: null });
this.showMessage = false;
+ if (this.steps[0]) {
+ this.steps[0].invalid = false;
+ }
}
onRemoteRoleChange() {
this.form.patchValue({ localRole: null, remoteRole: REMOTE_ROLE });
this.showMessage = true;
+ if (this.steps[0]) {
+ this.steps[0].invalid = false;
+ }
}
onSubmit() {}
ModalModule,
NumberModule,
PlaceholderModule,
+ RadioModule,
SelectModule,
TimePickerModule,
TilesModule,
TreeviewModule,
TabsModule,
- RadioModule,
NotificationModule
} from 'carbon-components-angular';
import Trash from '@carbon/icons/es/trash-can/32';
import { CephfsMirroringWizardComponent } from './cephfs-mirroring-wizard/cephfs-mirroring-wizard.component';
import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/cephfs-filesystem-selector.component';
+import { CephfsMirroringEntityComponent } from './cephfs-mirroring-entity/cephfs-mirroring-entity.component';
@NgModule({
imports: [
LayoutModule,
ComboBoxModule,
IconModule,
+ RadioModule,
BaseChartDirective,
TabsModule,
RadioModule,
CephfsMirroringListComponent,
CephfsMirroringWizardComponent,
CephfsFilesystemSelectorComponent,
- CephfsMirroringErrorComponent
+ CephfsMirroringErrorComponent,
+ CephfsMirroringEntityComponent
],
providers: [provideCharts(withDefaultRegisterables())]
})
});
}
- setAuth(fsName: string, clientId: number, caps: string[], rootSquash: boolean) {
+ setAuth(fsName: string, clientId: string | number, caps: string[], rootSquash: boolean) {
return this.http.put(`${this.baseURL}/auth`, {
fs_name: fsName,
client_id: `client.${clientId}`,
import { Observable } from 'rxjs';
+import { CephClusterUser } from '~/app/shared/models/cluster.model';
+
@Injectable({
providedIn: 'root'
})
{ headers: { Accept: 'application/vnd.ceph.api.v0.1+json' } }
);
}
+
+ listUser(): Observable<CephClusterUser[]> {
+ return this.http.get<CephClusterUser[]>(`${this.baseURL}/user`);
+ }
+
+ createUser(payload: CephClusterUser) {
+ return this.http.post(`${this.baseURL}/user`, payload);
+ }
}
onDeselect(deselectedRowIndex: number) {
this.model.selectRow(deselectedRowIndex, false);
if (this.selectionType === 'single' || this.selectionType === 'singleRadio') {
+ this.selection.selected = [];
+ this.updateSelection.emit(this.selection);
return;
}
this._toggleSelection(deselectedRowIndex, false);
}
export type DaemonResponse = Daemon[];
+
+export type MirroringEntityRow = {
+ entity: string;
+ mdsCaps: string;
+ monCaps: string;
+ osdCaps: string;
+};
--- /dev/null
+export interface CephClusterUser {
+ [key: string]: unknown;
+}
+
+export type CephAuthUser = {
+ [key: string]: unknown;
+ entity?: string;
+ caps?: {
+ mds?: string;
+ mon?: string;
+ osd?: string;
+ };
+};
'cephfs/smb/standalone/delete': this.newTaskMessage(
this.commonOperations.delete,
(metadata: { usersGroupsId: string }) => this.smbUsersgroups(metadata)
+ ),
+ 'ceph-user/create': this.newTaskMessage(
+ this.commonOperations.create,
+ (metadata: { userEntity: string }) => this.cephUser(metadata)
)
};
getRunningText(task: Task) {
return this._getTaskTitle(task).operation.running;
}
+
+ cephUser(metadata: { userEntity: string }) {
+ return $localize`Ceph user '${metadata.userEntity}'`;
+ }
}