diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts index b7af4a19ea..12db72f9ba 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts @@ -89,7 +89,7 @@ export class ApplicationTabsBaseComponent implements OnInit, OnDestroy { switchMap(space => this.currentUserPermissionsService.can(CfCurrentUserPermissions.APPLICATION_VIEW_ENV_VARS, this.applicationService.cfGuid, space.metadata.guid) ), - map(can => !can) + map(can => !can), ); this.tabLinks = [ diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-endpoints/cf-endpoints-list-config.service.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-endpoints/cf-endpoints-list-config.service.ts index a72c7221e7..f38a7a0705 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-endpoints/cf-endpoints-list-config.service.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-endpoints/cf-endpoints-list-config.service.ts @@ -36,7 +36,7 @@ export class CFEndpointsListConfigService implements IListConfig paginationMonitorFactory: PaginationMonitorFactory, entityMonitorFactory: EntityMonitorFactory, internalEventMonitorFactory: InternalEventMonitorFactory, - endpointsListConfigService: EndpointsListConfigService + endpointsListConfigService: EndpointsListConfigService, ) { this.columns = endpointsListConfigService.columns.filter(column => { return column.columnId !== 'type'; @@ -46,7 +46,8 @@ export class CFEndpointsListConfigService implements IListConfig this, paginationMonitorFactory, entityMonitorFactory, - internalEventMonitorFactory); + internalEventMonitorFactory, + ); } public getColumns = () => this.columns; public getGlobalActions = () => []; diff --git a/src/frontend/packages/core/sass/components/json-schema-form.scss b/src/frontend/packages/core/sass/components/json-schema-form.scss new file mode 100644 index 0000000000..9f004b481a --- /dev/null +++ b/src/frontend/packages/core/sass/components/json-schema-form.scss @@ -0,0 +1,5 @@ +json-schema-form { + mat-error.mat-error { + margin-bottom: .05rem; + } +} diff --git a/src/frontend/packages/core/sass/components/text-status.theme.scss b/src/frontend/packages/core/sass/components/text-status.theme.scss index 68b3d1821b..a054375175 100644 --- a/src/frontend/packages/core/sass/components/text-status.theme.scss +++ b/src/frontend/packages/core/sass/components/text-status.theme.scss @@ -6,6 +6,7 @@ $status-warning: map-get($status-colors, warning); $status-danger: map-get($status-colors, danger); $status-tentative: map-get($status-colors, tentative); + $status-info: map-get($status-colors, info); .text-success { color: $status-success; @@ -23,6 +24,10 @@ color: $status-tentative; } + .text-info { + color: $status-info; + } + // Border colors .border-success { @@ -41,4 +46,8 @@ border-color: $status-tentative; } + .border-info { + border-color: $status-info; + } + } diff --git a/src/frontend/packages/core/sass/mat-desktop.scss b/src/frontend/packages/core/sass/mat-desktop.scss index 3538518e13..7e11af970e 100644 --- a/src/frontend/packages/core/sass/mat-desktop.scss +++ b/src/frontend/packages/core/sass/mat-desktop.scss @@ -17,6 +17,13 @@ $desktop-toggle-button-item-height: $desktop-menu-item-height - 2px; line-height: $desktop-menu-item-height; min-width: 128px; padding: 0 24px; + + &.hasIcon { + padding-left: 16px; + .mat-icon { + margin-right: 12px; + } + } } .mat-menu-panel { @@ -56,6 +63,10 @@ $desktop-toggle-button-item-height: $desktop-menu-item-height - 2px; font-size: $desktop-font-size; } + .mat-slide-toggle-label { + font-size: $desktop-font-size; + } + // Allow a slightly-wider snackbar on desktop .mat-snack-bar-container { max-width: 40vw; diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.html b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.html index 2c4b59dc38..d7ca4b06f5 100644 --- a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.html +++ b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.html @@ -16,10 +16,18 @@
-
+
-
- {{ activeTabLabel }} +
+ {{ data[0] }} +
+
+ + {{ breadcrumbDef.value }} + {{ breadcrumbDef.value }} + chevron_right +
diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss index 26a2d129ce..34b3de01a3 100644 --- a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss +++ b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss @@ -79,6 +79,11 @@ $app-header-height: 56px; } .page-header-sub-nav { + $breadcrumb-opacity: .7; + $breadcrumb-hover-opacity: 1; + $breadcrumb-padding: 3px; + $font-size: 20px; + align-items: center; border-bottom: 1px solid rgba(0, 0, 0, .1); display: flex; @@ -91,7 +96,6 @@ $app-header-height: 56px; } &__title { display: none; - $font-size: 20px; font-size: $font-size; font-weight: bold; line-height: $font-size; @@ -106,4 +110,34 @@ $app-header-height: 56px; opacity: .6; width: 100%; } + &__breadcrumb { + font-size: $font-size; + font-weight: bold; + line-height: $font-size; + } + &__breadcrumb, + &__breadcrumb-separator { + opacity: $breadcrumb-opacity; + } + &__breadcrumb-separator { + font-size: 24px; + margin: 0 $breadcrumb-padding; + user-select: none; + } + &__breadcrumbs { + align-items: center; + display: flex; + justify-content: center; + } + &__breadcrumb-nolink { + opacity: $breadcrumb-hover-opacity; + } + &__breadcrumb-link { + cursor: pointer; + outline: none; + &:hover { + opacity: $breadcrumb-hover-opacity; + } + } + } diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts index 3a4844e107..b7da02c398 100644 --- a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts +++ b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts @@ -16,8 +16,10 @@ import { selectDashboardState } from '../../../../../store/src/selectors/dashboa import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; import { CustomizationService } from '../../../core/customizations.types'; import { EndpointsService } from '../../../core/endpoints.service'; +import { IHeaderBreadcrumbLink } from '../../../shared/components/page-header/page-header.types'; import { SidePanelService } from '../../../shared/services/side-panel.service'; import { TabNavService } from '../../../tab-nav.service'; +import { IPageSideNavTab } from '../page-side-nav/page-side-nav.component'; import { PageHeaderService } from './../../../core/page-header-service/page-header.service'; import { SideNavItem } from './../side-nav/side-nav.component'; @@ -30,12 +32,12 @@ import { SideNavItem } from './../side-nav/side-nav.component'; export class DashboardBaseComponent implements OnInit, OnDestroy, AfterViewInit { public activeTabLabel$: Observable; - public subNavData$: Observable<[string, Portal]>; + public subNavData$: Observable<[string, Portal, IPageSideNavTab, IHeaderBreadcrumbLink[]]>; public isMobile$: Observable; public sideNavMode$: Observable; public sideNavMode: string; - public mainNavState$: Observable<{ mode: string; opened: boolean; iconMode: boolean }>; - public rightNavState$: Observable<{ opened: boolean, component?: object, props?: object }>; + public mainNavState$: Observable<{ mode: string; opened: boolean; iconMode: boolean; }>; + public rightNavState$: Observable<{ opened: boolean, component?: object, props?: object; }>; private dashboardState$: Observable; public noMargin$: Observable; private closeSub: Subscription; @@ -133,8 +135,9 @@ export class DashboardBaseComponent implements OnInit, OnDestroy, AfterViewInit this.tabNavService.getCurrentTabHeaderObservable().pipe( startWith(null) ), - this.tabNavService.tabSubNav$ - ); + this.tabNavService.tabSubNav$, + this.tabNavService.tabSubNavBreadcrumbs$ + ).pipe(map(([tabNav, tabSubNav, tabSubNavBreadcrumb]) => [tabNav ? tabNav.label : null, tabSubNav, tabNav, tabSubNavBreadcrumb])); // Register all health checks for endpoint types that support this entityCatalog.getAllEndpointTypes().forEach(epType => { diff --git a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss index d7c1ebb669..193c3fb671 100644 --- a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss +++ b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss @@ -11,6 +11,7 @@ $side-nav-bottom-color: map-get($app-theme, subdued-color); $background-color: map-get($app-theme, app-background-color); $darker-background-color: darken($background-color, 4%); + $is-dark: map-get($theme, is-dark); .page-side-nav { background-color: $background-color; @@ -27,6 +28,10 @@ &--active { background-color: transparentize($primary-color, .9); color: $primary-color; + @if $is-dark == true { + background-color: transparentize($primary-color, .8); + color: mat-contrast($primary, 500); + } } } &__items-header { diff --git a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts index ab5c62d690..aa0fb03459 100644 --- a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts +++ b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; import { AppState } from '../../../../../store/src/app-state'; import { EntityServiceFactory } from '../../../../../store/src/entity-service-factory.service'; @@ -54,7 +55,7 @@ export class PageSideNavComponent implements OnInit { } ngOnInit() { - this.activeTab$ = this.tabNavService.getCurrentTabHeaderObservable(); + this.activeTab$ = this.tabNavService.getCurrentTabHeaderObservable().pipe(map(item => item ? item.label : null)); } } diff --git a/src/frontend/packages/core/src/shared/components/boolean-indicator/boolean-indicator.component.ts b/src/frontend/packages/core/src/shared/components/boolean-indicator/boolean-indicator.component.ts index c5aca6d528..be48e1d828 100644 --- a/src/frontend/packages/core/src/shared/components/boolean-indicator/boolean-indicator.component.ts +++ b/src/frontend/packages/core/src/shared/components/boolean-indicator/boolean-indicator.component.ts @@ -38,7 +38,16 @@ export class BooleanIndicatorComponent { // Invert the text labels with the icons (No text for yes value and vice-versa) @Input() inverse = false; // Should we use a subtle display - this won't show the No option as danger (typically red) - @Input() subtle = true; + private pSubtle = true; + @Input() + get subtle(): boolean { + return this.pSubtle; + } + set subtle(subtle: boolean) { + this.pSubtle = subtle; + this.updateBooleanOutput(); + } + @Input() showText = true; private icons = { diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html index ea0067edbc..b0747565c1 100644 --- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html +++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html @@ -14,6 +14,20 @@
{{ labelSingular && value === '1' ? labelSingular : label }}
+
+
+ info + {{ alertInfo.info }} +
+
+ warning +
{{ alertInfo.warning }}
+
+
+ error + {{ alertInfo.error }} +
+
diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss index 482e94a189..78aa3599eb 100644 --- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss +++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss @@ -40,4 +40,26 @@ &__limit { font-size: 18px; } + &__alerts { + cursor: pointer; + display: flex; + flex: 0; + flex-direction: column; + } + &__alert-badge { + align-items: center; + border-radius: 4px; + color: #fff; + display: flex; + font-size: 14px; + margin-bottom: 2px; + padding: 2px 4px; + + &> mat-icon { + font-size: 16px; + height: 16px; + margin-right: 2px; + width: 16x; + } + } } diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss index eba218e2e2..2493db6d56 100644 --- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss +++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss @@ -1,6 +1,12 @@ @mixin app-card-number-metric-theme($theme, $app-theme) { $status-colors: map-get($app-theme, status); $subdued: mat-color($app-theme, subdued-color); + + $status-colors: map-get($app-theme, status); + $status-warning: map-get($status-colors, warning); + $status-danger: map-get($status-colors, danger); + $status-info: map-get($status-colors, info); + .number-metric-card { &__icon, &__anchor, @@ -9,4 +15,16 @@ color: $subdued; } } -} + + .number-metric-card__alert-badge { + &-error { + background-color: $status-danger; + } + &-info { + background-color: $status-info; + } + &-warning { + background-color: $status-warning; + } + } +} \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts index f0170d28b4..fa02ab447e 100644 --- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts +++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { Store } from '@ngrx/store'; import { BehaviorSubject } from 'rxjs'; @@ -8,6 +8,14 @@ import { StratosStatus } from '../../../../../../store/src/types/shared.types'; import { UtilsService } from '../../../../core/utils.service'; import { determineCardStatus } from '../card-status/card-status.component'; +enum AlertLevel { + OK = 0, + Info, + Warning, + Error, + Unknown, +} + @Component({ selector: 'app-card-number-metric', templateUrl: './card-number-metric.component.html', @@ -26,6 +34,16 @@ export class CardNumberMetricComponent implements OnInit, OnChanges { @Input() textOnly = false; @Input() labelAtTop = false; @Input() link: () => void | string; + @Output() showAlerts = new EventEmitter(); + + @Input('alerts') + set alerts(alerts) { + if (alerts) { + this.processAlerts(alerts); + } + } + + alertInfo: any; formattedValue: string; formattedLimit: string; @@ -102,4 +120,31 @@ export class CardNumberMetricComponent implements OnInit, OnChanges { this.link(); } } + + processAlerts(alerts) { + this.alertInfo = { + info: 0, + warning: 0, + error: 0 + }; + + alerts.forEach((alert) => { + switch (alert.level as AlertLevel) { + case AlertLevel.Warning: + this.alertInfo.warning++; + break; + case AlertLevel.Error: + this.alertInfo.error++; + break; + case AlertLevel.Info: + this.alertInfo.info++; + break; + } + }); + } + + public alertsClicked() { + this.showAlerts.emit(this.alertInfo); + } + } diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts index 979c260eb6..5d12e93d83 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnDestroy } from '@angular/core'; -import { Subscription, Observable } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { objectHelper } from '../../../../../core/helper-classes/object.helpers'; import { pathGet } from '../../../../../core/utils.service'; @@ -22,6 +22,7 @@ export class TableCellDefaultComponent extends TableCellCustom implements this.pRow = row; if (row) { this.setValue(row, this.schemaKey); + this.setSyncLink(); } } @@ -32,6 +33,7 @@ export class TableCellDefaultComponent extends TableCellCustom implements this.pSchemaKey = schemaKey; if (this.row) { this.setValue(this.row, schemaKey); + this.setSyncLink(); } } @@ -63,7 +65,7 @@ export class TableCellDefaultComponent extends TableCellCustom implements } private setSyncLink() { - if (!this.cellDefinition.getLink) { + if (!this.cellDefinition || !this.cellDefinition.getLink) { return; } const linkValue = this.cellDefinition.getLink(this.row); diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-actions/table-cell-actions.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-actions/table-cell-actions.component.html index 01680e8f60..107f5c8149 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-actions/table-cell-actions.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-actions/table-cell-actions.component.html @@ -3,8 +3,9 @@ - diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts index afacea4130..22f224cbe5 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts @@ -11,7 +11,8 @@ import { InternalEventMonitorFactory } from '../../../../../../../store/src/moni import { PaginationMonitorFactory } from '../../../../../../../store/src/monitors/pagination-monitor.factory'; import { endpointEntitiesSelector } from '../../../../../../../store/src/selectors/endpoint.selectors'; import { EndpointModel } from '../../../../../../../store/src/types/endpoint.types'; -import { ListDataSource } from '../../data-sources-controllers/list-data-source'; +import { PaginationEntityState } from '../../../../../../../store/src/types/pagination.types'; +import { DataFunction, DataFunctionDefinition, ListDataSource } from '../../data-sources-controllers/list-data-source'; import { IListDataSourceConfig } from '../../data-sources-controllers/list-data-source-config'; import { RowsState } from '../../data-sources-controllers/list-data-source-types'; import { TableRowStateManager } from '../../list-table/table-row/table-row-state-manager'; @@ -27,11 +28,15 @@ export function syncPaginationSection( store.dispatch(new CreatePagination( action, paginationKey, - action.paginationKey + action.paginationKey, + action.initialParams )); } export class BaseEndpointsDataSource extends ListDataSource { + + public static typeFilterKey = 'endpointType'; + store: Store; /** * Used to distinguish between data sources providing all endpoints or those that only provide endpoints matching this value. @@ -49,7 +54,8 @@ export class BaseEndpointsDataSource extends ListDataSource { paginationMonitorFactory: PaginationMonitorFactory, entityMonitorFactory: EntityMonitorFactory, internalEventMonitorFactory: InternalEventMonitorFactory, - onlyConnected = true + onlyConnected = true, + filterByType = false ) { const rowStateHelper = new ListRowSateHelper(); const { rowStateManager, sub } = rowStateHelper.getRowStateManager( @@ -73,21 +79,28 @@ export class BaseEndpointsDataSource extends ListDataSource { () => this.store.dispatch(action) ); + const transformEntities: (DataFunctionDefinition | DataFunction)[] = [{ + type: 'filter', + field: 'name' + }]; + if (dsEndpointType || onlyConnected) { + transformEntities.push((entities: EndpointModel[]) => { + return dsEndpointType || onlyConnected ? entities.filter(endpoint => { + return (!onlyConnected || endpoint.connectionStatus === 'connected') && + (!dsEndpointType || endpoint.cnsi_type === dsEndpointType); + }) : entities; + }); + } + if (filterByType) { + transformEntities.push((entities: EndpointModel[], paginationState: PaginationEntityState) => + BaseEndpointsDataSource.endpointTypeFilter(entities, paginationState) + ); + } + super({ ...config, paginationKey: action.paginationKey, - transformEntities: [ - (entities: EndpointModel[]) => { - return dsEndpointType || onlyConnected ? entities.filter(endpoint => { - return (!onlyConnected || endpoint.connectionStatus === 'connected') && - (!dsEndpointType || endpoint.cnsi_type === dsEndpointType); - }) : entities; - }, - { - type: 'filter', - field: 'name' - }, - ], + transformEntities, }); this.dsEndpointType = dsEndpointType; } @@ -154,4 +167,18 @@ export class BaseEndpointsDataSource extends ListDataSource { })), ).subscribe(); } + + static endpointTypeFilter: DataFunction = (entities: EndpointModel[], paginationState: PaginationEntityState) => { + if ( + !paginationState.clientPagination || + !paginationState.clientPagination.filter || + !paginationState.clientPagination.filter.items[BaseEndpointsDataSource.typeFilterKey] + ) { + return entities; + } + const searchTerm = paginationState.clientPagination.filter.items[BaseEndpointsDataSource.typeFilterKey]; + return searchTerm ? + entities.filter(endpoint => endpoint.cnsi_type === searchTerm) : + entities; + }; } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-data-source.ts index cdb066f099..8ae915ccf3 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-data-source.ts @@ -18,7 +18,8 @@ export class EndpointsDataSource extends BaseEndpointsDataSource { listConfig: IListConfig, paginationMonitorFactory: PaginationMonitorFactory, entityMonitorFactory: EntityMonitorFactory, - internalEventMonitorFactory: InternalEventMonitorFactory + internalEventMonitorFactory: InternalEventMonitorFactory, + filterByType = false ) { super( store, @@ -28,7 +29,8 @@ export class EndpointsDataSource extends BaseEndpointsDataSource { paginationMonitorFactory, entityMonitorFactory, internalEventMonitorFactory, - false + false, + filterByType ); } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index 0f091dbd85..217c3b43b9 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -1,18 +1,29 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { filter } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, of } from 'rxjs'; +import { debounceTime, filter, map } from 'rxjs/operators'; import { ListView } from '../../../../../../../store/src/actions/list.actions'; +import { SetClientFilter } from '../../../../../../../store/src/actions/pagination.actions'; import { AppState } from '../../../../../../../store/src/app-state'; import { entityCatalog } from '../../../../../../../store/src/entity-catalog/entity-catalog'; import { FavoritesConfigMapper } from '../../../../../../../store/src/favorite-config-mapper'; import { EntityMonitorFactory } from '../../../../../../../store/src/monitors/entity-monitor.factory.service'; import { InternalEventMonitorFactory } from '../../../../../../../store/src/monitors/internal-event-monitor.factory'; import { PaginationMonitorFactory } from '../../../../../../../store/src/monitors/pagination-monitor.factory'; +import { stratosEntityCatalog } from '../../../../../../../store/src/stratos-entity-catalog'; import { EndpointModel } from '../../../../../../../store/src/types/endpoint.types'; +import { PaginationEntityState } from '../../../../../../../store/src/types/pagination.types'; import { createTableColumnFavorite } from '../../list-table/table-cell-favorite/table-cell-favorite.component'; import { ITableColumn } from '../../list-table/table.types'; -import { IListAction, IListConfig, ListViewTypes } from '../../list.component.types'; +import { + IListAction, + IListConfig, + IListMultiFilterConfig, + IListMultiFilterConfigItem, + ListViewTypes, +} from '../../list.component.types'; +import { BaseEndpointsDataSource } from './base-endpoints-data-source'; import { EndpointCardComponent } from './endpoint-card/endpoint-card.component'; import { EndpointListHelper } from './endpoint-list.helpers'; import { EndpointsDataSource } from './endpoints-data-source'; @@ -110,12 +121,14 @@ export class EndpointsListConfigService implements IListConfig { (row: EndpointModel) => favoritesConfigMapper.getFavoriteEndpointFromEntity(row) ); this.columns.push(favoriteCell); + this.dataSource = new EndpointsDataSource( this.store, this, paginationMonitorFactory, entityMonitorFactory, - internalEventMonitorFactory + internalEventMonitorFactory, + true ); } @@ -124,9 +137,68 @@ export class EndpointsListConfigService implements IListConfig { public getSingleActions = () => this.singleActions; public getColumns = () => this.columns; public getDataSource = () => this.dataSource; - public getMultiFiltersConfigs = () => []; + + public getMultiFiltersConfigs = (): IListMultiFilterConfig[] => [this.createEndpointTypeFilter()]; private getEndpointTypeString(endpoint: EndpointModel): string { return entityCatalog.getEndpoint(endpoint.cnsi_type, endpoint.sub_type).definition.label; } + + private createEndpointTypeFilter(): IListMultiFilterConfig { + return { + key: BaseEndpointsDataSource.typeFilterKey, + label: 'Endpoint Type', + list$: combineLatest([ + stratosEntityCatalog.endpoint.store.getPaginationMonitor().currentPage$, + stratosEntityCatalog.endpoint.store.getPaginationMonitor().pagination$ + ]).pipe( + debounceTime(100),// This can get pretty spammy, to help protect resetEndpointTypeFilter allow a pause + filter(([endpoints, pagination]) => !!endpoints), + map(([endpoints, pagination]) => { + // Provide a list of endpoint types only if there are more than two registered endpoint types + const types: { [type: string]: boolean; } = {}; + for (const endpoint of endpoints) { + types[endpoint.cnsi_type] = true; + } + if (Object.values(types).filter(type => type).length < 2) { + // If we're going to hid the endpoint filter ensure any existing filter value is reset + this.resetEndpointTypeFilter(pagination); + return []; + } + return entityCatalog.getAllBaseEndpointTypes() + .sort((a, b) => a.definition.renderPriority - b.definition.renderPriority) + .filter(et => types[et.type]) + .map(et => { + const res: IListMultiFilterConfigItem = { + label: et.definition.label, + item: et, + value: et.type + }; + return res; + }); + }) + ), + loading$: of(false), + select: new BehaviorSubject(undefined) + }; + } + + private resetEndpointTypeFilter(pagination: PaginationEntityState) { + if ( + pagination.clientPagination && + pagination.clientPagination.filter && + pagination.clientPagination.filter.items[BaseEndpointsDataSource.typeFilterKey] + ) { + const clientPaginationFilter = { + ...pagination.clientPagination.filter, + items: { + ...pagination.clientPagination.filter.items, + [BaseEndpointsDataSource.typeFilterKey]: null + } + }; + this.store.dispatch( + new SetClientFilter(this.dataSource.masterAction, this.dataSource.paginationKey, clientPaginationFilter) + ); + } + } } diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.html b/src/frontend/packages/core/src/shared/components/list/list.component.html index 28ee1e36ef..db67aaa1e0 100644 --- a/src/frontend/packages/core/src/shared/components/list/list.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list.component.html @@ -45,11 +45,12 @@
- + - {{ multiFilterManager.allLabel }} + {{ multiFilterManager.allLabel }} {{selectItem.label}} diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.types.ts b/src/frontend/packages/core/src/shared/components/list/list.component.types.ts index 98b47f4fa9..174fc5eaa9 100644 --- a/src/frontend/packages/core/src/shared/components/list/list.component.types.ts +++ b/src/frontend/packages/core/src/shared/components/list/list.component.types.ts @@ -1,7 +1,7 @@ import { Injectable, Type } from '@angular/core'; import moment from 'moment'; import { BehaviorSubject, combineLatest, Observable, of as observableOf } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; +import { first, map, startWith } from 'rxjs/operators'; import { ListView } from '../../../../../store/src/actions/list.actions'; import { ActionState } from '../../../../../store/src/reducers/api-request-reducer/types'; @@ -130,9 +130,11 @@ export interface IListMultiFilterConfig { key: string; label: string; allLabel?: string; + hideAllOption?: boolean; list$: Observable; loading$: Observable; select: BehaviorSubject; + autoSelectFirst?: boolean; } export interface IListFilter { @@ -203,11 +205,12 @@ export class MultiFilterManager { public filterIsReady$: Observable; public filterItems$: Observable; public hasItems$: Observable; - public hasOneItem$: Observable; + public hasOneOrLessItems$: Observable; public value: string; public filterKey: string; public allLabel: string; + public hideAllOption = false; constructor( public multiFilterConfig: IListMultiFilterConfig, @@ -215,10 +218,20 @@ export class MultiFilterManager { ) { this.filterKey = this.multiFilterConfig.key; this.allLabel = multiFilterConfig.allLabel || 'All'; + this.hideAllOption = multiFilterConfig.hideAllOption || false; this.filterItems$ = this.getItemObservable(multiFilterConfig); - this.hasOneItem$ = this.filterItems$.pipe(map(items => items.length === 1)); + this.hasOneOrLessItems$ = this.filterItems$.pipe(map(items => items.length <= 1)); this.hasItems$ = this.filterItems$.pipe(map(items => !!items.length)); this.filterIsReady$ = this.getReadyObservable(multiFilterConfig, dataSource, this.hasItems$); + + // Also select the first option if configured + if (multiFilterConfig.autoSelectFirst) { + this.filterItems$.pipe(first()).subscribe(options => { + if (options && options.length > 0) { + this.selectItem(options[0].value); + } + }); + } } private getReadyObservable( diff --git a/src/frontend/packages/core/src/shared/components/page-sub-nav/page-sub-nav.component.ts b/src/frontend/packages/core/src/shared/components/page-sub-nav/page-sub-nav.component.ts index eb37fb7ae0..604a2680e7 100644 --- a/src/frontend/packages/core/src/shared/components/page-sub-nav/page-sub-nav.component.ts +++ b/src/frontend/packages/core/src/shared/components/page-sub-nav/page-sub-nav.component.ts @@ -1,7 +1,8 @@ import { TemplatePortal } from '@angular/cdk/portal'; -import { AfterViewInit, Component, OnDestroy, TemplateRef, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, Input, OnDestroy, TemplateRef, ViewChild } from '@angular/core'; import { TabNavService } from '../../../tab-nav.service'; +import { IHeaderBreadcrumbLink } from '../page-header/page-header.types'; @Component({ selector: 'app-page-sub-nav', @@ -9,6 +10,12 @@ import { TabNavService } from '../../../tab-nav.service'; styleUrls: ['./page-sub-nav.component.scss'] }) export class PageSubNavComponent implements AfterViewInit, OnDestroy { + + @Input('breadcrumbs') + set breadcrumbs(crumbs: IHeaderBreadcrumbLink[]) { + this.tabNavService.setSubNavBreadcrumbs(crumbs); + } + @ViewChild('subNavTmpl', { static: true }) subNavTmpl: TemplateRef; constructor(private tabNavService: TabNavService) { } diff --git a/src/frontend/packages/core/src/shared/components/ssh-viewer/ssh-viewer.component.ts b/src/frontend/packages/core/src/shared/components/ssh-viewer/ssh-viewer.component.ts index 8d9cedccb8..8c075008f8 100644 --- a/src/frontend/packages/core/src/shared/components/ssh-viewer/ssh-viewer.component.ts +++ b/src/frontend/packages/core/src/shared/components/ssh-viewer/ssh-viewer.component.ts @@ -122,7 +122,7 @@ export class SshViewerComponent implements OnInit, OnDestroy { this.xterm.write(String.fromCharCode(parseInt(c, 16))); } } else { - console.log('Error') + console.error('Error: ', this.errorMessage) const eMsg = this.errorMessage; this.errorMessage = eMsg; } @@ -145,17 +145,17 @@ export class SshViewerComponent implements OnInit, OnDestroy { parseInt(chars[1], 16) === 93 && parseInt(chars[2], 16) === 50 && parseInt(chars[3], 16) === 59) { - let title = ''; - for (let i = 4; i < chars.length - 1; i++) { - title += String.fromCharCode(parseInt(chars[i], 16)); - } - if (title.length > 0 && title.charAt(0) === '!') { - this.errorMessage = title.substr(1); - console.log(this.errorMessage); - return true; - } - this.message = title; + let title = ''; + for (let i = 4; i < chars.length - 1; i++) { + title += String.fromCharCode(parseInt(chars[i], 16)); } + if (title.length > 0 && title.charAt(0) === '!') { + this.errorMessage = title.substr(1); + console.error(this.errorMessage); + return true; + } + this.message = title; + } return false; } } diff --git a/src/frontend/packages/core/src/shared/components/stepper/step/step.component.ts b/src/frontend/packages/core/src/shared/components/stepper/step/step.component.ts index 73f164301a..9ab65bcd51 100644 --- a/src/frontend/packages/core/src/shared/components/stepper/step/step.component.ts +++ b/src/frontend/packages/core/src/shared/components/stepper/step/step.component.ts @@ -21,7 +21,7 @@ export interface StepOnNextResult { data?: any; } -export type StepOnNextFunction = () => Observable; +export type StepOnNextFunction = (index: number, step: StepComponent) => Observable; @Component({ selector: 'app-step', diff --git a/src/frontend/packages/core/src/shared/components/stepper/steppers/steppers.component.ts b/src/frontend/packages/core/src/shared/components/stepper/steppers/steppers.component.ts index 6092e11cd5..6c87a6e2a4 100644 --- a/src/frontend/packages/core/src/shared/components/stepper/steppers/steppers.component.ts +++ b/src/frontend/packages/core/src/shared/components/stepper/steppers/steppers.component.ts @@ -115,7 +115,7 @@ export class SteppersComponent implements OnInit, AfterContentInit, OnDestroy { if (this.currentIndex < this.steps.length) { const step = this.steps[this.currentIndex]; step.busy = true; - const obs$ = step.onNext(); + const obs$ = step.onNext(this.currentIndex, step); if (!(obs$ instanceof Observable)) { return; } diff --git a/src/frontend/packages/core/src/shared/services/session.service.ts b/src/frontend/packages/core/src/shared/services/session.service.ts new file mode 100644 index 0000000000..1c133f2106 --- /dev/null +++ b/src/frontend/packages/core/src/shared/services/session.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { first, map } from 'rxjs/operators'; + +import { GeneralEntityAppState } from '../../../../store/src/app-state'; +import { selectSessionData } from '../../../../store/src/reducers/auth.reducer'; + +@Injectable() +export class SessionService { + + constructor(private store: Store) { } + + isTechPreview(): Observable { + return this.store.select(selectSessionData()).pipe( + first(), + map(sessionData => sessionData.config.enableTechPreview || false) + ) + } +} \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/services/snackbar.service.ts b/src/frontend/packages/core/src/shared/services/snackbar.service.ts index 13ba4eb2c6..bfa94ce28e 100644 --- a/src/frontend/packages/core/src/shared/services/snackbar.service.ts +++ b/src/frontend/packages/core/src/shared/services/snackbar.service.ts @@ -4,26 +4,31 @@ import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material/s import { SnackBarReturnComponent } from '../components/snackbar-return/snackbar-return.component'; /** - * Servicve for showing snackbars + * Service for showing snackbars */ @Injectable({ providedIn: 'root', }) export class SnackBarService { - constructor(public snackBar: MatSnackBar) {} + constructor(public snackBar: MatSnackBar) { } private snackBars: MatSnackBarRef[] = []; public show(message: string, closeMessage?: string, duration: number = 5000) { this.snackBars.push(this.snackBar.open(message, closeMessage, { - duration: closeMessage ? null :duration + duration: closeMessage ? null : duration })); } - public showReturn(message: string, returnUrl: string, returnLabel: string) { + public showReturn(message: string, returnUrl: string | string[], returnLabel: string, duration?: number) { this.snackBars.push(this.snackBar.openFromComponent(SnackBarReturnComponent, { - data: { message, returnUrl, returnLabel } + duration, + data: { + message, + returnUrl, + returnLabel, + } })); } diff --git a/src/frontend/packages/core/src/shared/shared.module.ts b/src/frontend/packages/core/src/shared/shared.module.ts index e18dd04581..cb3c000b5d 100644 --- a/src/frontend/packages/core/src/shared/shared.module.ts +++ b/src/frontend/packages/core/src/shared/shared.module.ts @@ -118,6 +118,7 @@ import { UsageBytesPipe } from './pipes/usage-bytes.pipe'; import { ValuesPipe } from './pipes/values.pipe'; import { LongRunningOperationsService } from './services/long-running-op.service'; import { MetricsRangeSelectorService } from './services/metrics-range-selector.service'; +import { SessionService } from './services/session.service'; import { UserPermissionDirective } from './user-permission.directive'; @@ -324,6 +325,7 @@ import { UserPermissionDirective } from './user-permission.directive'; InternalEventMonitorFactory, MetricsRangeSelectorService, LongRunningOperationsService, + SessionService ] }) export class SharedModule { } diff --git a/src/frontend/packages/core/src/styles.scss b/src/frontend/packages/core/src/styles.scss index c4e58757d0..58d85e671b 100644 --- a/src/frontend/packages/core/src/styles.scss +++ b/src/frontend/packages/core/src/styles.scss @@ -10,6 +10,7 @@ @import '../sass/components/mat-tabs'; @import '../sass/components/mat-table'; @import '../sass/components/mat-button-toggle-group'; +@import '../sass/components/json-schema-form'; body { font-family: Lato, sans-serif; // font-size: 13px; diff --git a/src/frontend/packages/core/src/tab-nav.service.ts b/src/frontend/packages/core/src/tab-nav.service.ts index 634f485c11..99af87716c 100644 --- a/src/frontend/packages/core/src/tab-nav.service.ts +++ b/src/frontend/packages/core/src/tab-nav.service.ts @@ -5,6 +5,7 @@ import { asapScheduler, BehaviorSubject, combineLatest, Observable, Subject } fr import { filter, map, observeOn, publishReplay, refCount, startWith } from 'rxjs/operators'; import { IPageSideNavTab } from './features/dashboard/page-side-nav/page-side-nav.component'; +import { IHeaderBreadcrumbLink } from './shared/components/page-header/page-header.types'; @Injectable() @@ -21,6 +22,9 @@ export class TabNavService { private tabSubNavSubject: BehaviorSubject>; public tabSubNav$: Observable>; + private tabSubNavBreadcrumbsSubject: BehaviorSubject; + public tabSubNavBreadcrumbs$: Observable; + private pageHeaderSubject: BehaviorSubject>; public pageHeader$: Observable>; @@ -36,6 +40,10 @@ export class TabNavService { this.tabSubNavSubject.next(portal); } + public setSubNavBreadcrumbs(breadcrumbs: IHeaderBreadcrumbLink[]) { + this.tabSubNavBreadcrumbsSubject.next(breadcrumbs); + } + public setPageHeader(portal: Portal) { this.pageHeaderSubject.next(portal); } @@ -43,12 +51,13 @@ export class TabNavService { public clear() { this.tabNavsSubject.next(undefined); this.tabHeaderSubject.next(undefined); - this.tabSubNavSubject.next(undefined); + this.clearSubNav(); this.pageHeaderSubject.next(undefined); } public clearSubNav() { this.tabSubNavSubject.next(undefined); + this.tabSubNavBreadcrumbsSubject.next(undefined); } public getCurrentTabHeaderObservable() { @@ -63,7 +72,7 @@ export class TabNavService { ); } - public getCurrentTabHeader = (tabs: IPageSideNavTab[]) => { + private getCurrentTabHeader = (tabs: IPageSideNavTab[]) => { if (!tabs) { return null; } @@ -74,8 +83,8 @@ export class TabNavService { if (!activeTab) { return null; } - return activeTab.label; - } + return activeTab; + }; private observeSubject(subject: Subject) { return subject.asObservable().pipe( @@ -93,6 +102,8 @@ export class TabNavService { this.tabHeader$ = this.observeSubject(this.tabHeaderSubject); this.tabSubNavSubject = new BehaviorSubject(undefined); this.tabSubNav$ = this.observeSubject(this.tabSubNavSubject); + this.tabSubNavBreadcrumbsSubject = new BehaviorSubject(undefined); + this.tabSubNavBreadcrumbs$ = this.observeSubject(this.tabSubNavBreadcrumbsSubject); this.pageHeaderSubject = new BehaviorSubject(undefined); this.pageHeader$ = this.observeSubject(this.pageHeaderSubject); } diff --git a/src/frontend/packages/store/src/actions/pagination.actions.ts b/src/frontend/packages/store/src/actions/pagination.actions.ts index b36603d1e6..9c133b7bf7 100644 --- a/src/frontend/packages/store/src/actions/pagination.actions.ts +++ b/src/frontend/packages/store/src/actions/pagination.actions.ts @@ -6,6 +6,7 @@ import { PaginationClientFilter, PaginationParam } from '../types/pagination.typ export const CLEAR_PAGINATION_OF_TYPE = '[Pagination] Clear all pages of type'; export const CLEAR_PAGINATION_OF_ENTITY = '[Pagination] Clear pagination of entity'; export const RESET_PAGINATION = '[Pagination] Reset pagination'; +export const RESET_PAGINATION_OF_TYPE = '[Pagination] Reset pagination of type'; export const CREATE_PAGINATION = '[Pagination] Create pagination'; export const CLEAR_PAGES = '[Pagination] Clear pages only'; export const SET_PAGE = '[Pagination] Set page'; @@ -57,11 +58,23 @@ export class ResetPagination extends BasePaginationAction implements Action { type = RESET_PAGINATION; } +export class ResetPaginationOfType extends BasePaginationAction implements Action { + constructor(pEntityConfig: Partial) { + super(pEntityConfig); + } + type = RESET_PAGINATION_OF_TYPE; +} + export class CreatePagination extends BasePaginationAction implements Action { /** * @param seed The pagination key for the section we should use as a seed when creating the new pagination section. */ - constructor(pEntityConfig: Partial, public paginationKey: string, public seed?: string) { + constructor( + pEntityConfig: Partial, + public paginationKey: string, + public seed?: string, + public initialParams?: PaginationParam + ) { super(pEntityConfig); } type = CREATE_PAGINATION; diff --git a/src/frontend/packages/store/src/entity-request-pipeline/entity-pagination-request-pipeline.ts b/src/frontend/packages/store/src/entity-request-pipeline/entity-pagination-request-pipeline.ts index 353886937b..c2bb4371e1 100644 --- a/src/frontend/packages/store/src/entity-request-pipeline/entity-pagination-request-pipeline.ts +++ b/src/frontend/packages/store/src/entity-request-pipeline/entity-pagination-request-pipeline.ts @@ -107,7 +107,7 @@ export const basePaginatedRequestPipeline: EntityRequestPipeline = ( ); // Keep, helpful for debugging below chain via tap - // const debug = (val, location) => console.log(`${entity.endpointType}:${entity.entityKey}:${location}: `, val); + // const debug = (val, location) => console.warn(`${entity.endpointType}:${entity.entityKey}:${location}: `, val); return getRequestObjectObservable(request).pipe( first(), diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-clear-pagination-type.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-clear-pagination-type.ts index aaf5dafbaf..7191d36695 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-clear-pagination-type.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-clear-pagination-type.ts @@ -1,5 +1,3 @@ -import { EndpointActionComplete } from '../../actions/endpoint.actions'; -import { entityCatalog } from '../../entity-catalog/entity-catalog'; import { PaginationState } from '../../types/pagination.types'; import { getDefaultPaginationEntityState } from './pagination-reducer-reset-pagination'; @@ -22,14 +20,3 @@ export function paginationClearAllTypes(state: PaginationState, entityKeys: stri return prevState; }, state); } - -export function clearEndpointEntities(state: PaginationState, action: EndpointActionComplete) { - const entityKeys = entityCatalog.getAllEntitiesForEndpointType(action.endpointType).map(entity => entity.entityKey); - if (entityKeys.length > 0) { - return paginationClearAllTypes( - state, - entityKeys - ); - } - return state; -} diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts index 7e57e52957..ad60adc0d1 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts @@ -1,7 +1,12 @@ +import { CreatePagination } from '../../actions/pagination.actions'; import { entityCatalog } from '../../entity-catalog/entity-catalog'; import { EntityCatalogEntityConfig } from '../../entity-catalog/entity-catalog.types'; -import { CreatePagination } from '../../actions/pagination.actions'; -import { PaginationEntityState, PaginationState } from '../../types/pagination.types'; +import { + PaginationEntityState, + PaginationEntityTypeState, + PaginationParam, + PaginationState, +} from '../../types/pagination.types'; import { spreadClientPagination } from './pagination-reducer.helper'; function getPaginationKey(entityConfig: EntityCatalogEntityConfig) { @@ -42,7 +47,7 @@ function mergeWithSeed(state: PaginationState, action: CreatePagination, default const currentPagination = state[entityKey][action.paginationKey] || defaultState; const seeded = action.seed && state[entityKey] && state[entityKey][action.seed]; const seedPagination = seeded ? state[entityKey][action.seed] : defaultState; - const entityState = { + const entityState: PaginationEntityTypeState = { ...newState[entityKey], [action.paginationKey]: { ...seedPagination, @@ -50,7 +55,10 @@ function mergeWithSeed(state: PaginationState, action: CreatePagination, default pageCount: currentPagination.pageCount, currentPage: currentPagination.currentPage, clientPagination: mergePaginationSections(currentPagination, seedPagination, defaultState), - seed: seeded ? action.seed : null + seed: seeded ? action.seed : null, + // Ensure any filters from seed are not carried into new list + // For example, sort by type on endpoints page, go to cf endpoint page and type is not shown + params: mergeParamsSections(currentPagination, action.initialParams) } }; return { @@ -59,6 +67,16 @@ function mergeWithSeed(state: PaginationState, action: CreatePagination, default }; } +function mergeParamsSections( + currentPagination: PaginationEntityState, + initialSeedParams: PaginationParam = {}, +): PaginationParam { + return { + ...currentPagination.params, + ...initialSeedParams, + }; +} + function mergePaginationSections( currentPagination: PaginationEntityState, seedPagination: PaginationEntityState, diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination.ts index 603b2b767f..fb4bcce3b0 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination.ts @@ -1,3 +1,4 @@ +import { EndpointActionComplete } from '../../actions/endpoint.actions'; import { ResetPagination } from '../../actions/pagination.actions'; import { entityCatalog } from '../../entity-catalog/entity-catalog'; import { PaginationEntityState, PaginationEntityTypeState, PaginationState } from '../../types/pagination.types'; @@ -37,26 +38,84 @@ export function getDefaultPaginationEntityState(ignoreMaxed?: boolean): Paginati }; } -export function paginationResetPagination(state: PaginationState, action: ResetPagination): PaginationState { + +export function paginationResetPagination(state: PaginationState, action: ResetPagination, allTypes = false): PaginationState { const entityKey = entityCatalog.getEntityKey(action.entityConfig); - if (!state[entityKey] || !state[entityKey][action.paginationKey]) { + + if (!state[entityKey]) { return state; } - const { ids, pageRequests, pageCount, currentPage, totalResults } = getDefaultPaginationEntityState(); + + const entityState = allTypes ? + paginationResetAllPaginationSections(state, entityKey) : + paginationResetPaginationSection(state, action.paginationKey, entityKey); + + if (!entityState) { + return state; + } + const newState = { ...state }; - const entityState = { - ...newState[entityKey], - [action.paginationKey]: { - ...newState[entityKey][action.paginationKey], - ids, - pageRequests, - pageCount, - currentPage, - totalResults, - } - } as PaginationEntityTypeState; return { ...newState, [entityKey]: entityState }; } + +/** + * Reset all pagination sections of an entity type + */ +function paginationResetAllPaginationSections(state: PaginationState, entityKey: string): PaginationEntityTypeState { + return Object.entries(state[entityKey]).reduce((res, [paginationKey, paginationSection]) => { + res[paginationKey] = paginationResetPaginationState(paginationSection); + return res; + }, {} as PaginationEntityTypeState); +} + +/** + * Reset a single pagination section of an entity type + */ +function paginationResetPaginationSection(state: PaginationState, paginationKey: string, entityKey: string): PaginationEntityTypeState { + + const paginationSection = state[entityKey][paginationKey] + if (!paginationSection) { + return; + } + + const entityState: PaginationEntityTypeState = { + ...state[entityKey], + [paginationKey]: paginationResetPaginationState(paginationSection) + }; + return entityState; +} + +/** + * Reset a pagination section (retain initial/user sort/filter/etc) + */ +function paginationResetPaginationState(oldEntityState: PaginationEntityState) { + const { ids, pageRequests, pageCount, currentPage, totalResults } = getDefaultPaginationEntityState(); + const entityState: PaginationEntityState = { + ...oldEntityState, + ids, + pageRequests, + pageCount, + currentPage, + totalResults, + } + return entityState; +} + +export function resetEndpointEntities(state: PaginationState, action: EndpointActionComplete) { + const entityKeys = entityCatalog.getAllEntitiesForEndpointType(action.endpointType).map(entity => entity.entityKey); + if (entityKeys.length > 0) { + return entityKeys.reduce((prevState, entityKey) => { + if (prevState[entityKey]) { + return { + ...prevState, + [entityKey]: paginationResetAllPaginationSections(prevState, entityKey) + } + } + return prevState; + }, state); + } + return state; +} \ No newline at end of file diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts index f39aa853f0..eef23bc50d 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts @@ -14,6 +14,7 @@ import { IgnorePaginationMaxedState, REMOVE_PARAMS, RESET_PAGINATION, + RESET_PAGINATION_OF_TYPE, SET_CLIENT_FILTER, SET_CLIENT_FILTER_KEY, SET_CLIENT_PAGE, @@ -35,11 +36,15 @@ import { UpdatePaginationMaxedState } from './../../actions/pagination.actions'; import { paginationAddParams } from './pagination-reducer-add-params'; import { paginationClearPages } from './pagination-reducer-clear-pages'; import { paginationClearOfEntity } from './pagination-reducer-clear-pagination-of-entity'; -import { clearEndpointEntities, paginationClearAllTypes } from './pagination-reducer-clear-pagination-type'; +import { paginationClearAllTypes } from './pagination-reducer-clear-pagination-type'; import { createNewPaginationSection } from './pagination-reducer-create-pagination'; import { paginationIgnoreMaxed, paginationMaxReached } from './pagination-reducer-max-reached'; import { paginationRemoveParams } from './pagination-reducer-remove-params'; -import { getDefaultPaginationEntityState, paginationResetPagination } from './pagination-reducer-reset-pagination'; +import { + getDefaultPaginationEntityState, + paginationResetPagination, + resetEndpointEntities, +} from './pagination-reducer-reset-pagination'; import { paginationSetClientFilter } from './pagination-reducer-set-client-filter'; import { paginationSetClientFilterKey } from './pagination-reducer-set-client-filter-key'; import { paginationSetClientPage } from './pagination-reducer-set-client-page'; @@ -121,6 +126,10 @@ function paginate(action, state = {}, updatePagination) { return paginationResetPagination(state, action); } + if (action.type === RESET_PAGINATION_OF_TYPE && !action.keepPages) { + return paginationResetPagination(state, action, true); + } + if (action.type === CLEAR_PAGINATION_OF_TYPE) { const clearAction = action as ClearPaginationOfType; const clearEntityType = entityCatalog.getEntityKey(clearAction.entityConfig.endpointType, clearAction.entityConfig.entityType); @@ -132,7 +141,7 @@ function paginate(action, state = {}, updatePagination) { } if (isEndpointAction(action)) { - return clearEndpointEntities(state, action); + return resetEndpointEntities(state, action); } if (action.type === UPDATE_MAXED_STATE) { diff --git a/src/frontend/packages/theme/_helper.scss b/src/frontend/packages/theme/_helper.scss index 7b9015f992..70cb9f9eda 100644 --- a/src/frontend/packages/theme/_helper.scss +++ b/src/frontend/packages/theme/_helper.scss @@ -115,6 +115,6 @@ $oss-dark-theme: mat-dark-theme($oss-dark-primary, $oss-dark-accent, $oss-dark-w $warn: map-get($theme, warn); $primary: map-get($theme, primary); $white: #fff; // Use default palette for status - @return (success: map-get($mat-green, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, ); + @return (success: map-get($mat-green, 500), info: map-get($mat-blue, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, ); } }