Unverified Commit bd041f8c authored by AlexKarpov's avatar AlexKarpov Committed by GitHub
Browse files

NAS-117474 / 22.12 / Datasets table header sticky (#7091)

* NAS-117474: Datasets table header sticky update

* NAS-117474: JARVIS-117747: Datasets table sticky header fix

* NAS-117474: Datasets sticky header & side stats height fix

* NAS-117474: header sticky initially done

* NAS-117474: Sticky header on datasets table (fix)

* NAS-117474: reverted some changes & some fixes made
parent c16cb006
Showing with 11369 additions and 11873 deletions
+11369 -11873
......@@ -24,23 +24,33 @@
></ix-search-input>
</div>
<div class="dataset-tree-wrapper">
<div class="dataset-tree-inner">
<div class="dataset-tree-header">
<div>
<span class="dataset-name-header">
{{ 'Dataset Name' | translate }}
</span>
</div>
<div>
{{ 'Used' | translate }}
/
{{ 'Available' | translate }}
</div>
<div>{{ 'Encryption' | translate }}</div>
<div>{{ 'Roles' | translate }}</div>
<div
class="sticky-header"
#ixTreeHeader
(scroll)="updateScroll(scrollTypes.IxTreeHeader)"
>
<div class="dataset-tree-header" [style.width.px]="ixTreeHeaderWidth">
<div>
<span class="dataset-name-header">
{{ 'Dataset Name' | translate }}
</span>
</div>
<div>
{{ 'Used' | translate }}
/
{{ 'Available' | translate }}
</div>
<div>{{ 'Encryption' | translate }}</div>
<div>{{ 'Roles' | translate }}</div>
</div>
</div>
<div
#ixTree
class="dataset-tree-wrapper"
(scroll)="updateScroll(scrollTypes.IxTree)"
>
<div class="dataset-tree-inner" (resized)="onIxTreeWidthChange($event)">
<ix-tree
class="dataset-tree"
[dataSource]="dataSource"
......
......@@ -172,14 +172,18 @@ ix-dataset-details-panel {
width: 100%;
}
.dataset-tree-search {
background-color: var(--bg2);
padding: 16px;
.sticky-header {
overflow: hidden;
position: sticky;
top: -15px;
z-index: 1;
}
.dataset-tree-search {
background-color: var(--bg2);
padding: 16px;
}
.dataset-tree-wrapper {
display: flex;
flex: 1;
......@@ -201,17 +205,13 @@ ix-dataset-details-panel {
}
}
.dataset-tree {
border-top: 2px solid var(--lines);
}
.dataset-tree-header {
@include grid-row();
align-items: center;
background: var(--bg1);
border-bottom: 2px solid var(--lines);
color: var(--fg2);
// Override default fraction based values to avoid misaligned columns
grid-template-columns: auto 125px 125px 125px 40px;
min-height: 48px;
padding-left: 48px;
......@@ -223,7 +223,6 @@ ix-dataset-details-panel {
@media (min-width: $breakpoint-singlecolumn) {
grid-template-columns: auto 125px 125px 125px;
overflow-x: hidden;
}
> div {
......@@ -246,8 +245,10 @@ ix-dataset-details-panel {
.dataset-name-header {
background: linear-gradient(90deg, var(--bg1) 0%, var(--bg1) calc(100% - 13px), transparent 100%);
padding-left: 8px;
left: 15px;
padding-left: 0;
padding-right: 15px;
position: sticky;
}
}
......
......@@ -10,46 +10,71 @@ import {
Component,
OnInit,
AfterViewInit,
OnDestroy,
ViewChild,
ElementRef,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService } from '@ngx-translate/core';
import { filter, map } from 'rxjs/operators';
import { ResizedEvent } from 'angular-resize-event';
import { Subject, Subscription } from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
filter,
map,
} from 'rxjs/operators';
import { DatasetDetails } from 'app/interfaces/dataset.interface';
import { Job } from 'app/interfaces/job.interface';
import { WebsocketError } from 'app/interfaces/websocket-error.interface';
import { EmptyConfig, EmptyType } from 'app/modules/entity/entity-empty/entity-empty.component';
import {
EmptyConfig,
EmptyType,
} from 'app/modules/entity/entity-empty/entity-empty.component';
import { IxNestedTreeDataSource } from 'app/modules/ix-tree/ix-nested-tree-datasource';
import { flattenTreeWithFilter } from 'app/modules/ix-tree/utils/flattern-tree-with-filter';
import { DatasetTreeStore } from 'app/pages/datasets/store/dataset-store.service';
import { isRootDataset } from 'app/pages/datasets/utils/dataset.utils';
import { DialogService, WebSocketService } from 'app/services';
enum ScrollType {
IxTree = 'ixTree',
IxTreeHeader = 'ixTreeHeader',
}
@UntilDestroy()
@Component({
templateUrl: './dataset-management.component.html',
styleUrls: ['./dataset-management.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DatasetsManagementComponent implements OnInit, AfterViewInit {
export class DatasetsManagementComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('ixTreeHeader', { static: false }) ixTreeHeader: ElementRef;
@ViewChild('ixTree', { static: false }) ixTree: ElementRef;
isLoading$ = this.datasetStore.isLoading$;
selectedDataset$ = this.datasetStore.selectedDataset$;
dataSource: IxNestedTreeDataSource<DatasetDetails>;
treeControl = new NestedTreeControl<DatasetDetails, string>((dataset) => dataset.children, {
trackBy: (dataset) => dataset.id,
});
readonly hasNestedChild = (_: number, dataset: DatasetDetails): boolean => Boolean(dataset.children?.length);
treeControl = new NestedTreeControl<DatasetDetails, string>(
(dataset) => dataset.children,
{ trackBy: (dataset) => dataset.id },
);
showMobileDetails = false;
isMobileView = false;
systemDataset: string;
isLoading = true;
subscription = new Subscription();
scrollTypes = ScrollType;
ixTreeHeaderWidth: number | null = null;
entityEmptyConf: EmptyConfig = {
type: EmptyType.NoPageData,
large: true,
title: this.translate.instant('No Datasets'),
message: `${this.translate.instant(
'It seems you haven\'t configured pools yet.',
"It seems you haven't configured pools yet.",
)} ${this.translate.instant(
'Please click the button below to create a pool.',
)}`,
......@@ -59,7 +84,8 @@ export class DatasetsManagementComponent implements OnInit, AfterViewInit {
},
};
isLoading = true;
readonly hasNestedChild = (_: number, dataset: DatasetDetails): boolean => Boolean(dataset.children?.length);
private readonly scrollSubject = new Subject<number>();
constructor(
private ws: WebSocketService,
......@@ -70,37 +96,15 @@ export class DatasetsManagementComponent implements OnInit, AfterViewInit {
protected translate: TranslateService,
private dialogService: DialogService,
private breakpointObserver: BreakpointObserver,
) { }
) {}
ngOnInit(): void {
this.datasetStore.loadDatasets();
this.listenForRouteChanges();
this.setupTree();
this.ws.call('systemdataset.config').pipe(
map((config) => config.pool),
untilDestroyed(this),
).subscribe({
next: (systemDataset) => {
this.systemDataset = systemDataset;
},
error: this.handleError,
});
this.isLoading$
.pipe(untilDestroyed(this))
.subscribe((isLoading) => {
this.isLoading = isLoading;
this.cdr.markForCheck();
});
}
handleError = (error: WebsocketError | Job<null, unknown[]>): void => {
this.dialogService.errorReportMiddleware(error);
};
isSystemDataset(dataset: DatasetDetails): boolean {
return isRootDataset(dataset) && this.systemDataset === dataset.name;
this.listenForRouteChanges();
this.loadSystemDatasetConfig();
this.listenForLoading();
this.listenForDatasetScrolling();
}
ngAfterViewInit(): void {
......@@ -118,34 +122,89 @@ export class DatasetsManagementComponent implements OnInit, AfterViewInit {
});
}
onSearch(query: string): void {
this.dataSource.filter(query);
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
private setupTree(): void {
this.datasetStore.datasets$
.pipe(untilDestroyed(this))
loadSystemDatasetConfig(): void {
this.ws
.call('systemdataset.config')
.pipe(
map((config) => config.pool),
untilDestroyed(this),
)
.subscribe({
next: (datasets) => {
this.sortDatasetsByName(datasets);
this.createDataSource(datasets);
this.treeControl.dataNodes = datasets;
this.cdr.markForCheck();
if (!datasets.length) {
return;
}
const routeDatasetId = this.activatedRoute.snapshot.paramMap.get('datasetId');
if (routeDatasetId) {
this.datasetStore.selectDatasetById(routeDatasetId);
} else {
const firstNode = this.treeControl.dataNodes[0];
this.router.navigate(['/datasets', firstNode.id]);
}
next: (systemDataset) => {
this.systemDataset = systemDataset;
},
error: this.handleError,
});
}
listenForLoading(): void {
this.isLoading$.pipe(untilDestroyed(this)).subscribe((isLoading) => {
this.isLoading = isLoading;
this.cdr.markForCheck();
});
}
handleError = (error: WebsocketError | Job<null, unknown[]>): void => {
this.dialogService.errorReportMiddleware(error);
};
isSystemDataset(dataset: DatasetDetails): boolean {
return isRootDataset(dataset) && this.systemDataset === dataset.name;
}
updateScroll(type: ScrollType): void {
this.scrollSubject.next(
type === ScrollType.IxTree ? this.ixTree.nativeElement.scrollLeft : this.ixTreeHeader.nativeElement.scrollLeft,
);
}
onIxTreeWidthChange(event: ResizedEvent): void {
this.ixTreeHeaderWidth = Math.round(event.newRect.width);
}
private listenForDatasetScrolling(): void {
this.subscription.add(
this.scrollSubject
.pipe(debounceTime(0), distinctUntilChanged(), untilDestroyed(this))
.subscribe({
next: (scrollLeft: number) => {
this.ixTreeHeader.nativeElement.scrollLeft = scrollLeft;
this.ixTree.nativeElement.scrollLeft = scrollLeft;
},
}),
);
}
onSearch(query: string): void {
this.dataSource.filter(query);
}
private setupTree(): void {
this.datasetStore.datasets$.pipe(untilDestroyed(this)).subscribe({
next: (datasets) => {
this.sortDatasetsByName(datasets);
this.createDataSource(datasets);
this.treeControl.dataNodes = datasets;
this.cdr.markForCheck();
if (!datasets.length) {
return;
}
const routeDatasetId = this.activatedRoute.snapshot.paramMap.get('datasetId');
if (routeDatasetId) {
this.datasetStore.selectDatasetById(routeDatasetId);
} else {
const firstNode = this.treeControl.dataNodes[0];
this.router.navigate(['/datasets', firstNode.id]);
}
},
error: this.handleError,
});
this.datasetStore.selectedBranch$
.pipe(filter(Boolean), untilDestroyed(this))
......@@ -158,13 +217,15 @@ export class DatasetsManagementComponent implements OnInit, AfterViewInit {
}
private listenForRouteChanges(): void {
this.activatedRoute.params.pipe(
map((params) => params.datasetId),
filter(Boolean),
untilDestroyed(this),
).subscribe((datasetId: string) => {
this.datasetStore.selectDatasetById(datasetId);
});
this.activatedRoute.params
.pipe(
map((params) => params.datasetId),
filter(Boolean),
untilDestroyed(this),
)
.subscribe((datasetId: string) => {
this.datasetStore.selectDatasetById(datasetId);
});
}
private createDataSource(datasets: DatasetDetails[]): void {
......
......@@ -33,7 +33,7 @@
}
&:first-child {
left: 0;
left: 15px;
position: sticky;
@media (max-width: $breakpoint-tablet) {
......
......@@ -10,6 +10,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSortModule } from '@angular/material/sort';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { AngularResizeEventModule } from 'angular-resize-event';
import { ChartsModule } from 'ng2-charts';
import { NgxFilesizeModule } from 'ngx-filesize';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
......@@ -81,6 +82,7 @@ import { DatasetNodeComponent } from './components/dataset-node/dataset-node.com
CoreComponents,
AppLoaderModule,
SnapshotsModule,
AngularResizeEventModule,
],
declarations: [
DatasetsManagementComponent,
......
......@@ -34,7 +34,7 @@
}
&:first-child {
left: 0;
left: 15px;
position: sticky;
@media (max-width: $breakpoint-tablet) {
......
......@@ -23,7 +23,7 @@ $columns: 7;
min-height: 48px;
&:first-child {
left: 0;
left: 15px;
position: sticky;
}
......
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment