- Enhance datatable to handle loading errors.
- Add warning panel component
- Modify the icon of the error panel component
- Use cd-[info|warning|error]-panel in the cd-view-cache component
Signed-off-by: Volker Theile <vtheile@suse.com>
[status]="viewCacheStatus.status"
[statusFor]="viewCacheStatus.statusFor"></cd-view-cache>
-<cd-table [data]="images"
+<cd-table #table
+ [data]="images"
columnMode="flex"
[columns]="columns"
identifier="id"
import { RbdService } from '../../../shared/api/rbd.service';
import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component';
+import { TableComponent } from '../../../shared/datatable/table/table.component';
import { CellTemplate } from '../../../shared/enum/cell-template.enum';
import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
styleUrls: ['./rbd-list.component.scss']
})
export class RbdListComponent implements OnInit, OnDestroy {
+ @ViewChild(TableComponent) table: TableComponent;
@ViewChild('usageTpl') usageTpl: TemplateRef<any>;
@ViewChild('parentTpl') parentTpl: TemplateRef<any>;
@ViewChild('nameTpl') nameTpl: TemplateRef<any>;
this.summaryDataSubscription = this.summaryService.summaryData$.subscribe((data: any) => {
this.loadImages(data.executing_tasks);
});
+ },
+ () => {
+ this.table.reset(); // Disable loading indicator.
+ this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }];
});
}
this.executingTasks = executingTasks;
},
() => {
+ this.table.reset(); // Disable loading indicator.
this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }];
}
);
<cd-table [data]="filesystems"
columnMode="flex"
[columns]="columns"
- (fetchData)="loadFilesystems()"
+ (fetchData)="loadFilesystems($event)"
identifier="id"
forceIdentifier="true"
selectionType="single"
import { CephfsService } from '../../../shared/api/cephfs.service';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
@Component({
];
}
- loadFilesystems() {
- this.cephfsService.list().subscribe((resp: any[]) => {
- this.filesystems = resp;
- });
+ loadFilesystems(context: CdTableFetchDataContext) {
+ this.cephfsService.list().subscribe(
+ (resp: any[]) => {
+ this.filesystems = resp;
+ },
+ () => {
+ context.error();
+ }
+ );
}
updateSelection(selection: CdTableSelection) {
</ol>
</nav>
<cd-table [data]="data | filter:filters"
- (fetchData)="getConfigurationList()"
+ (fetchData)="getConfigurationList($event)"
[columns]="columns"
selectionType="single"
(updateSelection)="updateSelection($event)">
import { ConfigurationService } from '../../../shared/api/configuration.service';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
@Component({
this.selection = selection;
}
- getConfigurationList() {
- this.configurationService.getConfigData().subscribe((data: any) => {
- this.data = data;
- });
+ getConfigurationList(context: CdTableFetchDataContext) {
+ this.configurationService.getConfigData().subscribe(
+ (data: any) => {
+ this.data = data;
+ },
+ () => {
+ context.error();
+ }
+ );
}
updateFilter() {
<cd-table [data]="hosts"
[columns]="columns"
columnMode="flex"
- (fetchData)="getHosts()">
+ (fetchData)="getHosts($event)">
<ng-template #servicesTpl let-value="value">
<span *ngFor="let service of value; last as isLast">
<a [routerLink]="[service.cdLink]"
import { HostService } from '../../../shared/api/host.service';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
import { Permissions } from '../../../shared/models/permissions';
import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
];
}
- getHosts() {
+ getHosts(context: CdTableFetchDataContext) {
if (this.isLoadingHosts) {
return;
}
mgr: 'manager'
};
this.isLoadingHosts = true;
- this.hostService
- .list()
- .then((resp) => {
- resp.map((host) => {
- host.services.map((service) => {
- service.cdLink = `/perf_counters/${service.type}/${service.id}`;
- const permissionKey = typeToPermissionKey[service.type];
- service.canRead = this.permissions[permissionKey].read;
- return service;
- });
- return host;
+ this.hostService.list().then((resp) => {
+ resp.map((host) => {
+ host.services.map((service) => {
+ service.cdLink = `/perf_counters/${service.type}/${service.id}`;
+ const permissionKey = typeToPermissionKey[service.type];
+ service.canRead = this.permissions[permissionKey].read;
+ return service;
});
- this.hosts = resp;
- this.isLoadingHosts = false;
- })
- .catch(() => {
- this.isLoadingHosts = false;
+ return host;
});
+ this.hosts = resp;
+ this.isLoadingHosts = false;
+ }).catch(() => {
+ this.isLoadingHosts = false;
+ context.error();
+ });
}
}
</ol>
</nav>
<cd-table [data]="pools"
- (fetchData)="getPoolList()"
+ (fetchData)="getPoolList($event)"
[columns]="columns"
selectionType="single"
(updateSelection)="updateSelection($event)">
import { PoolService } from '../../../shared/api/pool.service';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
@Component({
columns: CdTableColumn[];
selection = new CdTableSelection();
- constructor(
- private poolService: PoolService,
- ) {
+ constructor(private poolService: PoolService) {
this.columns = [
{
prop: 'pool_name',
this.selection = selection;
}
- getPoolList() {
- this.poolService.getList().subscribe((pools: any[]) => {
- this.pools = pools;
- });
+ getPoolList(context: CdTableFetchDataContext) {
+ this.poolService.getList().subscribe(
+ (pools: any[]) => {
+ this.pools = pools;
+ },
+ () => {
+ context.error();
+ }
+ );
}
-
}
selectionType="multi"
(updateSelection)="updateSelection($event)"
identifier="bucket"
- (fetchData)="getBucketList()">
+ (fetchData)="getBucketList($event)">
<div class="table-actions">
<div class="btn-group"
dropdown>
import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component';
import { TableComponent } from '../../../shared/datatable/table/table.component';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
import { Permission } from '../../../shared/models/permissions';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
];
}
- getBucketList() {
+ getBucketList(context: CdTableFetchDataContext) {
this.rgwBucketService.list().subscribe(
(resp: object[]) => {
this.buckets = resp;
},
() => {
- // Force datatable to hide the loading indicator in
- // case of an error.
- this.buckets = [];
+ context.error();
}
);
}
columnMode="flex"
selectionType="single"
(updateSelection)="updateSelection($event)"
- (fetchData)="getDaemonList()">
+ (fetchData)="getDaemonList($event)">
<cd-rgw-daemon-details cdTableDetail
[selection]="selection">
</cd-rgw-daemon-details>
import { RgwDaemonService } from '../../../shared/api/rgw-daemon.service';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
];
}
- getDaemonList() {
+ getDaemonList(context: CdTableFetchDataContext) {
this.rgwDaemonService.list().subscribe(
(resp: object[]) => {
this.daemons = resp;
},
() => {
- // Force datatable to hide the loading indicator in
- // case of an error.
- this.daemons = [];
+ context.error();
}
);
}
selectionType="multi"
(updateSelection)="updateSelection($event)"
identifier="user_id"
- (fetchData)="getUserList()">
+ (fetchData)="getUserList($event)">
<div class="table-actions">
<div class="btn-group" dropdown>
<button type="button"
import { TableComponent } from '../../../shared/datatable/table/table.component';
import { CellTemplate } from '../../../shared/enum/cell-template.enum';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
import { Permission } from '../../../shared/models/permissions';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
];
}
- getUserList() {
+ getUserList(context: CdTableFetchDataContext) {
this.rgwUserService.list().subscribe(
(resp: object[]) => {
this.users = resp;
},
() => {
- // Force datatable to hide the loading indicator in
- // case of an error.
- this.users = [];
+ context.error();
}
);
}
import { SubmitButtonComponent } from './submit-button/submit-button.component';
import { UsageBarComponent } from './usage-bar/usage-bar.component';
import { ViewCacheComponent } from './view-cache/view-cache.component';
+import { WarningPanelComponent } from './warning-panel/warning-panel.component';
@NgModule({
imports: [
InfoPanelComponent,
ModalComponent,
DeletionModalComponent,
- ConfirmationModalComponent
+ ConfirmationModalComponent,
+ WarningPanelComponent
],
providers: [],
exports: [
LoadingPanelComponent,
InfoPanelComponent,
UsageBarComponent,
- ModalComponent
+ ModalComponent,
+ WarningPanelComponent
],
entryComponents: [ModalComponent, DeletionModalComponent, ConfirmationModalComponent]
})
<table>
<tr>
<td rowspan="2" class="error-panel-alert-icon">
- <i class="fa fa-3x fa-exclamation-triangle alert-danger"
+ <i class="fa fa-3x fa-times-circle alert-danger"
aria-hidden="true"></i>
</td>
<td class="error-panel-alert-title">
-<alert i18n
- type="info"
- *ngIf="status === vcs.ValueNone">
+<cd-info-panel i18n
+ *ngIf="status === vcs.ValueNone">
Retrieving data<span *ngIf="statusFor"> for <span [innerHtml]="statusFor"></span></span>. Please wait...
-</alert>
+</cd-info-panel>
-<alert i18n
- type="warning"
- *ngIf="status === vcs.ValueStale">
+<cd-warning-panel i18n
+ *ngIf="status === vcs.ValueStale">
Displaying previously cached data<span *ngIf="statusFor"> for <span [innerHtml]="statusFor"></span></span>.
-</alert>
+</cd-warning-panel>
-<alert i18n
- type="danger"
- *ngIf="status === vcs.ValueException">
+<cd-error-panel i18n
+ *ngIf="status === vcs.ValueException">
Could not load data<span *ngIf="statusFor"> for <span [innerHtml]="statusFor"></span></span>. Please check the cluster health.
-</alert>
+</cd-error-panel>
import { AlertModule } from 'ngx-bootstrap';
import { configureTestBed } from '../../unit-test-helper';
+import { ErrorPanelComponent } from '../error-panel/error-panel.component';
+import { InfoPanelComponent } from '../info-panel/info-panel.component';
+import { WarningPanelComponent } from '../warning-panel/warning-panel.component';
import { ViewCacheComponent } from './view-cache.component';
describe('ViewCacheComponent', () => {
let fixture: ComponentFixture<ViewCacheComponent>;
configureTestBed({
- declarations: [ViewCacheComponent],
+ declarations: [
+ ErrorPanelComponent,
+ InfoPanelComponent,
+ ViewCacheComponent,
+ WarningPanelComponent
+ ],
imports: [AlertModule.forRoot()]
});
--- /dev/null
+<alert type="warning">
+ <table>
+ <tr>
+ <td rowspan="2" class="warning-panel-alert-icon">
+ <i class="fa fa-3x fa-warning alert-warning"
+ aria-hidden="true"></i>
+ </td>
+ <td class="warning-panel-alert-title">
+ {{ title }}
+ </td>
+ </tr>
+ <tr>
+ <td class="warning-panel-alert-text">
+ <ng-content></ng-content>
+ </td>
+ </tr>
+ </table>
+</alert>
--- /dev/null
+.warning-panel-alert-icon {
+ vertical-align: top;
+ padding-right: 15px; // See @alert-padding in bootstrap/less/variables.less
+}
+.warning-panel-alert-title {
+ font-weight: bold;
+}
+.warning-panel-alert-text {
+}
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AlertModule } from 'ngx-bootstrap';
+
+import { configureTestBed } from '../../unit-test-helper';
+import { WarningPanelComponent } from './warning-panel.component';
+
+describe('WarningPanelComponent', () => {
+ let component: WarningPanelComponent;
+ let fixture: ComponentFixture<WarningPanelComponent>;
+
+ configureTestBed({
+ declarations: [WarningPanelComponent],
+ imports: [AlertModule.forRoot()]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(WarningPanelComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'cd-warning-panel',
+ templateUrl: './warning-panel.component.html',
+ styleUrls: ['./warning-panel.component.scss']
+})
+export class WarningPanelComponent {
+ /**
+ * The title to be displayed. Defaults to 'Warning'.
+ * @type {string}
+ */
+ @Input() title = 'Warning';
+}
+<cd-error-panel i18n
+ *ngIf="loadingError">
+ Failed to load data.
+</cd-error-panel>
<div class="dataTables_wrapper">
<div class="dataTables_header clearfix"
*ngIf="toolHeader">
import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { ComponentsModule } from '../../components/components.module';
+import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
import { configureTestBed } from '../../unit-test-helper';
import { TableComponent } from './table.component';
clearLocalStorage();
});
});
+
+ describe('reload data', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ component.data = [];
+ component.updating = false;
+ });
+
+ it('should call fetchData callback function', () => {
+ component.fetchData.subscribe((context) => {
+ expect(context instanceof CdTableFetchDataContext).toBeTruthy();
+ });
+ component.reloadData();
+ });
+
+ it('should call error function', () => {
+ component.data = createFakeData(5);
+ component.fetchData.subscribe((context) => {
+ context.error();
+ expect(component.loadingError).toBeTruthy();
+ expect(component.data.length).toBe(0);
+ expect(component.loadingIndicator).toBeFalsy();
+ expect(component.updating).toBeFalsy();
+ });
+ component.reloadData();
+ });
+
+ it('should call error function with custom config', () => {
+ component.data = createFakeData(10);
+ component.fetchData.subscribe((context) => {
+ context.errorConfig.resetData = false;
+ context.errorConfig.displayError = false;
+ context.error();
+ expect(component.loadingError).toBeFalsy();
+ expect(component.data.length).toBe(10);
+ expect(component.loadingIndicator).toBeFalsy();
+ expect(component.updating).toBeFalsy();
+ });
+ component.reloadData();
+ });
+
+ afterEach(() => {
+ clearLocalStorage();
+ });
+ });
});
import { CellTemplate } from '../../enum/cell-template.enum';
import { CdTableColumn } from '../../models/cd-table-column';
+import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
import { CdTableSelection } from '../../models/cd-table-selection';
import { CdUserConfig } from '../../models/cd-user-config';
search = '';
rows = [];
loadingIndicator = true;
+ loadingError = false;
paginationClasses = {
pagerLeftArrow: 'i fa fa-angle-double-left',
pagerRightArrow: 'i fa fa-angle-double-right',
reloadData() {
if (!this.updating) {
- this.fetchData.emit();
+ this.loadingError = false;
+ const context = new CdTableFetchDataContext(() => {
+ // Do we have to display the error panel?
+ this.loadingError = context.errorConfig.displayError;
+ // Force data table to show no data?
+ if (context.errorConfig.resetData) {
+ this.data = [];
+ }
+ // Stop the loading indicator and reset the data table
+ // to the correct state.
+ this.useData();
+ });
+ this.fetchData.emit(context);
this.updating = true;
}
}
if (this.search.length > 0) {
this.updateFilter(true);
}
- this.loadingIndicator = false;
- this.updating = false;
+ this.reset();
if (this.updateSelectionOnRefresh) {
this.updateSelected();
}
}
+ /**
+ * Reset the data table to correct state. This includes:
+ * - Disable loading indicator
+ * - Reset 'Updating' flag
+ */
+ reset() {
+ this.loadingIndicator = false;
+ this.updating = false;
+ }
+
/**
* After updating the data, we have to update the selected items
* because details may have changed,
--- /dev/null
+export class CdTableFetchDataContext {
+ errorConfig = {
+ resetData: true, // Force data table to show no data
+ displayError: true // Show an error panel above the data table
+ };
+
+ /**
+ * The function that should be called from within the error handler
+ * of the 'fetchData' function to display the error panel and to
+ * reset the data table to the correct state.
+ */
+ error: Function;
+
+ constructor(error: () => void) {
+ this.error = error;
+ }
+}