diff --git a/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts b/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts index 20e226cbd5..56e70242de 100644 --- a/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts +++ b/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts @@ -1,3 +1,4 @@ +import { Compiler, Injector } from '@angular/core'; import { Action, Store } from '@ngrx/store'; import moment from 'moment'; import { combineLatest, Observable, of } from 'rxjs'; @@ -236,6 +237,33 @@ export interface CFBasePipelineRequestActionMeta { flatten?: boolean; } +function cfShortcuts(id: string) { + return [ + { + title: 'View Organizations', + link: ['/cloud-foundry', id, 'organizations'], + icon: 'organization', + iconFont: 'stratos-icons' + }, + { + title: 'View Applications', + link: ['/applications', id], + icon: 'apps' + }, + { + title: 'Deploy an Application', + link: ['/applications', 'new', id], + icon: 'publish' + }, + { + title: 'View Cloud Foundry Info', + link: ['/cloud-foundry', id], + icon: 'cloud_foundry', + iconFont: 'stratos-icons' + }, + ]; +} + export function generateCFEntities(): StratosBaseCatalogEntity[] { const endpointDefinition: StratosEndpointExtensionDefinition = { urlValidationRegexString: urlValidationExpression, @@ -246,6 +274,16 @@ export function generateCFEntities(): StratosBaseCatalogEntity[] { iconFont: 'stratos-icons', logoUrl: '/core/assets/endpoint-icons/cloudfoundry.png', authTypes: [BaseEndpointAuth.UsernamePassword, BaseEndpointAuth.SSO], + homeCard: { + component: (compiler: Compiler, injector: Injector) => import('./features/home/cfhome-card/cfhome-card.module').then(m => { + return compiler.compileModuleAndAllComponentsAsync(m.CFHomeCardModule).then(cm => { + const mod = cm.ngModuleFactory.create(injector); + return mod.instance.createHomeCard(mod.componentFactoryResolver); + }); + }), + shortcuts: cfShortcuts, + fullView: false, + }, listDetailsComponent: CfEndpointDetailsComponent, renderPriority: 1, healthCheck: new EndpointHealthCheck(CF_ENDPOINT_TYPE, (endpoint) => cfEntityCatalog.cfInfo.api.get(endpoint.guid)), diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/application-wall/application-wall.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/applications/application-wall/application-wall.component.spec.ts index e7060da064..3c7ef0d531 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/application-wall/application-wall.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/application-wall/application-wall.component.spec.ts @@ -1,5 +1,6 @@ import { DatePipe } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { TabNavService } from '../../../../../core/src/tab-nav.service'; import { generateCfBaseTestModules } from '../../../../test-framework/cloud-foundry-endpoint-service.helper'; @@ -24,6 +25,15 @@ describe('ApplicationWallComponent', () => { DatePipe, TabNavService, CloudFoundryService, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + params: {}, + queryParams: {} + } + } + } ] }) .compileComponents(); diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/application-wall/application-wall.component.ts b/src/frontend/packages/cloud-foundry/src/features/applications/application-wall/application-wall.component.ts index c6cfa5d7dc..79bb6cb74c 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/application-wall/application-wall.component.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/application-wall/application-wall.component.ts @@ -1,5 +1,6 @@ import { animate, query, style, transition, trigger } from '@angular/animations'; import { Component, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -12,6 +13,7 @@ import { CfAppsDataSource } from '../../../shared/components/list/list-types/app import { CfOrgSpaceDataService, initCfOrgSpaceService } from '../../../shared/data-services/cf-org-space-service.service'; import { CloudFoundryService } from '../../../shared/data-services/cloud-foundry.service'; import { CfCurrentUserPermissions } from '../../../user-permissions/cf-user-permissions-checkers'; +import { goToAppWall } from '../../cf/cf.helpers'; @Component({ selector: 'app-application-wall', @@ -48,7 +50,15 @@ export class ApplicationWallComponent implements OnDestroy { public cloudFoundryService: CloudFoundryService, private store: Store, private cfOrgSpaceService: CfOrgSpaceDataService, + activatedRoute: ActivatedRoute, ) { + // If we have an endpoint ID, select it and redirect + const { endpointId } = activatedRoute.snapshot.params; + if (endpointId) { + goToAppWall(this.store, endpointId); + return; + } + this.cfIds$ = cloudFoundryService.cFEndpoints$.pipe( map(endpoints => endpoints.map(endpoint => endpoint.guid)), ); @@ -65,6 +75,8 @@ export class ApplicationWallComponent implements OnDestroy { } ngOnDestroy(): void { - this.initCfOrgSpaceService.unsubscribe(); + if (this.initCfOrgSpaceService) { + this.initCfOrgSpaceService.unsubscribe(); + } } } diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/applications.routing.ts b/src/frontend/packages/cloud-foundry/src/features/applications/applications.routing.ts index 1b0ffd86b4..d8f3bf62cf 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/applications.routing.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/applications.routing.ts @@ -39,6 +39,12 @@ const applicationsRoutes: Routes = [ { path: 'new', component: NewApplicationBaseStepComponent, + pathMatch: 'full' + }, + { + path: 'new/:endpointId', + component: NewApplicationBaseStepComponent, + pathMatch: 'full' }, { path: 'create', @@ -59,6 +65,11 @@ const applicationsRoutes: Routes = [ extensionsActionsKey: StratosActionType.Applications } }, + { + path: ':endpointId', + component: ApplicationWallComponent, + pathMatch: 'full' + }, { path: ':endpointId/:id', component: ApplicationBaseComponent, diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.ts b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.ts index ff481bc25d..7dd21b8781 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.ts @@ -22,6 +22,7 @@ import { RouterNav } from '../../../../../store/src/actions/router.actions'; import { selectPaginationState } from '../../../../../store/src/selectors/pagination.selectors'; import { CfAppsDataSource } from '../../../shared/components/list/list-types/app/cf-apps-data-source'; import { CfOrgSpaceDataService } from '../../../shared/data-services/cf-org-space-service.service'; +import { AUTO_SELECT_CF_URL_PARAM } from '../new-application-base-step/new-application-base-step.component'; import { ApplicationDeploySourceTypes } from './deploy-application-steps.types'; @Component({ @@ -78,6 +79,11 @@ export class DeployApplicationComponent implements OnInit, OnDestroy { } ngOnInit(): void { + // Has the endpoint ID been specified in the URL? + const endpoint = this.activatedRoute.snapshot.queryParams[AUTO_SELECT_CF_URL_PARAM]; + if (endpoint) { + this.cfOrgSpaceService.cf.select.next(endpoint); + } if (this.appGuid) { this.deployButtonText = 'Redeploy'; diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/new-application-base-step/new-application-base-step.component.ts b/src/frontend/packages/cloud-foundry/src/features/applications/new-application-base-step/new-application-base-step.component.ts index 24b9f15696..66d77a7be0 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/new-application-base-step/new-application-base-step.component.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/new-application-base-step/new-application-base-step.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; import { CFAppState } from '../../../../../cloud-foundry/src/cf-app-state'; @@ -11,10 +12,14 @@ import { AUTO_SELECT_DEPLOY_TYPE_URL_PARAM, } from '../deploy-application/deploy-application-steps.types'; -interface IAppTileData extends ITileData { +export const AUTO_SELECT_CF_URL_PARAM = 'auto-select-endpoint'; + + +export interface IAppTileData extends ITileData { type: string; subType?: string; } + @Component({ selector: 'app-new-application-base-step', templateUrl: './new-application-base-step.component.html', @@ -36,6 +41,12 @@ export class NewApplicationBaseStepComponent { if (tile.data.subType) { query[AUTO_SELECT_DEPLOY_TYPE_URL_PARAM] = tile.data.subType; } + const endpoint = this.activatedRoute.snapshot.params.endpointId; + if (endpoint) { + query[AUTO_SELECT_CF_URL_PARAM] = endpoint; + query[BASE_REDIRECT_QUERY] += `/${endpoint}`; + } + this.store.dispatch(new RouterNav({ path: `${baseUrl}/${type}`, query @@ -45,7 +56,10 @@ export class NewApplicationBaseStepComponent { constructor( private store: Store, - appDeploySourceTypes: ApplicationDeploySourceTypes) { + appDeploySourceTypes: ApplicationDeploySourceTypes, + private activatedRoute: ActivatedRoute, + + ) { this.sourceTypes = appDeploySourceTypes.getTypes(); this.tileSelectorConfig = [ ...this.sourceTypes.map(type => diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/cloud-foundry-section.module.ts b/src/frontend/packages/cloud-foundry/src/features/cf/cloud-foundry-section.module.ts index 688e900238..f8adc26ba8 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/cloud-foundry-section.module.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cf/cloud-foundry-section.module.ts @@ -10,6 +10,7 @@ import { CloudFoundrySharedModule } from '../../shared/cf-shared.module'; import { CFEndpointsListConfigService, } from '../../shared/components/list/list-types/cf-endpoints/cf-endpoints-list-config.service'; +import { CFHomeCardModule } from '../home/cfhome-card/cfhome-card.module'; import { AddOrganizationComponent } from './add-organization/add-organization.component'; import { CreateOrganizationStepComponent, @@ -138,7 +139,8 @@ import { RemoveUserComponent } from './users/remove-user/remove-user.component'; CloudFoundrySectionRoutingModule, RouterModule, NgxChartsModule, - CloudFoundrySharedModule + CloudFoundrySharedModule, + CFHomeCardModule, ], declarations: [ CloudFoundryBaseComponent, diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/services/cloud-foundry-endpoint.service.ts b/src/frontend/packages/cloud-foundry/src/features/cf/services/cloud-foundry-endpoint.service.ts index 4561313f5f..58f5047596 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/services/cloud-foundry-endpoint.service.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cf/services/cloud-foundry-endpoint.service.ts @@ -134,6 +134,26 @@ export class CloudFoundryEndpointService { return fetchTotalResults(action, store, pmf); } + // Fetch the cound of organisations in a Cloud Foundry + public static fetchOrgCount(store: Store, pmf: PaginationMonitorFactory, cfGuid: string): Observable { + const getAllOrgsAction = CloudFoundryEndpointService.createGetAllOrganizations(cfGuid); + return fetchTotalResults(getAllOrgsAction, store, pmf); + } + + public static fetchOrgs(store: Store, pmf: PaginationMonitorFactory, cfGuid: string): + Observable[]> { + const getAllOrgsAction = CloudFoundryEndpointService.createGetAllOrganizations(cfGuid); + return getPaginationObservables>({ + store, + action: getAllOrgsAction, + paginationMonitor: pmf.create( + getAllOrgsAction.paginationKey, + cfEntityFactory(organizationEntityType), + getAllOrgsAction.flattenPagination + ) + }, getAllOrgsAction.flattenPagination).entities$; + } + constructor( public activeRouteCfOrgSpace: ActiveRouteCfOrgSpace, private store: Store, @@ -141,7 +161,6 @@ export class CloudFoundryEndpointService { private pmf: PaginationMonitorFactory, ) { this.cfGuid = activeRouteCfOrgSpace.cfGuid; - this.cfEndpointEntityService = stratosEntityCatalog.endpoint.store.getEntityService(this.cfGuid); this.cfInfoEntityService = cfEntityCatalog.cfInfo.store.getEntityService(this.cfGuid); @@ -152,16 +171,7 @@ export class CloudFoundryEndpointService { private constructCoreObservables() { this.endpoint$ = this.cfEndpointEntityService.waitForEntity$; - const getAllOrgsAction = CloudFoundryEndpointService.createGetAllOrganizations(this.cfGuid); - this.orgs$ = getPaginationObservables>({ - store: this.store, - action: getAllOrgsAction, - paginationMonitor: this.pmf.create( - getAllOrgsAction.paginationKey, - cfEntityFactory(organizationEntityType), - getAllOrgsAction.flattenPagination - ) - }, getAllOrgsAction.flattenPagination).entities$; + this.orgs$ = CloudFoundryEndpointService.fetchOrgs(this.store, this.pmf, this.cfGuid); this.info$ = this.cfInfoEntityService.waitForEntity$; diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-spaces/tabs/cloud-foundry-space-summary/cloud-foundry-space-summary.component.html b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-spaces/tabs/cloud-foundry-space-summary/cloud-foundry-space-summary.component.html index 3f8bab4fa0..355e1f50c8 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-spaces/tabs/cloud-foundry-space-summary/cloud-foundry-space-summary.component.html +++ b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-spaces/tabs/cloud-foundry-space-summary/cloud-foundry-space-summary.component.html @@ -75,7 +75,8 @@ + [endpoint]="cfEndpointService.cfGuid" + [loading$]="cfSpaceService.loadingApps$" (refresh)="cfSpaceService.fetchApps()"> diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-spaces/tabs/cloud-foundry-space-summary/cloud-foundry-space-summary.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-spaces/tabs/cloud-foundry-space-summary/cloud-foundry-space-summary.component.spec.ts index 9b1935631e..e973d6b546 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-spaces/tabs/cloud-foundry-space-summary/cloud-foundry-space-summary.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-spaces/tabs/cloud-foundry-space-summary/cloud-foundry-space-summary.component.spec.ts @@ -6,12 +6,6 @@ import { generateCfBaseTestModules, } from '../../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { CloudFoundrySpaceServiceMock } from '../../../../../../../../test-framework/cloud-foundry-space.service.mock'; -import { - CardCfRecentAppsComponent, -} from '../../../../../../../shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component'; -import { - CompactAppCardComponent, -} from '../../../../../../../shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component'; import { CardCfSpaceDetailsComponent, } from '../../../../../../../shared/components/cards/card-cf-space-details/card-cf-space-details.component'; @@ -22,6 +16,10 @@ import { import { CloudFoundryUserProvidedServicesService, } from '../../../../../../../shared/services/cloud-foundry-user-provided-services.service'; +import { CardCfRecentAppsComponent } from '../../../../../../home/card-cf-recent-apps/card-cf-recent-apps.component'; +import { + CompactAppCardComponent, +} from '../../../../../../home/card-cf-recent-apps/compact-app-card/compact-app-card.component'; import { CloudFoundryEndpointService } from '../../../../../services/cloud-foundry-endpoint.service'; import { CloudFoundryOrganizationService } from '../../../../../services/cloud-foundry-organization.service'; import { CloudFoundrySpaceService } from '../../../../../services/cloud-foundry-space.service'; diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-summary/cloud-foundry-organization-summary.component.html b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-summary/cloud-foundry-organization-summary.component.html index c0b58c00cd..7f08c97325 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-summary/cloud-foundry-organization-summary.component.html +++ b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-summary/cloud-foundry-organization-summary.component.html @@ -75,6 +75,7 @@ diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-summary/cloud-foundry-organization-summary.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-summary/cloud-foundry-organization-summary.component.spec.ts index 639ae6ff62..ea18e34694 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-summary/cloud-foundry-organization-summary.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-organizations/cf-organization-summary/cloud-foundry-organization-summary.component.spec.ts @@ -11,13 +11,9 @@ import { import { CardCfOrgUserDetailsComponent, } from '../../../../../shared/components/cards/card-cf-org-user-details/card-cf-org-user-details.component'; -import { - CardCfRecentAppsComponent, -} from '../../../../../shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component'; -import { - CompactAppCardComponent, -} from '../../../../../shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component'; import { CfUserPermissionDirective } from '../../../../../shared/directives/cf-user-permission/cf-user-permission.directive'; +import { CardCfRecentAppsComponent } from '../../../../home/card-cf-recent-apps/card-cf-recent-apps.component'; +import { CompactAppCardComponent } from '../../../../home/card-cf-recent-apps/compact-app-card/compact-app-card.component'; import { CloudFoundryOrganizationService } from '../../../services/cloud-foundry-organization.service'; import { CloudFoundryOrganizationSummaryComponent } from './cloud-foundry-organization-summary.component'; diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-summary-tab/cloud-foundry-summary-tab.component.html b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-summary-tab/cloud-foundry-summary-tab.component.html index 00060a2ab9..a45b8fd8f4 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-summary-tab/cloud-foundry-summary-tab.component.html +++ b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-summary-tab/cloud-foundry-summary-tab.component.html @@ -34,6 +34,7 @@ diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-summary-tab/cloud-foundry-summary-tab.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-summary-tab/cloud-foundry-summary-tab.component.spec.ts index 9c459b2db9..bb055e4564 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-summary-tab/cloud-foundry-summary-tab.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-summary-tab/cloud-foundry-summary-tab.component.spec.ts @@ -6,12 +6,8 @@ import { generateTestCfEndpointServiceProvider, } from '../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { CardCfInfoComponent } from '../../../../shared/components/cards/card-cf-info/card-cf-info.component'; -import { - CardCfRecentAppsComponent, -} from '../../../../shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component'; -import { - CompactAppCardComponent, -} from '../../../../shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component'; +import { CardCfRecentAppsComponent } from '../../../home/card-cf-recent-apps/card-cf-recent-apps.component'; +import { CompactAppCardComponent } from '../../../home/card-cf-recent-apps/compact-app-card/compact-app-card.component'; import { CloudFoundrySummaryTabComponent } from './cloud-foundry-summary-tab.component'; describe('CloudFoundrySummaryTabComponent', () => { diff --git a/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.html b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.html new file mode 100644 index 0000000000..7c77d6d64c --- /dev/null +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.html @@ -0,0 +1,30 @@ + + + + Recently updated applications + + + +
+ There are no applications. +
+
+ +
+
+
+
+ + + + + Recently updated applications + + +
+ +
+
+
+
\ No newline at end of file diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.scss b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.scss similarity index 70% rename from src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.scss rename to src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.scss index e6f70f9ab0..96249b7129 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.scss +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.scss @@ -1,9 +1,16 @@ +:host { + display: flex; +} .recent-apps-card { display: flex; flex-direction: column; height: 100%; width: 100%; + &__plain { + box-shadow: none; + } + &__content { display: flex; overflow-y: auto; @@ -14,7 +21,6 @@ flex: 1; height: 40px; justify-content: space-between; - padding-top: 0; mat-card-title { margin-bottom: 0; @@ -31,5 +37,10 @@ overflow-y: auto; width: 100%; } +} +// Remove box shadow in plain mode +mat-card.recent-apps-card.mat-card.recent-apps-card__plain { + box-shadow: none; + padding: 0 1em; } diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.spec.ts similarity index 64% rename from src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.spec.ts rename to src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.spec.ts index b93ba4295a..53fbd7045f 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.spec.ts @@ -3,21 +3,21 @@ import { of as observableOf } from 'rxjs'; import { ApplicationStateIconComponent, -} from '../../../../../../core/src/shared/components/application-state/application-state-icon/application-state-icon.component'; +} from '../../../../../core/src/shared/components/application-state/application-state-icon/application-state-icon.component'; import { ApplicationStateIconPipe, -} from '../../../../../../core/src/shared/components/application-state/application-state-icon/application-state-icon.pipe'; +} from '../../../../../core/src/shared/components/application-state/application-state-icon/application-state-icon.pipe'; import { PollingIndicatorComponent, -} from '../../../../../../core/src/shared/components/polling-indicator/polling-indicator.component'; -import { EntityMonitorFactory } from '../../../../../../store/src/monitors/entity-monitor.factory.service'; -import { PaginationMonitorFactory } from '../../../../../../store/src/monitors/pagination-monitor.factory'; +} from '../../../../../core/src/shared/components/polling-indicator/polling-indicator.component'; +import { EntityMonitorFactory } from '../../../../../store/src/monitors/entity-monitor.factory.service'; +import { PaginationMonitorFactory } from '../../../../../store/src/monitors/pagination-monitor.factory'; import { generateActiveRouteCfOrgSpaceMock, generateCfBaseTestModulesNoShared, -} from '../../../../../test-framework/cloud-foundry-endpoint-service.helper'; -import { CloudFoundryEndpointService } from '../../../../features/cf/services/cloud-foundry-endpoint.service'; -import { CfUserService } from '../../../data-services/cf-user.service'; +} from '../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { CfUserService } from '../../../shared/data-services/cf-user.service'; +import { CloudFoundryEndpointService } from '../../cf/services/cloud-foundry-endpoint.service'; import { CardCfRecentAppsComponent } from './card-cf-recent-apps.component'; import { CompactAppCardComponent } from './compact-app-card/compact-app-card.component'; diff --git a/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.ts b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.ts new file mode 100644 index 0000000000..bd5bfa11db --- /dev/null +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.ts @@ -0,0 +1,103 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; +import { filter, map, startWith, tap } from 'rxjs/operators'; + +import { PaginationObservables } from '../../../../../store/src/reducers/pagination-reducer/pagination-reducer.types'; +import { APIResource } from '../../../../../store/src/types/api.types'; +import { IApp } from '../../../cf-api.types'; +import { cfEntityCatalog } from '../../../cf-entity-catalog'; +import { appDataSort } from '../../cf/services/cloud-foundry-endpoint.service'; + + +const RECENT_ITEMS_COUNT = 10; + +@Component({ + selector: 'app-card-cf-recent-apps', + templateUrl: './card-cf-recent-apps.component.html', + styleUrls: ['./card-cf-recent-apps.component.scss'], +}) +export class CardCfRecentAppsComponent implements OnInit { + + public recentApps$: Observable[]>; + @Input() allApps$: Observable[]>; + @Input() loading$: Observable; + @Output() refresh = new EventEmitter(); + @Input() endpoint: string; + @Input() mode: string; + @Input() showDate = true; + @Input() dateMode: string; + @Input() noStats = false; + @Input() placeholderMode = false; + @Input() hideWhenEmpty = false; + + public canRefresh = false; + + public placeholders: any[]; + + appsPagObs: PaginationObservables>; + + hasEntities$: Observable; + show$: Observable; + + private maxRowsSubject = new BehaviorSubject(RECENT_ITEMS_COUNT); + + @Input() set maxRows(value: number) { + this.maxRowsSubject.next(value); + this.placeholders = new Array(value).fill(null); + } + + constructor() { + this.placeholders = new Array(RECENT_ITEMS_COUNT).fill(null); + } + + ngOnInit() { + if (this.placeholderMode) { + this.canRefresh = false; + this.hasEntities$ = of(false); + return; + } + this.canRefresh = this.refresh.observers.length > 0; + this.appsPagObs = cfEntityCatalog.application.store.getPaginationService(this.endpoint); + if (!this.allApps$) { + this.allApps$ = this.appsPagObs.entities$; + this.loading$ = this.appsPagObs.fetchingEntities$; + this.hasEntities$ = this.appsPagObs.hasEntities$ + } else { + this.hasEntities$ = of(true); + } + + this.recentApps$ = combineLatest( + this.allApps$, + this.maxRowsSubject.asObservable() + ).pipe( + filter(([apps]) => !!apps), + map(([apps, maxRows]) => this.restrictApps(apps, maxRows)), + tap(apps => this.fetchAppStats(apps)) + ); + + this.show$ = this.allApps$.pipe( + map(apps => { + return !this.hideWhenEmpty || this.hideWhenEmpty && apps.length > 0; + }), + startWith(true), + ); + } + + private fetchAppStats(recentApps: APIResource[]) { + if(!this.noStats) { + recentApps.forEach(app => { + if (app.entity.state === 'STARTED') { + cfEntityCatalog.appStats.api.getMultiple(app.metadata.guid, this.endpoint); + } + }); + } + } + + private restrictApps(apps: APIResource[], maxRows = RECENT_ITEMS_COUNT): APIResource[] { + if (!apps) { + return []; + } + return apps.sort(appDataSort).slice(0, maxRows); + } + +} diff --git a/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.html b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.html new file mode 100644 index 0000000000..06a1acd218 --- /dev/null +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.html @@ -0,0 +1,17 @@ +
+ + +
{{ app.metadata.updated_at | date:'medium' }}
+
+ + +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss new file mode 100644 index 0000000000..bb7def5219 --- /dev/null +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss @@ -0,0 +1,79 @@ +:host { + border: 1px solid #ccc; + display: flex; + padding: 0 12px; + + &:not(:first-child) { + border-top: 0; + } +} + +.compact-app-card { + align-items: center; + display: flex; + flex-direction: row; + font-size: 14px; + height: 40px; + width: 100%; + &__name { + flex: 1; + padding-left: 12px; + } + &__subtle { + font-size: 12px; + opacity: 0.7; + } +} + +$ph-bg: #fff !default; +$ph-color: #ced4da !default; +$ph-border-radius: 2px !default; + +$ph-gutter: 30px !default; +$ph-spacer: 15px !default; + +$ph-avatar-border-radius: 50% !default; + +$ph-animation-duration: .8s !default; + +.ph-item { + height: 14px; + background-color: #d8d8d8; + overflow: hidden; + position: relative; + + &::before { + animation: phAnimation $ph-animation-duration linear infinite; + background: linear-gradient(to right, rgba($ph-bg, 0) 46%, rgba($ph-bg, .35) 50%, rgba($ph-bg, 0) 54%) 50% 50%; + content: ''; + top: 0; + right: 0; + bottom: 0; + left: 50%; + z-index: 1; + width: 500%; + margin-left: -250%; + position: absolute; + } + + &.ph-large { + width: 120px; + } + &.ph-small { + width: 80px; + } + &.ph-icon { + border-radius: 50%; + height: 20px; + width: 20px; + } +} + +@keyframes phAnimation { + 0% { + transform: translate3d(-30%, 0, 0); + } + 100% { + transform: translate3d(30%, 0, 0); + } +} \ No newline at end of file diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.spec.ts similarity index 69% rename from src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.spec.ts rename to src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.spec.ts index d6655a60b1..2f7bc155e6 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.spec.ts @@ -2,13 +2,13 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ApplicationStateIconComponent, -} from '../../../../../../../core/src/shared/components/application-state/application-state-icon/application-state-icon.component'; +} from '../../../../../../core/src/shared/components/application-state/application-state-icon/application-state-icon.component'; import { ApplicationStateIconPipe, -} from '../../../../../../../core/src/shared/components/application-state/application-state-icon/application-state-icon.pipe'; -import { generateCfBaseTestModulesNoShared } from '../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; -import { ActiveRouteCfOrgSpace } from '../../../../../features/cf/cf-page.types'; -import { ApplicationStateService } from '../../../../services/application-state.service'; +} from '../../../../../../core/src/shared/components/application-state/application-state-icon/application-state-icon.pipe'; +import { generateCfBaseTestModulesNoShared } from '../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { ApplicationStateService } from '../../../../shared/services/application-state.service'; +import { ActiveRouteCfOrgSpace } from '../../../cf/cf-page.types'; import { CompactAppCardComponent } from './compact-app-card.component'; describe('CompactAppCardComponent', () => { diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.ts b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.ts similarity index 65% rename from src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.ts rename to src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.ts index 456ba80949..6e3aef7245 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.ts +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.ts @@ -3,12 +3,12 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; -import { CFAppState } from '../../../../../../../cloud-foundry/src/cf-app-state'; -import { ApplicationService } from '../../../../../../../cloud-foundry/src/features/applications/application.service'; -import { BREADCRUMB_URL_PARAM } from '../../../../../../../core/src/shared/components/breadcrumbs/breadcrumbs.types'; -import { StratosStatus } from '../../../../../../../store/src/types/shared.types'; -import { ActiveRouteCfOrgSpace } from '../../../../../features/cf/cf-page.types'; -import { ApplicationStateData, ApplicationStateService } from '../../../../services/application-state.service'; +import { BREADCRUMB_URL_PARAM } from '../../../../../../core/src/shared/components/breadcrumbs/breadcrumbs.types'; +import { StratosStatus } from '../../../../../../store/src/types/shared.types'; +import { CFAppState } from '../../../../cf-app-state'; +import { ApplicationStateData, ApplicationStateService } from '../../../../shared/services/application-state.service'; +import { ApplicationService } from '../../../applications/application.service'; +import { ActiveRouteCfOrgSpace } from '../../../cf/cf-page.types'; @Component({ @@ -20,6 +20,11 @@ export class CompactAppCardComponent implements OnInit { @Input() app; + @Input() endpoint: string; + + @Input() showDate = true; + @Input() dateMode: string; + applicationState$: Observable; appStatus$: Observable; @@ -34,14 +39,23 @@ export class CompactAppCardComponent implements OnInit { ) { } ngOnInit() { + if(this.activeRouteCfOrgSpace) { + this.bcType = this.setBreadcrumbType(this.activeRouteCfOrgSpace); + if (!this.endpoint) { + this.endpoint = this.activeRouteCfOrgSpace.cfGuid; + } + } + + if (!this.app) { + return + } - this.bcType = this.setBreadcrumbType(this.activeRouteCfOrgSpace); const initState = this.appStateService.get(this.app.entity, null); this.applicationState$ = ApplicationService.getApplicationState( this.appStateService, this.app.entity, this.app.metadata.guid, - this.activeRouteCfOrgSpace.cfGuid + this.endpoint ).pipe( startWith(initState) ); diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html new file mode 100644 index 0000000000..4fe72cddd9 --- /dev/null +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + +
+ apps +
You don't have any applications
+
+
Get started by deploying an Application from ...
+ +
+
+
+
+
diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss new file mode 100644 index 0000000000..85cb520946 --- /dev/null +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss @@ -0,0 +1,30 @@ +.cf-home-card { + &__plain-tiles { + margin-left: 0; + margin-right: 1em; + margin-top: 1em; + } + &__no-apps { + align-items: center; + display: flex; + flex-direction: column; + margin-top: 20px; + opacity: 0.7; + > mat-icon { + font-size: 48px; + height: 48px; + width: 48px; + } + &-title { + font-size: 16px; + margin: 10px 8px; + text-align: center; + } + &-deploy { + font-size: 16px; + font-weight: bold; + margin: 20px 0; + text-align: center; + } + } +} diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.spec.ts new file mode 100644 index 0000000000..87195f96e6 --- /dev/null +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.spec.ts @@ -0,0 +1,31 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { generateCfBaseTestModules } from '../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { ApplicationDeploySourceTypes } from '../../applications/deploy-application/deploy-application-steps.types'; +import { CFHomeCardComponent } from './cfhome-card.component'; + +describe('CFHomeCardComponent', () => { + let component: CFHomeCardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CFHomeCardComponent ], + imports: generateCfBaseTestModules(), + providers: [ + ApplicationDeploySourceTypes + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CFHomeCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.ts b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.ts new file mode 100644 index 0000000000..4c522f6129 --- /dev/null +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.ts @@ -0,0 +1,199 @@ +import { Component, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { filter, first, map, pairwise } from 'rxjs/operators'; + +import { BASE_REDIRECT_QUERY } from '../../../../../core/src/shared/components/stepper/stepper.types'; +import { RouterNav } from '../../../../../store/src/actions/router.actions'; +import { PaginationMonitorFactory } from '../../../../../store/src/monitors/pagination-monitor.factory'; +import { EndpointModel } from '../../../../../store/src/public-api'; +import { ActionState } from '../../../../../store/src/reducers/api-request-reducer/types'; +import { APIResource } from '../../../../../store/src/types/api.types'; +import { IApp } from '../../../cf-api.types'; +import { CFAppState } from '../../../cf-app-state'; +import { cfEntityCatalog } from '../../../cf-entity-catalog'; +import { SourceType } from '../../../store/types/deploy-application.types'; +import { + ApplicationDeploySourceTypes, + AUTO_SELECT_DEPLOY_TYPE_URL_PARAM, +} from '../../applications/deploy-application/deploy-application-steps.types'; +import { + AUTO_SELECT_CF_URL_PARAM, + IAppTileData, +} from '../../applications/new-application-base-step/new-application-base-step.component'; +import { ActiveRouteCfOrgSpace } from '../../cf/cf-page.types'; +import { goToAppWall } from '../../cf/cf.helpers'; +import { appDataSort, CloudFoundryEndpointService } from '../../cf/services/cloud-foundry-endpoint.service'; +import { HomePageCardLayout, HomePageEndpointCard } from './../../../../../core/src/features/home/home.types'; +import { ITileConfig } from './../../../../../core/src/shared/components/tile/tile-selector.types'; + + +@Component({ + selector: 'app-cfhome-card', + templateUrl: './cfhome-card.component.html', + styleUrls: ['./cfhome-card.component.scss'], + providers: [ + { + provide: ActiveRouteCfOrgSpace, + useValue: null, + }, + CloudFoundryEndpointService + ] +}) +export class CFHomeCardComponent implements HomePageEndpointCard { + + _layout: HomePageCardLayout; + + get layout(): HomePageCardLayout { + return this._layout; + } + + @Input() set layout(value: HomePageCardLayout) { + if (value) { + this._layout = value; + } + this.updateLayout(); + }; + + @Input() set endpoint(value: EndpointModel) { + this.guid = value.guid; + } + + guid: string; + + recentAppsRows = 10; + + appLink: () => void; + + appCount$: Observable; + orgCount$: Observable; + routeCount$: Observable; + + hasNoApps$: Observable; + + cardLoaded = false; + + recentApps = []; + + private appStatsLoaded = new BehaviorSubject(false); + private appStatsToLoad: APIResource[] = []; + + private sourceTypes: SourceType[]; + public tileSelectorConfig: ITileConfig[]; + + showDeployAppTiles = false; + + constructor( + private store: Store, + private pmf: PaginationMonitorFactory, + appDeploySourceTypes: ApplicationDeploySourceTypes, + ) { + // Set a default layout + this._layout = new HomePageCardLayout(1, 1); + + // Get source types for if we are showing tiles to deploy an application + this.sourceTypes = appDeploySourceTypes.getTypes(); + this.tileSelectorConfig = [ + ...this.sourceTypes.map(type => + new ITileConfig( + type.name, + type.graphic, + { type: 'deploy', subType: type.id }, + ) + ) + ]; + } + + // Deploy an app from the Home Card for the given endpoint + set selectedTile(tile: ITileConfig) { + const type = tile ? tile.data.type : null; + if (tile) { + const query = { + [BASE_REDIRECT_QUERY]: `applications/new/${this.guid}`, + [AUTO_SELECT_CF_URL_PARAM]:this.guid + }; + if (tile.data.subType) { + query[AUTO_SELECT_DEPLOY_TYPE_URL_PARAM] = tile.data.subType; + } + this.store.dispatch(new RouterNav({ path: `applications/${type}`, query })); + } + } + + // Card is instructed to load its view by the container, whn it is visible + load(): Observable { + this.cardLoaded = true; + this.routeCount$ = CloudFoundryEndpointService.fetchRouteCount(this.store, this.pmf, this.guid); + this.appCount$ = CloudFoundryEndpointService.fetchAppCount(this.store, this.pmf, this.guid); + this.orgCount$ = CloudFoundryEndpointService.fetchOrgCount(this.store, this.pmf, this.guid); + + this.appLink = () => goToAppWall(this.store, this.guid);; + + const appsPagObs = cfEntityCatalog.application.store.getPaginationService(this.guid); + + // When the apps are loaded, fetch the app stats + this.hasNoApps$ = appsPagObs.entities$.pipe(first(), map(apps => { + this.recentApps = apps; + this.appStatsToLoad = this.restrictApps(apps); + this.fetchAppStats(); + this.fetchAppStats(); + return apps.length === 0; + })); + + const appStatLoaded$ = this.appStatsLoaded.asObservable().pipe(filter(loaded => loaded)); + return combineLatest([ + this.routeCount$, + this.appCount$, + this.orgCount$, + appsPagObs.entities$, + appStatLoaded$ + ]).pipe( + map(() => true) + ); + } + + public updateLayout() { + const currentRows = this.recentAppsRows; + this.recentAppsRows = this.layout.y > 1 ? 5 : 10; + + // Hide recent apps if more than 2 columns + if (this.layout.x > 2) { + this.recentAppsRows = 0; + } + + // If the layout changes and there are apps to show then we need to fetch the app stats for them + if (this.recentAppsRows > currentRows) { + this.appStatsToLoad = this.restrictApps(this.recentApps); + this.fetchAppStats(); + } + + // Only show the deploy app tiles in the full view + this.showDeployAppTiles = this.layout.x === 1 && this.layout.y === 1; + } + + // Fetch the app stats - we fetch two at a time + private fetchAppStats() { + if (this.appStatsToLoad.length > 0) { + const app = this.appStatsToLoad.shift(); + if (app.entity.state === 'STARTED') { + cfEntityCatalog.appStats.api.getMultiple(app.metadata.guid, this.guid).pipe( + map(a => a as ActionState), + pairwise(), + filter(([oldR, newR]) => oldR.busy && !newR.busy), + first() + ).subscribe(a => { + this.fetchAppStats(); + }); + } else { + this.fetchAppStats(); + } + } else { + this.appStatsLoaded.next(true); + } + } + + private restrictApps(apps: APIResource[]): APIResource[] { + return !apps ? [] :[...apps.sort(appDataSort).slice(0, this.recentAppsRows)]; + } + +} + diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.module.ts b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.module.ts new file mode 100644 index 0000000000..c9bd1f1179 --- /dev/null +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.module.ts @@ -0,0 +1,42 @@ +import { ComponentFactoryResolver, NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { CoreModule } from '../../../../../core/src/core/core.module'; +import { HomeModule } from '../../../../../core/src/features/home/home.module'; +import { SharedModule } from '../../../../../core/src/public-api'; +import { ApplicationStateService } from '../../../shared/services/application-state.service'; +import { ApplicationDeploySourceTypes } from '../../applications/deploy-application/deploy-application-steps.types'; +import { CardCfRecentAppsComponent } from '../card-cf-recent-apps/card-cf-recent-apps.component'; +import { CompactAppCardComponent } from '../card-cf-recent-apps/compact-app-card/compact-app-card.component'; +import { MDAppModule } from './../../../../../core/src/core/md.module'; +import { CFHomeCardComponent } from './cfhome-card.component'; + +@NgModule({ + imports: [ + CoreModule, + RouterModule, + MDAppModule, + SharedModule, + HomeModule, + ], + declarations: [ + CFHomeCardComponent, + CardCfRecentAppsComponent, + CompactAppCardComponent, + ], + exports: [ + CFHomeCardComponent, + CardCfRecentAppsComponent, + CompactAppCardComponent, + ], + providers: [ + ApplicationStateService, + ApplicationDeploySourceTypes, + ] +}) +export class CFHomeCardModule { + + public createHomeCard(componentFactoryResolver: ComponentFactoryResolver) { + return componentFactoryResolver.resolveComponentFactory(CFHomeCardComponent); + } +} diff --git a/src/frontend/packages/cloud-foundry/src/shared/cf-shared.module.ts b/src/frontend/packages/cloud-foundry/src/shared/cf-shared.module.ts index 206abb267a..46ec970398 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/cf-shared.module.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/cf-shared.module.ts @@ -31,8 +31,6 @@ import { CardCfInfoComponent } from './components/cards/card-cf-info/card-cf-inf import { CardCfOrgUserDetailsComponent, } from './components/cards/card-cf-org-user-details/card-cf-org-user-details.component'; -import { CardCfRecentAppsComponent } from './components/cards/card-cf-recent-apps/card-cf-recent-apps.component'; -import { CompactAppCardComponent } from './components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component'; import { CardCfSpaceDetailsComponent } from './components/cards/card-cf-space-details/card-cf-space-details.component'; import { CardCfUserInfoComponent } from './components/cards/card-cf-user-info/card-cf-user-info.component'; import { @@ -300,8 +298,6 @@ const cfListCards: Type>[] = [ CardCfUserInfoComponent, CardCfOrgUserDetailsComponent, CardCfSpaceDetailsComponent, - CardCfRecentAppsComponent, - CompactAppCardComponent, ServiceSummaryCardComponent, ServiceBrokerCardComponent, ServiceRecentInstancesCardComponent, @@ -349,8 +345,6 @@ const cfListCards: Type>[] = [ CardCfUserInfoComponent, CardCfOrgUserDetailsComponent, CardCfSpaceDetailsComponent, - CardCfRecentAppsComponent, - CompactAppCardComponent, ServiceSummaryCardComponent, ServiceBrokerCardComponent, ServiceRecentInstancesCardComponent, diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.html b/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.html deleted file mode 100644 index f26820ae69..0000000000 --- a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - Recently updated applications - - - -
- There are no applications. -
-
- -
-
-
-
\ No newline at end of file diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.ts b/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.ts deleted file mode 100644 index 136fc16ee1..0000000000 --- a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/card-cf-recent-apps.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; -import { filter, first, map, tap } from 'rxjs/operators'; - -import { CFAppState } from '../../../../../../cloud-foundry/src/cf-app-state'; -import { APIResource } from '../../../../../../store/src/types/api.types'; -import { IApp } from '../../../../cf-api.types'; -import { cfEntityCatalog } from '../../../../cf-entity-catalog'; -import { appDataSort, CloudFoundryEndpointService } from '../../../../features/cf/services/cloud-foundry-endpoint.service'; - -const RECENT_ITEMS_COUNT = 10; - -@Component({ - selector: 'app-card-cf-recent-apps', - templateUrl: './card-cf-recent-apps.component.html', - styleUrls: ['./card-cf-recent-apps.component.scss'], -}) -export class CardCfRecentAppsComponent implements OnInit { - - public recentApps$: Observable[]>; - @Input() allApps$: Observable[]>; - @Input() loading$: Observable; - @Output() refresh = new EventEmitter(); - - constructor( - private store: Store, - public cfEndpointService: CloudFoundryEndpointService, - ) { } - - ngOnInit() { - this.recentApps$ = this.allApps$.pipe( - filter(apps => !!apps), - first(), - map(apps => this.restrictApps(apps)), - tap(apps => this.fetchAppStats(apps)) - ); - } - - private fetchAppStats(recentApps: APIResource[]) { - recentApps.forEach(app => { - if (app.entity.state === 'STARTED') { - cfEntityCatalog.appStats.api.getMultiple(app.metadata.guid, this.cfEndpointService.cfGuid); - } - }); - } - - private restrictApps(apps: APIResource[]): APIResource[] { - if (!apps) { - return []; - } - return apps.sort(appDataSort).slice(0, RECENT_ITEMS_COUNT); - } - -} - - diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.html b/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.html deleted file mode 100644 index 994e79e2d9..0000000000 --- a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.html +++ /dev/null @@ -1,7 +0,0 @@ -
- - -
{{ app.metadata.updated_at | date:'medium' }}
-
diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss b/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss deleted file mode 100644 index ff8d0d2082..0000000000 --- a/src/frontend/packages/cloud-foundry/src/shared/components/cards/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -:host { - border: 1px solid #ccc; - display: flex; - padding: 0 12px; - - &:not(:first-child) { - border-top: 0; - } -} - -.compact-app-card { - align-items: center; - display: flex; - flex-direction: row; - font-size: 14px; - height: 40px; - width: 100%; - &__name { - flex: 1; - padding-left: 12px; - } -} diff --git a/src/frontend/packages/core/sass/_all-theme.scss b/src/frontend/packages/core/sass/_all-theme.scss index 1cb538fe61..35a77b74ea 100644 --- a/src/frontend/packages/core/sass/_all-theme.scss +++ b/src/frontend/packages/core/sass/_all-theme.scss @@ -53,13 +53,11 @@ @import './mat-desktop'; @import './fonts'; @import './ansi-colors'; -@import '../src/shared/components/favorites-global-list/favorites-global-list.component.theme'; -@import '../src/shared/components/favorites-meta-card/favorites-meta-card.component.theme'; - -@import '../../core/src/features/error-page/error-page/error-page.component.theme'; -@import '../../core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme'; -@import '../../core/src/features/metrics/metrics/metrics.component.theme'; - +@import '../src/features/home/home/favorites-meta-card/favorites-meta-card.component.theme'; +@import '../src/features/home//home/home-page-endpoint-card/home-page-endpoint-card.component.theme'; +@import '../src/features/error-page/error-page/error-page.component.theme'; +@import '../src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme'; +@import '../src/features/metrics/metrics/metrics.component.theme'; // Creates the app theme and applies it to the application // $theme = Angular Material Theme @@ -116,7 +114,6 @@ @include stateful-icon($theme, $app-theme); @include app-simple-usage-chart($theme, $app-theme); @include home-page-theme($theme, $app-theme); - @include favorites-global-list-theme($theme, $app-theme); @include favorites-meta-card-theme($theme, $app-theme); @include page-side-nav-theme($theme, $app-theme); @include entity-summary-title-theme($theme, $app-theme); @@ -129,5 +126,6 @@ @include metrics-component-theme($theme, $app-theme); @include intro-screen-theme($theme, $app-theme); @include app-json-view-theme($theme, $app-theme); + @include home-page-endpoint-card-theme($theme, $app-theme); } diff --git a/src/frontend/packages/core/src/core/endpoints.service.ts b/src/frontend/packages/core/src/core/endpoints.service.ts index 8da0f1d610..6d61ef7af4 100644 --- a/src/frontend/packages/core/src/core/endpoints.service.ts +++ b/src/frontend/packages/core/src/core/endpoints.service.ts @@ -24,6 +24,7 @@ export class EndpointsService implements CanActivate { haveRegistered$: Observable; haveConnected$: Observable; disablePersistenceFeatures$: Observable; + connectedEndpoints$: Observable; static getLinkForEndpoint(endpoint: EndpointModel): string { if (!endpoint) { @@ -44,18 +45,17 @@ export class EndpointsService implements CanActivate { ) { this.endpoints$ = store.select(endpointEntitiesSelector); this.haveRegistered$ = this.endpoints$.pipe(map(endpoints => !!Object.keys(endpoints).length)); - this.haveConnected$ = this.endpoints$.pipe(map(endpoints => - !!Object.values(endpoints).find(endpoint => { + this.connectedEndpoints$ = this.endpoints$.pipe(map(endpoints => + Object.values(endpoints).filter(endpoint => { const epType = entityCatalog.getEndpoint(endpoint.cnsi_type, endpoint.sub_type); if (!epType || !epType.definition) { return false; } const epEntity = epType.definition; - return epEntity.unConnectable || - endpoint.connectionStatus === 'connected' || - endpoint.connectionStatus === 'checking'; - })) - ); + return epEntity.unConnectable || endpoint.connectionStatus === 'connected' || endpoint.connectionStatus === 'checking'; + }) + )); + this.haveConnected$ = this.connectedEndpoints$.pipe(map(endpoints => endpoints.length > 0)); this.disablePersistenceFeatures$ = this.store.select('auth').pipe( map((auth) => auth.sessionData && diff --git a/src/frontend/packages/core/src/core/md.module.ts b/src/frontend/packages/core/src/core/md.module.ts index 0e401fd3a8..044d0f7b9b 100644 --- a/src/frontend/packages/core/src/core/md.module.ts +++ b/src/frontend/packages/core/src/core/md.module.ts @@ -1,5 +1,4 @@ - - +import { ScrollingModule } from '@angular/cdk/scrolling'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { MAT_MOMENT_DATE_FORMATS, MomentDateAdapter } from '@angular/material-moment-adapter'; @@ -10,7 +9,7 @@ import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatCardModule } from '@angular/material/card'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatChipsModule } from '@angular/material/chips'; -import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core'; +import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatExpansionModule } from '@angular/material/expansion'; @@ -35,6 +34,8 @@ import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; + + const importExport = [ CommonModule, MatButtonModule, @@ -66,7 +67,8 @@ const importExport = [ MatListModule, MatRadioModule, MatDatepickerModule, - MatBadgeModule + MatBadgeModule, + ScrollingModule, ]; @NgModule({ 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 d7ca4b06f5..8f1388b896 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 @@ -33,7 +33,7 @@ -
+
@@ -42,9 +42,10 @@ -
+
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 884ed4f392..fdceacd890 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 @@ -63,6 +63,10 @@ $app-header-height: 56px; min-width: auto; } } + &__side-narrow { + max-width: 400px; + min-width: 400px; + } } .page-content { 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 384cd1b40a..47e65121c6 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 @@ -17,7 +17,7 @@ import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-ca 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 { SidePanelMode, 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'; @@ -53,6 +53,9 @@ export class DashboardBaseComponent implements OnInit, OnDestroy, AfterViewInit @ViewChild('content') public content; + // Slide-in side panel mode + sidePanelMode: SidePanelMode = SidePanelMode.Modal; + constructor( public pageHeaderService: PageHeaderService, private store: Store, diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard.module.ts b/src/frontend/packages/core/src/features/dashboard/dashboard.module.ts index 6df87a993c..712f6f4b18 100644 --- a/src/frontend/packages/core/src/features/dashboard/dashboard.module.ts +++ b/src/frontend/packages/core/src/features/dashboard/dashboard.module.ts @@ -1,16 +1,18 @@ +import { ScrollingModule } from '@angular/cdk/scrolling'; import { NgModule } from '@angular/core'; import { CoreModule } from '../../core/core.module'; import { SharedModule } from '../../shared/shared.module'; -import { DashboardBaseComponent } from './dashboard-base/dashboard-base.component'; -import { SideNavComponent } from './side-nav/side-nav.component'; import { MetricsModule } from '../metrics/metrics.module'; +import { DashboardBaseComponent } from './dashboard-base/dashboard-base.component'; import { PageSideNavComponent } from './page-side-nav/page-side-nav.component'; +import { SideNavComponent } from './side-nav/side-nav.component'; @NgModule({ imports: [ CoreModule, + ScrollingModule, SharedModule, MetricsModule, ], diff --git a/src/frontend/packages/core/src/features/home/home.module.ts b/src/frontend/packages/core/src/features/home/home.module.ts index db80b5af85..702a3ae973 100644 --- a/src/frontend/packages/core/src/features/home/home.module.ts +++ b/src/frontend/packages/core/src/features/home/home.module.ts @@ -1,17 +1,32 @@ import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CoreModule, SharedModule } from '@stratosui/core'; -import { CoreModule } from '../../core/core.module'; -import { SharedModule } from '../../shared/shared.module'; +import { MDAppModule } from './../../core/md.module'; +import { FavoritesMetaCardComponent } from './home/favorites-meta-card/favorites-meta-card.component'; +import { FavoritesSidePanelComponent } from './home/favorites-side-panel/favorites-side-panel.component'; +import { HomePageEndpointCardComponent } from './home/home-page-endpoint-card/home-page-endpoint-card.component'; import { HomePageComponent } from './home/home-page.component'; +import { HomeShortcutsComponent } from './home/home-shortcuts/home-shortcuts.component'; @NgModule({ imports: [ CoreModule, + RouterModule, + MDAppModule, SharedModule, ], declarations: [ - HomePageComponent + HomePageComponent, + HomePageEndpointCardComponent, + FavoritesMetaCardComponent, + HomeShortcutsComponent, + FavoritesSidePanelComponent, + ], + exports: [ + FavoritesMetaCardComponent, + HomeShortcutsComponent, ] }) export class HomeModule { } diff --git a/src/frontend/packages/core/src/features/home/home.types.ts b/src/frontend/packages/core/src/features/home/home.types.ts new file mode 100644 index 0000000000..2e60d1afc8 --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home.types.ts @@ -0,0 +1,29 @@ +import { Observable } from 'rxjs'; + +import { HomeCardShortcut } from '../../../../store/src/entity-catalog/entity-catalog.types'; +import { EndpointModel } from '../../../../store/src/public-api'; +import { IHydrationResults } from '../../../../store/src/types/user-favorite-manager.types'; + +// Layout for a home page card + +// Defined in terms of how many cards we are trying to fit vertically and horizontally +export class HomePageCardLayout { + + public id: number; + + constructor(public x: number, public y: number, public title?: string) { + this.id = x + y * 1000; + } +} + +export abstract class HomePageEndpointCard { + public layout: HomePageCardLayout; + public endpoint: EndpointModel; + public load: () => Observable; +} + +export interface LinkMetadata { + favs: IHydrationResults[], + shortcuts: HomeCardShortcut[] +} + diff --git a/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.html b/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.html new file mode 100644 index 0000000000..cb1e36637a --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.html @@ -0,0 +1,10 @@ + +
+ {{ icon.icon }} +
+
+
{{ config.name }}
+
{{ prettyName }}
+
+ +
diff --git a/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.scss b/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.scss new file mode 100644 index 0000000000..7146bac920 --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.scss @@ -0,0 +1,41 @@ +.mat-card.fav-meta-card { + border-radius: 0; + box-shadow: none; +} + +.fav-meta-card { + align-items: center; + display: flex; + outline: none; + padding: 8px 10px; + &__clickable { + cursor: pointer; + } + &__icon { + margin-right: 4px; + opacity: .7; + } + &__icon-panel { + align-self: center; + display: flex; + justify-content: left; + width: 32px; + } + &__info { + flex: 1; + } + &__name { + font-size: 14px; + font-weight: bold; + margin-bottom: 4px; + word-break: break-all; + } + &__type { + font-size: 12px; + opacity: .6; + } + &__star { + flex: 0; + opacity: .7; + } +} diff --git a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.spec.ts b/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.spec.ts similarity index 89% rename from src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.spec.ts rename to src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.spec.ts index e76f49a277..72ac89c8f4 100644 --- a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.spec.ts +++ b/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.spec.ts @@ -1,6 +1,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { BaseTestModules } from '../../../../test-framework/core-test.helper'; +import { BaseTestModules } from '../../../../../test-framework/core-test.helper'; import { FavoritesMetaCardComponent } from './favorites-meta-card.component'; describe('FavoritesMetaCardComponent', () => { diff --git a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.theme.scss b/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.theme.scss similarity index 100% rename from src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.theme.scss rename to src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.theme.scss diff --git a/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.ts b/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.ts new file mode 100644 index 0000000000..c41abcc8dc --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/favorites-meta-card/favorites-meta-card.component.ts @@ -0,0 +1,47 @@ +import { Component, Input } from '@angular/core'; + +import { IFavoritesMetaCardConfig } from '../../../../../../store/src/favorite-config-mapper'; +import { entityCatalog } from '../../../../../../store/src/public-api'; +import { IFavoriteEntity } from '../../../../../../store/src/types/user-favorite-manager.types'; +import { IFavoriteMetadata, UserFavorite } from '../../../../../../store/src/types/user-favorites.types'; + +interface FavoriteIconData { + icon?: string; + iconFont?: string; +} + +@Component({ + selector: 'app-favorites-meta-card', + templateUrl: './favorites-meta-card.component.html', + styleUrls: ['./favorites-meta-card.component.scss'] +}) +export class FavoritesMetaCardComponent { + + @Input() + public endpoint; + + public favorite: UserFavorite; + + public prettyName: string; + + public config: IFavoritesMetaCardConfig; + + public icon: FavoriteIconData; + + @Input() + set favoriteEntity(favoriteEntity: IFavoriteEntity) { + if (favoriteEntity) { + const { cardMapper, favorite, prettyName } = favoriteEntity; + this.favorite = favorite; + this.prettyName = prettyName || 'Unknown'; + const entityDef = entityCatalog.getEntity(this.favorite.endpointType, this.favorite.entityType); + this.icon = { + icon: entityDef.definition.icon, + iconFont: entityDef.definition.iconFont, + }; + + const config = cardMapper && favorite && favorite.metadata ? cardMapper(favorite.metadata) : null; + this.config = config; + } + } +} diff --git a/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.html b/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.html new file mode 100644 index 0000000000..9fffafab65 --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.html @@ -0,0 +1,5 @@ + +
+ +
+
diff --git a/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.scss b/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.scss new file mode 100644 index 0000000000..c0137eecc0 --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.scss @@ -0,0 +1,7 @@ +.fav-side-panel { + &__card { + display: flex; + flex-direction: column; + margin-bottom: 6px; + } +} \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.spec.ts b/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.spec.ts new file mode 100644 index 0000000000..ba66b4964d --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FavoritesSidePanelComponent } from './favorites-side-panel.component'; + +describe('FavoritesSidePanelComponent', () => { + let component: FavoritesSidePanelComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ FavoritesSidePanelComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FavoritesSidePanelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.ts b/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.ts new file mode 100644 index 0000000000..cc886ef1dd --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/favorites-side-panel/favorites-side-panel.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { PreviewableComponent } from '../../../../shared/previewable-component'; + +@Component({ + selector: 'app-favorites-side-panel', + templateUrl: './favorites-side-panel.component.html', + styleUrls: ['./favorites-side-panel.component.scss'] +}) +export class FavoritesSidePanelComponent implements PreviewableComponent { + + favorites$: Observable; + name: string; + + setProps(props: { [key: string]: any; }): void { + this.favorites$ = props.favorites$ + this.name = props.endpoint.name; + } + +} diff --git a/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html new file mode 100644 index 0000000000..7cfde62e55 --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html @@ -0,0 +1,51 @@ + + +
+ +
+
+

+
+ {{ endpoint.name }} +
+

+
{{ definition.label }}
+
+
+ +
+
+ warning +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+ +
+
+
No favorites
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.scss b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.scss new file mode 100644 index 0000000000..7a34726535 --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.scss @@ -0,0 +1,123 @@ +.home-endpoint-card { + border-radius: 0; + display: flex; + flex-direction: column; + height: 100%; + outline: none; + padding: 0; + + &__header { + align-items: center; + display: flex; + margin: 8px 16px 16px 16px; + } + &__content { + display: flex; + height: 100%; + } + &__header-text { + font-size: 18px; + line-height: 24px; + margin-top: 0; + } + &__header-text-panel { + cursor: pointer; + margin-left: 12px; + width: 100%; + &:focus { + outline: none; + } + } + &__logo-panel { + cursor: pointer; + display: flex; + justify-content: center; + width: 42px; + &:focus { + outline: none; + } + } + &__logo { + height: 42px; + width: auto; + } + &__header-star { + display: flex; + margin-left: 20px; + opacity: 0.7; + } + &__error { + cursor: help; + display: flex; + } + &__type { + $type-height: 20px; + font-size: 13px; + line-height: $type-height; + margin-top: -($type-height - 2px); + opacity: .6; + } + &__panel { + display: flex; + :first-child { + flex: 1; + } + } +} + +.home-card { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + &__left { + flex: 1; + } + &__right { + flex: 0 0 30%; + padding: 16px; + min-width: 200px; + max-width: 400px; + + mat-card { + box-shadow: none; + } + } + &__right-2 { + min-width: 280px; + } + &__fav { + > * { + margin-bottom: 10px; + } + } + &__fav-none { + opacity: 0.7; + text-align: center; + } + &__fav-title-panel { + display: flex; + margin-bottom: 10px; + } + &__fav-title { + flex: 1; + } + &__fav-more { + font-size: 12px; + A { + cursor: pointer; + } + } + &__fav-card { + display: flex; + flex-direction: column; + margin-bottom: 5px; + } + &__shortcuts-left { + display: flex; + flex-direction: column; + margin-bottom: 10px; + margin-top: -10px; + padding: 0 16px; + } +} diff --git a/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.spec.ts b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.spec.ts new file mode 100644 index 0000000000..eb75b1541c --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.spec.ts @@ -0,0 +1,50 @@ +import { CommonModule } from '@angular/common'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { createBasicStoreModule } from '../../../../../../store/testing/public-api'; +import { CoreTestingModule } from '../../../../../test-framework/core-test.modules'; +import { CoreModule, SharedModule } from '../../../../public-api'; +import { SidePanelService } from '../../../../shared/services/side-panel.service'; +import { HomePageEndpointCardComponent } from './home-page-endpoint-card.component'; + +describe('HomePageEndpointCardComponent', () => { + let component: HomePageEndpointCardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HomePageEndpointCardComponent ], + imports: [ + CommonModule, + CoreModule, + SharedModule, + RouterTestingModule, + CoreTestingModule, + createBasicStoreModule() + ], + providers: [ + SidePanelService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomePageEndpointCardComponent); + fixture.componentInstance.endpoint = { + cnsi_type: 'metrics', + } as EndpointModel; + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + component.ngOnDestroy(); + }) + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.theme.scss b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.theme.scss new file mode 100644 index 0000000000..9b7a9ecb77 --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.theme.scss @@ -0,0 +1,16 @@ +@mixin home-page-endpoint-card-theme($theme, $app-theme) { + $foreground: map-get($theme, foreground); + + .home-endpoint-card { + &__content { + border-top: 1px solid mat-color($foreground, divider); + } + } + + .home-card__right { + border-left: 1px solid mat-color($foreground, divider); + mat-card { + border: 1px solid mat-color($foreground, divider);; + } + } +} diff --git a/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.ts b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.ts new file mode 100644 index 0000000000..7e2134242e --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.ts @@ -0,0 +1,233 @@ +import { + AfterViewInit, + Compiler, + Component, + ComponentRef, + EventEmitter, + Injector, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs'; +import { filter, first, map, timeout } from 'rxjs/operators'; + +import { + EntityCatalogSchemas, + IStratosEndpointDefinition, +} from '../../../../../../store/src/entity-catalog/entity-catalog.types'; +import { FavoritesConfigMapper } from '../../../../../../store/src/favorite-config-mapper'; +import { EndpointModel, entityCatalog } from '../../../../../../store/src/public-api'; +import { UserFavoriteManager } from '../../../../../../store/src/user-favorite-manager'; +import { SidePanelMode, SidePanelService } from '../../../../shared/services/side-panel.service'; +import { FavoritesSidePanelComponent } from '../favorites-side-panel/favorites-side-panel.component'; +import { UserFavoriteEndpoint } from './../../../../../../store/src/types/user-favorites.types'; +import { HomePageCardLayout, HomePageEndpointCard, LinkMetadata } from './../../home.types'; + +const MAX_FAVS_NORMAL = 15; +const MAX_FAVS_COMPACT = 5; +const CUTOFF_SHOW_SHORTCUTS_ON_LEFT = 10; +const MAX_SHORTCUTS = 5; +const MAX_LINKS = 5; + +@Component({ + selector: 'app-home-page-endpoint-card', + templateUrl: './home-page-endpoint-card.component.html', + styleUrls: ['./home-page-endpoint-card.component.scss'] +}) +export class HomePageEndpointCardComponent implements OnInit, OnDestroy, AfterViewInit { + + @ViewChild('customCard', {read:ViewContainerRef}) customCard: ViewContainerRef; + + @Input() endpoint: EndpointModel; + + _layout: HomePageCardLayout; + + get layout(): HomePageCardLayout { + return this._layout; + } + + @Input() set layout(value: HomePageCardLayout) { + if (value) { + this._layout = value; + } + this.updateLayout(); + }; + + @Output() loaded = new EventEmitter(); + + favorites$: Observable; + + layout$ = new BehaviorSubject(null); + + links$: Observable; + + entity; StratosCatalogEndpointEntity; + + definition: IStratosEndpointDefinition; + + favorite: UserFavoriteEndpoint; + + public link: string; + + load$: Observable; + loadSubj = new BehaviorSubject(false); + isLoading = false; + isError = false; + + private ref: ComponentRef; + private sub: Subscription; + + private canLoad = false; + + // Should we show shortcuts on the side or udner the manin panel? + showShortcutsOnSide = true; + hiddenFavorites = 0; + + // Should the Home Card use the whole width, or do we show the links panel as well? + fullView = false; + + constructor( + private favoritesConfigMapper: FavoritesConfigMapper, + private userFavoriteManager: UserFavoriteManager, + private sidePanelService: SidePanelService, + private compiler: Compiler, + private injector: Injector, + ) { + this.load$ = this.loadSubj.asObservable(); + } + + ngAfterViewInit() { + // Dynamically load the component for the Home Card for this endopoint + const endpointEntity = entityCatalog.getEndpoint(this.endpoint.cnsi_type, this.endpoint.sub_type) + if (endpointEntity && endpointEntity.definition.homeCard && endpointEntity.definition.homeCard.component) { + this.createCard(endpointEntity); + } else { + console.warn(`No endpoint home card for ${this.endpoint.guid}`); + } + } + + ngOnInit() { + // Favorites for this endpoint + this.favorites$ = this.userFavoriteManager.getFavoritesForEndpoint(this.endpoint.guid).pipe( + map(f => f.map(item => this.userFavoriteManager.mapToHydrated(item))) + ); + + this.entity = entityCatalog.getEndpoint(this.endpoint.cnsi_type, this.endpoint.sub_type) + if (this.entity) { + this.definition = this.entity.definition; + this.favorite = this.favoritesConfigMapper.getFavoriteEndpointFromEntity(this.endpoint); + this.fullView = this.definition?.homeCard?.fullView; + + const mapper = this.favoritesConfigMapper.getMapperFunction(this.favorite); + if (mapper && this.favorite.metadata) { + const p = mapper(this.favorite.metadata); + if (p) { + this.link = p.routerLink; + } + } + } + + this.links$ = combineLatest([this.favorites$, this.layout$.asObservable()]).pipe( + filter(([favs, layout]) => !!layout), + map(([favs, layout]) => { + // Get the list of shortcuts for the endpoint for the given endpoint ID + const allShortcuts = this.definition?.homeCard?.shortcuts(this.endpoint.guid) || []; + let shortcuts = allShortcuts; + const max = (layout.y > 1) ? MAX_FAVS_COMPACT : MAX_FAVS_NORMAL; + const totalShortcuts = allShortcuts.length; + this.hiddenFavorites = favs.length - max; + + // Based on the layout, adjust the numbers returned + if (layout.y > 1) { + // Compact card view + this.showShortcutsOnSide = true; + if (favs.length > max) { + favs = favs.slice(0, max); + } + if (totalShortcuts > MAX_SHORTCUTS) { + shortcuts = allShortcuts.slice(0, MAX_SHORTCUTS); + } + // We only want to display 5 things + if (favs.length + totalShortcuts > MAX_LINKS) { + let limit = MAX_LINKS - favs.length; + if (limit === 1) { + limit = 0; + } + shortcuts = allShortcuts.slice(0, limit); + } + } else { + // Full card view - move the shortcuts into the main left panel if we have more + // than a certain number of favorites to also show + if (favs.length >= CUTOFF_SHOW_SHORTCUTS_ON_LEFT) { + this.showShortcutsOnSide = false; + } + } + return { + favs, + shortcuts + }; + }) + ); + } + + ngOnDestroy() { + if (this.ref) { + this.ref.destroy(); + } + if (this.sub) { + this.sub.unsubscribe(); + } + } + + // Layout has changed + public updateLayout() { + this.layout$.next(this.layout); + if (this.ref && this.ref.instance) { + this.ref.instance.layout = this._layout; + } + } + + async createCard(endpointEntity: any) { + this.customCard.clear(); + const component = await endpointEntity.definition.homeCard.component(this.compiler, this.injector); + this.ref = this.customCard.createComponent(component); + this.ref.instance.endpoint = this.endpoint; + this.ref.instance.layout = this._layout; + this.loadCardIfReady(); + } + + // Load the card + public load() { + this.canLoad = true; + this.loadCardIfReady(); + } + + // Ask the card to load itself + loadCardIfReady() { + if (this.canLoad && this.ref && this.ref.instance && this.ref.instance.load) { + this.isLoading = true; + const loadObs = this.ref.instance.load() || of(true); + + // Timeout after 15 seconds + this.sub = loadObs.pipe(timeout(15000), filter(v => v === true), first()).subscribe(() => { + this.loaded.next(); + this.isLoading = false; + }, () => { + this.loaded.next(); + this.isLoading = false; + this.isError = true; + }); + } + } + + public showFavoritesPanel() { + this.sidePanelService.showMode(SidePanelMode.Narrow, FavoritesSidePanelComponent, { + endpoint: this.endpoint, + favorites$: this.favorites$ + }); + } +} diff --git a/src/frontend/packages/core/src/features/home/home/home-page.component.html b/src/frontend/packages/core/src/features/home/home/home-page.component.html index 90bd4aad65..d534f44f93 100644 --- a/src/frontend/packages/core/src/features/home/home/home-page.component.html +++ b/src/frontend/packages/core/src/features/home/home/home-page.component.html @@ -1,25 +1,37 @@ - +

Home

-
-
-
-
-

Favorites

- +
+
+ +
-
-
-
-

Recent Activity

+ +
+
+
+
-
+
+ +
+
+ + - \ No newline at end of file + diff --git a/src/frontend/packages/core/src/features/home/home/home-page.component.scss b/src/frontend/packages/core/src/features/home/home/home-page.component.scss index 3e0d7d0cf6..81e0860654 100644 --- a/src/frontend/packages/core/src/features/home/home/home-page.component.scss +++ b/src/frontend/packages/core/src/features/home/home/home-page.component.scss @@ -1,26 +1,3 @@ -@import '../../../../sass/mixins'; -$page-padding: 20px; -.favorites-list { - display: flex; - flex: 2; - flex-direction: column; - &__header { - align-items: center; - display: flex; - margin-bottom: 12px; - } -} -.recent-list { - display: flex; - flex: 1; - flex-direction: column; - margin: 0 -#{$page-padding} -#{$page-padding}; - padding: 20px; - @include breakpoint(tablet) { - margin: -#{$page-padding} -#{$page-padding} -#{$page-padding} $page-padding; - } -} - .home-page { display: flex; flex-direction: column; @@ -36,14 +13,52 @@ $page-padding: 20px; margin-bottom: 12px; } } + &__card { + height: 100%; + &:last-child { + margin-bottom: 20px; + } + } + // Grid layout + &__list { + display: grid; + flex: 1; + grid-column-gap: 10px; + grid-row-gap: 10px; + grid-auto-rows: min-content; + } + &__list-2 { + grid-template-columns: repeat(2, 1fr); + } + &__list-3 { + grid-template-columns: repeat(3, 1fr); + } +} - @include breakpoint(tablet) { - flex-direction: row; +.layout-menu { + display: flex; + &__label { + flex: 1 + } + mat-icon.layout-menu__icon { + flex: 0 0 20px; + display: flex; + align-self: center; + margin: 0 0 0 10px; + font-size: 20px; + width: 20px; + height: 20px; + line-height: 20px; + opacity: 0; + } + mat-icon.layout-menu__icon.layout-menu__tick { + opacity: 1; } - &__scroller { - margin: 0 -#{$page-padding} -#{$page-padding}; - overflow: auto; - padding: 0 $page-padding $page-padding; + &__tick { + font-weight: bold; } -} + &__sep { + margin: 10px 0; + } +} \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/home/home/home-page.component.spec.ts b/src/frontend/packages/core/src/features/home/home/home-page.component.spec.ts index 9579ad3f19..50bc61bf43 100644 --- a/src/frontend/packages/core/src/features/home/home/home-page.component.spec.ts +++ b/src/frontend/packages/core/src/features/home/home/home-page.component.spec.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; -import { createBasicStoreModule } from '@stratosui/store/testing'; +import { createEmptyStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../core/core.module'; @@ -25,7 +25,7 @@ describe('HomePageComponent', () => { RouterTestingModule, NoopAnimationsModule, CoreTestingModule, - createBasicStoreModule() + createEmptyStoreModule() ], providers: [ TabNavService, diff --git a/src/frontend/packages/core/src/features/home/home/home-page.component.theme.scss b/src/frontend/packages/core/src/features/home/home/home-page.component.theme.scss index 540fc44dcd..ca136124b1 100644 --- a/src/frontend/packages/core/src/features/home/home/home-page.component.theme.scss +++ b/src/frontend/packages/core/src/features/home/home/home-page.component.theme.scss @@ -6,4 +6,7 @@ background-color: darken(mat-color($background-colors, background), 5%); border-left: 1px solid mat-color($foreground, divider); } + .layout-menu__sep { + border-bottom: 1px solid mat-color($foreground, divider);; + } } diff --git a/src/frontend/packages/core/src/features/home/home/home-page.component.ts b/src/frontend/packages/core/src/features/home/home/home-page.component.ts index 94e3e09635..8b44305217 100644 --- a/src/frontend/packages/core/src/features/home/home/home-page.component.ts +++ b/src/frontend/packages/core/src/features/home/home/home-page.component.ts @@ -1,38 +1,83 @@ -import { Component } from '@angular/core'; +import { ScrollDispatcher } from '@angular/cdk/scrolling'; +import { DOCUMENT } from '@angular/common'; +import { + AfterViewInit, + Component, + ElementRef, + HostListener, + Inject, + OnDestroy, + OnInit, + QueryList, + ViewChild, + ViewChildren, +} from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs'; +import { debounceTime, filter, first, map, startWith } from 'rxjs/operators'; +import { SetHomeCardLayoutAction } from '../../../../../store/src/actions/dashboard-actions'; import { RouterNav } from '../../../../../store/src/actions/router.actions'; -import { AppState, IRequestEntityTypeState } from '../../../../../store/src/app-state'; -import { EntityCatalogHelpers } from '../../../../../store/src/entity-catalog/entity-catalog.helper'; -import { IUserFavoritesGroups } from '../../../../../store/src/types/favorite-groups.types'; -import { UserFavorite } from '../../../../../store/src/types/user-favorites.types'; +import { AppState } from '../../../../../store/src/app-state'; +import { EndpointModel, entityCatalog } from '../../../../../store/src/public-api'; +import { selectDashboardState } from '../../../../../store/src/selectors/dashboard.selectors'; import { UserFavoriteManager } from '../../../../../store/src/user-favorite-manager'; import { EndpointsService } from '../../../core/endpoints.service'; +import { IUserFavoritesGroups } from './../../../../../store/src/types/favorite-groups.types'; +import { HomePageCardLayout } from './../home.types'; +import { HomePageEndpointCardComponent } from './home-page-endpoint-card/home-page-endpoint-card.component'; @Component({ selector: 'app-home-page', templateUrl: './home-page.component.html', styleUrls: ['./home-page.component.scss'] }) -export class HomePageComponent { +export class HomePageComponent implements AfterViewInit, OnInit, OnDestroy { public allEndpointIds$: Observable; public haveRegistered$: Observable; - public showFilterToggle$: Observable; - public showFilters = false; + public endpoints$: Observable; + + public layouts$: Observable; + + private layout = new BehaviorSubject(null); + public layout$: Observable; + + public haveThingsToShow$: Observable; + + public columns = 1; + + public layoutID = 0; + + private layouts: HomePageCardLayout[] = [ + new HomePageCardLayout(0, 0, 'Automatic'), + null, + new HomePageCardLayout(1, 1, 'Single Column'), + new HomePageCardLayout(1, 2, 'Compact Single Column'), + new HomePageCardLayout(2, 1, 'Two Column'), + new HomePageCardLayout(2, 2, 'Compact Two Column'), + new HomePageCardLayout(3, 2, 'Three Column'), + ]; + + @ViewChild('endpointsPanel') endpointsPanel; + @ViewChildren(HomePageEndpointCardComponent) endpointCards: QueryList; + @ViewChildren('endpointCard') endpointElements: QueryList; + + notLoadedCardIndices: number[] = []; + cardsToLoad: HomePageEndpointCardComponent[] = []; + isLoadingACard = false; + + private viewMonitorSub: Subscription; + private cardChangesSub: Subscription; + private checkLayout = new BehaviorSubject(true); constructor( - endpointsService: EndpointsService, - store: Store, - public userFavoriteManager: UserFavoriteManager + public endpointsService: EndpointsService, + private store: Store, + public userFavoriteManager: UserFavoriteManager, + private scrollDispatcher: ScrollDispatcher, + @Inject(DOCUMENT) private document ) { - this.allEndpointIds$ = endpointsService.endpoints$.pipe( - map(endpoints => Object.values(endpoints).map(endpoint => endpoint.guid)) - ); - this.haveRegistered$ = endpointsService.haveRegistered$; - // Redirect to /applications if not enabled endpointsService.disablePersistenceFeatures$.pipe( map(off => { @@ -48,22 +93,207 @@ export class HomePageComponent { first() ).subscribe(); - this.showFilterToggle$ = userFavoriteManager.getAllFavorites().pipe( - map(([, favEntities]: [IUserFavoritesGroups, IRequestEntityTypeState]) => { - if (favEntities) { - for (const favEntity of Object.values(favEntities)) { - if (favEntity.entityType !== EntityCatalogHelpers.endpointType) { - return true; - } - } + this.layouts$ = of(this.layouts); + this.layout$ = this.layout.asObservable(); + this.allEndpointIds$ = this.endpointsService.connectedEndpoints$.pipe( + map(endpoints => Object.values(endpoints).map(endpoint => endpoint.guid)) + ); + this.haveRegistered$ = this.endpointsService.haveRegistered$; + const connected$ = this.endpointsService.connectedEndpoints$; + + // Only show endpoints that have Home Card metadata + this.endpoints$ = combineLatest([connected$, userFavoriteManager.getAllFavorites()]).pipe( + map(([endpoints, [favGroups, favs]]) => { + const ordered = this.orderEndpoints(endpoints, favGroups); + return ordered.filter(ep => { + const defn = entityCatalog.getEndpoint(ep.cnsi_type, ep.sub_type); + return !!defn.definition.homeCard; + }); + }) + ); + + this.haveThingsToShow$ = this.endpoints$.pipe(map(eps => eps.length > 0), startWith(true)); + + // Set an initial layout + this.layout.next(this.getLayout(1, 1)); + + this.store.select(selectDashboardState).pipe( + map(dashboardState => dashboardState.homeLayout || 0), + first() + ).subscribe(id => { + const selected = this.layouts.find(hpcl => hpcl && hpcl.id === id) || this.layouts[0]; + this.onChangeLayout(selected); + }) + } + + ngOnInit() { + const check$ = this.checkLayout.asObservable().pipe(filter(v => v)); + const scroll$ = this.scrollDispatcher.scrolled().pipe(map((e: any) => { + const el = e.elementRef.nativeElement; + return el.scrollTop; + }), startWith(0)); + + // Load cards as they come into view + this.viewMonitorSub = combineLatest([scroll$, check$]).pipe(debounceTime(200)).subscribe(([scrollTop, check]) => { + // User has scrolled - check the remaining cards that have not been loaded to see if any are now visible and shoule be loaded + // Only load the first one - after that one has loaded, we'll call this method again and check for the next one + const remaining = []; + const processedCard = false; + for (const index of this.notLoadedCardIndices) { + const cardElement = this.endpointElements.toArray()[index] as ElementRef; + const cardTop = cardElement.nativeElement.offsetTop; + const cardBottom = cardTop + cardElement.nativeElement.offsetHeight; + const height = this.endpointsPanel.nativeElement.offsetParent.offsetHeight; + const scrollBottom = scrollTop + height; + // Check if the card is in view - either its top or bottom must be withtin he visible scroll area + if ((cardTop >= scrollTop && cardTop <= scrollBottom) || (cardBottom >= scrollTop && cardBottom <= scrollBottom)) { + const card = this.endpointCards.toArray()[index]; + this.cardsToLoad.push(card); + } else { + remaining.push(index); + } + }; + this.processCardsToLoad(); + this.notLoadedCardIndices = remaining; + }) + } + + processCardsToLoad() { + if (!this.isLoadingACard && this.cardsToLoad.length > 0) { + const nextCardToLoad = this.cardsToLoad.shift(); + this.isLoadingACard = true; + nextCardToLoad.load(); + } + } + + ngOnDestroy() { + if (this.viewMonitorSub) { + this.viewMonitorSub.unsubscribe(); + } + if (this.cardChangesSub) { + this.cardChangesSub.unsubscribe(); + } + } + + ngAfterViewInit(): void { + this.cardChangesSub = this.endpointElements.changes.subscribe(cards => this.setCardsToLoad(cards)); + if (this.endpointElements.toArray().length > 0) { + this.setCardsToLoad(this.endpointElements.toArray()); + } + } + + setCardsToLoad(cards: any[]) { + this.notLoadedCardIndices = []; + for(let i=0;i< cards.length; i++) { + this.notLoadedCardIndices.push(i); + } + setTimeout(() => this.checkCardsInView(), 1); + } + + // This is called after a card has loaded - we call the scroll handler again + // to check if there are more cards that are visible and thus can be loaded + cardLoaded() { + this.isLoadingACard = false; + this.processCardsToLoad(); + this.checkCardsInView(); + } + + @HostListener('window:resize') + onResize() { + // If we resize the window and make it larger then new cards may come into view + this.checkCardsInView(); + } + + // Check the cards in view + checkCardsInView() { + this.checkLayout.next(true); + } + + // The layout was changed + public onChangeLayout(layout: HomePageCardLayout) { + this.layoutID = layout.id; + + // If the layout is automatic, then adjust based on number of things to show + const lay$ = layout.id === 0 ? this.automaticLayout() : of(layout); + lay$.pipe(first()).subscribe(lo => { + this.layout.next(lo); + + // Update the grid columns based on the layout + this.columns = lo.x; + + // Persist the state + this.store.dispatch(new SetHomeCardLayoutAction(this.layoutID)); + + // Ensure we check again if any cards are now visible + // Schedule the check so it happens afer the cards have been laid out + setTimeout(() => this.checkCardsInView(), 1); + }); + } + + // Order the endpoint cards - we always show all endpoints, order is: + // 1. Endpoint has been added as a favourite + // 2. Endpoint that has child favourites + // 3. Remaining endpoints + private orderEndpoints(endpoints: EndpointModel[], favorites: IUserFavoritesGroups): EndpointModel[] { + const processed = {}; + const result = []; + + Object.keys(favorites).forEach(fav => { + if (!favorites[fav].ethereal) { + const id = this.userFavoriteManager.getEndpointIDFromFavoriteID(fav); + if (!!endpoints[id] && !processed[id]) { + processed[id] = true; + result.push(endpoints[id]); + } + } + }); + + Object.keys(favorites).forEach(fav => { + if (favorites[fav].ethereal) { + const id = this.userFavoriteManager.getEndpointIDFromFavoriteID(fav); + if (!!endpoints[id] && !processed[id]) { + processed[id] = true; + result.push(endpoints[id]); + } + } + }); + + endpoints.forEach(ep => { + if (!processed[ep.guid]) { + processed[ep.guid] = true; + result.push(ep); + } + }) + + // Filter out the disconnected ones + return result.filter(ep => ep.connectionStatus === 'connected'); + } + + // Automatic layout - select the best layout based on the available endpoints + private automaticLayout(): Observable { + return this.endpointsService.connectedEndpoints$.pipe( + map(eps => eps.filter(ep => { + const defn = entityCatalog.getEndpoint(ep.cnsi_type, ep.sub_type); + return !!defn.definition.homeCard; + })), + map(eps => { + switch(eps.length) { + case 1: + return this.getLayout(1, 1); + case 2: + return this.getLayout(1, 2); + case 3: + return this.getLayout(2, 2); + case 4: + return this.getLayout(2, 2); + default: + return this.getLayout(3, 2); } - return false; }) ); } - public toggleShowFilters() { - this.showFilters = !this.showFilters; + private getLayout(x: number, y: number): HomePageCardLayout { + return this.layouts.find(item => item && item.x === x && item.y === y); } } - diff --git a/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.html b/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.html new file mode 100644 index 0000000000..d9f0a887dc --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.html @@ -0,0 +1,9 @@ +
+
Shortcuts
+
+
+
{{ shortcut.icon }}
+ +
+
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.scss b/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.scss new file mode 100644 index 0000000000..8cba704199 --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.scss @@ -0,0 +1,23 @@ +.home-shortcut { + &__title { + margin-bottom: 10px; + margin-top: 20px; + } + &__item { + align-items: center; + display: flex; + margin-bottom: 10px; + margin-left: 12px; + + mat-icon { + cursor: pointer; + opacity: 0.7; + outline: none; + text-align: center; + } + } + &__icon { + margin-right: 8px; + width: 24px; + } +} diff --git a/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.spec.ts b/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.spec.ts new file mode 100644 index 0000000000..3495a47019 --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeShortcutsComponent } from './home-shortcuts.component'; + +describe('HomeShortcutsComponent', () => { + let component: HomeShortcutsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HomeShortcutsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeShortcutsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.ts b/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.ts new file mode 100644 index 0000000000..1b665083bd --- /dev/null +++ b/src/frontend/packages/core/src/features/home/home/home-shortcuts/home-shortcuts.component.ts @@ -0,0 +1,14 @@ +import { Component, Input } from '@angular/core'; + +import { HomeCardShortcut } from '../../../../../../store/src/entity-catalog/entity-catalog.types'; + +@Component({ + selector: 'app-home-shortcuts', + templateUrl: './home-shortcuts.component.html', + styleUrls: ['./home-shortcuts.component.scss'] +}) +export class HomeShortcutsComponent { + + @Input() shortcuts: HomeCardShortcut[]; + +} 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 b0747565c1..8290625d06 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 @@ -1,6 +1,6 @@
- + {{ icon }}
void | string; @Output() showAlerts = new EventEmitter(); + @Input() mode: string; @Input('alerts') set alerts(alerts) { diff --git a/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.html b/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.html deleted file mode 100644 index 450f368b5f..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.html +++ /dev/null @@ -1,37 +0,0 @@ -
-
- - - - - - - - - {{favoriteType.prettyName}} - - - -
-
No favorites found for the current filters.
-
- -
- - - -
- -
-
- - - -
-
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.scss b/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.scss deleted file mode 100644 index 474191d473..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -@import '../../../../sass/mixins'; - -.favorite-entity-list { - $bottom-space: 30px; - display: flex; - flex-direction: column; - &__search-form { - padding-left: 5px; - padding-top: 1.25em; - } - &__name-search { - padding-right: 20px; - } - &__cards { - display: grid; - grid-column-gap: 10px; - grid-row-gap: 10px; - grid-template-columns: 1fr; - min-height: 0; - min-width: 0; - @include breakpoint(tablet) { - grid-column-gap: $bottom-space; - grid-row-gap: $bottom-space; - grid-template-columns: repeat(3, 1fr); - } - } - &__card { - min-width: 0; - } - &__expand-button { - height: $bottom-space; - line-height: $bottom-space; - margin: auto; - margin-bottom: -20px; - margin-top: 10px; - width: $bottom-space; - } - &__hide-filters { - height: 0; - visibility: hidden; - } -} diff --git a/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.spec.ts b/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.spec.ts deleted file mode 100644 index a6bd579fc2..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { BaseTestModulesNoShared } from '../../../../test-framework/core-test.helper'; -import { SharedModule } from '../../shared.module'; -import { FavoritesEntityListComponent } from './favorites-entity-list.component'; - -describe('FavoritesEntityListComponent', () => { - let component: FavoritesEntityListComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - ...BaseTestModulesNoShared, - SharedModule - ], - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(FavoritesEntityListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.ts b/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.ts deleted file mode 100644 index ff7bccbc37..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-entity-list/favorites-entity-list.component.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { combineLatest, Observable, ReplaySubject, Subject } from 'rxjs'; -import { distinctUntilChanged, map, scan, startWith } from 'rxjs/operators'; - -import { FavoritesConfigMapper, IFavoriteTypes } from '../../../../../store/src/favorite-config-mapper'; -import { IFavoriteEntity } from '../../../../../store/src/types/user-favorite-manager.types'; - - -@Component({ - selector: 'app-favorites-entity-list', - templateUrl: './favorites-entity-list.component.html', - styleUrls: ['./favorites-entity-list.component.scss'] -}) -export class FavoritesEntityListComponent implements OnInit { - - constructor(private favoritesConfigMapper: FavoritesConfigMapper) {} - - @Input() - set entities(favoriteEntities: IFavoriteEntity[]) { - this.pEntities = favoriteEntities ? [...favoriteEntities] : favoriteEntities; - this.entitiesSubject.next(favoriteEntities); - this.hasEntities = this.pEntities && this.pEntities.length > 0; - } - @Input() - public placeholder = false; - - @Input() - public showFilters = false; - - @Input() - public endpointDisconnected = false; - - @Input() - public autoExpand = false; - - @Input() - set endpointTypes(types: string[] | string) { - if (!this.favoriteTypes) { - if (Array.isArray(types)) { - this.favoriteTypes = types.reduce((allTypes, endpointType) => { - return [ - ...allTypes, - ...this.favoritesConfigMapper.getAllTypesForEndpoint(endpointType) - ]; - }, []); - } else { - this.favoriteTypes = this.favoritesConfigMapper.getAllTypesForEndpoint(types) || []; - } - } - } - - private searchValueSubject = new Subject(); - public set searchValue(searchValue: string) { - this.searchValueSubject.next(searchValue); - } - - public hasEntities = false; - public typeSubject = new ReplaySubject(); - private entitiesSubject = new ReplaySubject(); - private limitToggleSubject = new ReplaySubject(); - - private entities$ = this.entitiesSubject.asObservable(); - public limitedEntities$: Observable; - public searchedEntities$: Observable; - - public noResultsDueToFilter$: Observable; - - public favoriteTypes: IFavoriteTypes[] = null; - - // User to filter favorites - public filterName: string; - public filterType: string; - - public pEntities: IFavoriteEntity[] = []; - - public limitedEntities: IFavoriteEntity[]; - public minLimit = 3; - public limit = this.minLimit; - - public limitToggle$ = this.limitToggleSubject.asObservable().pipe( - scan((acc) => this.minLimit === acc ? null : this.minLimit, this.minLimit), - startWith(this.minLimit) - ); - - public toggleExpand() { - this.limitToggleSubject.next(this.minLimit ? null : this.minLimit); - } - - public typeChanged(type: string) { - this.typeSubject.next(type); - } - - private limitEntities(entities: IFavoriteEntity[], limit: number) { - if (!entities || limit === null) { - return entities; - } else { - return entities.splice(0, limit); - } - } - - public trackByFavoriteId(index: number, entity: IFavoriteEntity) { - return entity.favorite.guid; - } - - ngOnInit() { - const searchValue$ = this.searchValueSubject.pipe(startWith(''), distinctUntilChanged()); - const type$ = this.typeSubject.asObservable().pipe(startWith(null)); - const typesEntities$ = combineLatest( - this.entities$, - type$ - ).pipe( - map(([entities, type]) => { - if (!type) { - return entities; - } - return entities.filter(entity => entity.favorite.entityType === type); - }) - ); - - this.searchedEntities$ = combineLatest( - typesEntities$, - searchValue$.pipe(startWith('')), - ).pipe( - map(([entities, nameSearch]) => { - if (!nameSearch) { - return entities; - } - const searchableEntities = [...entities]; - return searchableEntities.filter(entity => entity.cardMapper(entity.favorite.metadata).name.search(nameSearch) !== -1); - }), - map(searchEntities => searchEntities || []) - ); - - this.limitedEntities$ = combineLatest( - this.searchedEntities$, - this.limitToggle$ - ).pipe( - map(([entities, limit]) => this.limitEntities([...entities], limit)), - map(limitedEntities => limitedEntities || []) - ); - - this.noResultsDueToFilter$ = combineLatest( - searchValue$, - type$, - this.limitedEntities$, - ).pipe( - map(([nameSearch, type, entities]) => entities.length === 0 && (!!nameSearch || !!type)), - startWith(false) - ); - - if (this.autoExpand) { - this.toggleExpand(); - } - } -} diff --git a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.html b/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.html deleted file mode 100644 index 394ead2585..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
Could not fetch favorites
-
- -
- -
-
- - -
-
-
-
-
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.scss b/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.scss deleted file mode 100644 index b1651f7ee0..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.scss +++ /dev/null @@ -1,23 +0,0 @@ -.favorite-list { - &__group { - margin-bottom: 24px; - } - &__endpoint-card { - margin-bottom: 10px; - min-height: 200px; - } - &__entities { - display: flex; - flex-direction: column; - padding-bottom: 24px; - width: 100%; - } - &__empty-text { - margin-bottom: 20px; - } - &__seperator { - border-bottom: 1px solid rgba(0, 0, 0, .2); - margin-left: 25%; - width: 50%; - } -} diff --git a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.spec.ts b/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.spec.ts deleted file mode 100644 index b429b9affc..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { BaseTestModules } from '../../../../test-framework/core-test.helper'; -import { FavoritesGlobalListComponent } from './favorites-global-list.component'; - -describe('FavoritesGlobalListComponent', () => { - let component: FavoritesGlobalListComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [...BaseTestModules], - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(FavoritesGlobalListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.theme.scss b/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.theme.scss deleted file mode 100644 index d30d6062db..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.theme.scss +++ /dev/null @@ -1,8 +0,0 @@ -@mixin favorites-global-list-theme($theme, $app-theme) { - $foreground: map-get($theme, foreground); - .favorite-list { - &__seperator { - border-bottom: 1px solid mat-color($foreground, divider); - } - } -} diff --git a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.ts b/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.ts deleted file mode 100644 index f8313df5a2..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-global-list/favorites-global-list.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { AppState } from '../../../../../store/src/app-state'; -import { getFavoriteInfoObservable } from '../../../../../store/src/helpers/store-helpers'; -import { IFavoriteEntity, IGroupedFavorites } from '../../../../../store/src/types/user-favorite-manager.types'; -import { IFavoritesInfo } from '../../../../../store/src/types/user-favorites.types'; -import { UserFavoriteManager } from '../../../../../store/src/user-favorite-manager'; - - -@Component({ - selector: 'app-favorites-global-list', - templateUrl: './favorites-global-list.component.html', - styleUrls: ['./favorites-global-list.component.scss'] -}) -export class FavoritesGlobalListComponent implements OnInit { - public favInfo$: Observable; - public favoriteGroups$: Observable; - constructor( - private store: Store, - private userFavoriteManager: UserFavoriteManager - ) { } - - @Input() showFilters: boolean; - - ngOnInit() { - this.favoriteGroups$ = this.userFavoriteManager.hydrateAllFavorites().pipe( - map(favs => this.sortFavoriteGroups(favs)) - ); - - this.favInfo$ = getFavoriteInfoObservable(this.store); - } - - private sortFavoriteGroups(entityGroups: IGroupedFavorites[]) { - if (!entityGroups) { - return entityGroups; - } - return entityGroups.map(group => { - if (group.entities) { - group.entities = group.entities.sort(this.sortFavoriteGroup); - } - return group; - }); - } - - private sortFavoriteGroup(entityA: IFavoriteEntity, entityB: IFavoriteEntity) { - if (entityA.favorite.entityType < entityB.favorite.entityType) { - return -1; - } - if (entityA.favorite.entityType > entityB.favorite.entityType) { - return 1; - } - return 0; - } - - public trackByEndpointId(index: number, group: IGroupedFavorites) { - return group.endpoint.favorite.guid; - } -} diff --git a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.html b/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.html deleted file mode 100644 index 3c28d55952..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.html +++ /dev/null @@ -1,65 +0,0 @@ - - -
-
- -
-
- {{ icon.icon }} -
-
-

-
- {{ name$ | async }} -
Disconnected
-
-

-

- {{ name$ | async }} -

-
{{ prettyName }}
-
-
-
- - - {{ line[0] }} - {{ value }} - - -
- -
Favorite not found
- Could - not - fetch {{ prettyName | - lowercase }} - Endpoint - disconnected -
-
Type
-
{{ prettyName }}
-
ID
-
{{ favorite.entityId }}
- -
- -
- - -
Favorite endpoint is not registered
- - To unfavorite this endpoint, please unfavorite all child entities first. - -
-
ID
-
{{ favorite.endpointId }}
-
-
Disconnected
- -
- - - \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.scss b/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.scss deleted file mode 100644 index bbb07aa386..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.scss +++ /dev/null @@ -1,93 +0,0 @@ -.fav-meta-card { - min-height: 160px; - outline: none; - &__header-text { - line-height: 24px; - margin-top: 0; - } - &__type { - $type-height: 20px; - font-size: 13px; - line-height: $type-height; - margin-top: -($type-height - 2px); - opacity: .6; - } - &__missing { - align-items: center; - display: flex; - flex-direction: column; - height: 100%; - justify-content: space-around; - padding: 10px; - padding-top: 20px; - &-text { - font-size: 20px; - font-weight: bold; - } - &-small-text { - margin-bottom: 20px; - } - } - &__clickable { - cursor: pointer; - } - &__header-text-panel { - width: 100%; - } - &__header { - display: flex; - } - &__logo-panel { - display: flex; - justify-content: left; - width: 56px; - } - &__logo { - height: 48px; - margin-right: 8px; - width: auto; - } - &__icon { - margin-right: 4px; - opacity: .7; - } - &__icon-panel { - display: flex; - justify-content: left; - width: 32px; - } - &__panel { - display: flex; - :first-child { - flex: 1; - } - } - &__disconnected { - align-self: center; - border-radius: 2px; - display: flex; - font-size: 14px; - font-weight: normal; - margin-right: 8px; - padding: 2px 4px; - } -} - -.error-details { - display: flex; - flex-direction: column; - padding: 0 20px; - &__value { - padding-bottom: 5px; - } - &__label { - font-size: 10px; - opacity: .6; - } -} - -.subtle-text { - opacity: .6; -} - - diff --git a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.ts b/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.ts deleted file mode 100644 index 06367a9196..0000000000 --- a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { isObservable, Observable, of as observableOf } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; -import { IFavoritesMetaCardConfig } from '../../../../../store/src/favorite-config-mapper'; -import { stratosEntityFactory, userFavouritesEntityType } from '../../../../../store/src/helpers/stratos-entity-factory'; -import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; -import { MenuItem } from '../../../../../store/src/types/menu-item.types'; -import { ComponentEntityMonitorConfig, StratosStatus } from '../../../../../store/src/types/shared.types'; -import { IFavoriteEntity } from '../../../../../store/src/types/user-favorite-manager.types'; -import { IFavoriteMetadata, UserFavorite } from '../../../../../store/src/types/user-favorites.types'; -import { isEndpointConnected } from '../../../features/endpoints/connect.service'; -import { ConfirmationDialogConfig } from '../confirmation-dialog.config'; -import { ConfirmationDialogService } from '../confirmation-dialog.service'; - -interface FavoriteIconData { - hasIcon: boolean; - icon?: string; - iconFont?: string; - logoUrl?: string; -} - -@Component({ - selector: 'app-favorites-meta-card', - templateUrl: './favorites-meta-card.component.html', - styleUrls: ['./favorites-meta-card.component.scss'] -}) -export class FavoritesMetaCardComponent { - @Input() - public compact = false; - - @Input() - public placeholder = false; - - @Input() - public endpoint = false; - - @Input() - public endpointHasEntities = false; - - @Input() - public endpointDisconnected = false; - - public config: IFavoritesMetaCardConfig; - - public status$: Observable; - - public favorite: UserFavorite; - - /* - We use this to pass the favorite to the metacard, this dictates if we should show the favorite star or not. - We do not want to show the favorite star for endpoints that have favorite entities. - */ - public metaFavorite: UserFavorite; - - public entityConfig: ComponentEntityMonitorConfig; - - public showMore: boolean; - - public prettyName: string; - - public confirmation: ConfirmationDialogConfig; - - public endpointConnected$: Observable; - public name$: Observable; - public routerLink$: Observable; - public actions$: Observable; - - // Optional icon for the favorite - public iconUrl$: Observable; - - // Optional icon for the favorite - public icon: FavoriteIconData; - - @Input() - set favoriteEntity(favoriteEntity: IFavoriteEntity) { - if (!this.placeholder && favoriteEntity) { - const endpoint$ = stratosEntityCatalog.endpoint.store.getEntityMonitor(favoriteEntity.favorite.endpointId).entity$; - this.endpointConnected$ = endpoint$.pipe(map(endpoint => isEndpointConnected(endpoint))); - this.actions$ = this.endpointConnected$.pipe( - map(connected => connected ? this.config.menuItems : []) - ); - const { cardMapper, favorite, prettyName } = favoriteEntity; - this.favorite = favorite; - this.metaFavorite = !this.endpoint || (this.endpoint && !this.endpointHasEntities) ? favorite : null; - this.prettyName = prettyName || 'Unknown'; - this.entityConfig = new ComponentEntityMonitorConfig(favorite.guid, stratosEntityFactory(userFavouritesEntityType)); - - // If this favorite is an endpoint, lookup the image for it from the entity catalog - if (this.favorite.entityType === 'endpoint') { - this.iconUrl$ = endpoint$.pipe(map(a => entityCatalog.getEndpoint(a.cnsi_type, a.sub_type).definition.logoUrl)); - } else { - this.iconUrl$ = observableOf(''); - } - - const entityDef = entityCatalog.getEntity(this.favorite.endpointType, this.favorite.entityType); - this.icon = { - hasIcon: !!entityDef.definition.logoUrl || !!entityDef.definition.icon, - icon: entityDef.definition.icon, - iconFont: entityDef.definition.iconFont, - logoUrl: entityDef.definition.logoUrl, - }; - - this.setConfirmation(this.prettyName, favorite); - - const config = cardMapper && favorite && favorite.metadata ? cardMapper(favorite.metadata) : null; - - if (config) { - this.name$ = observableOf(config.name); - if (this.endpoint) { - this.routerLink$ = this.endpointConnected$.pipe(map(connected => connected ? config.routerLink : '/endpoints')); - } else { - this.routerLink$ = this.endpointConnected$.pipe(map(connected => connected ? config.routerLink : null)); - } - config.lines = this.mapLinesToObservables(config.lines); - this.config = config; - } - } - } - - constructor( - private confirmDialog: ConfirmationDialogService - ) { } - - public setConfirmation(prettyName: string, favorite: UserFavorite) { - this.confirmation = new ConfirmationDialogConfig( - `Unfavorite ${prettyName}`, - `Are you sure you would like to unfavorite this ${prettyName.toLocaleLowerCase()} with the id ${favorite.entityId}?`, - 'Yes', - true - ); - } - - public confirmFavoriteRemoval() { - this.confirmDialog.open(this.confirmation, this.removeFavorite); - } - - private removeFavorite = () => { - stratosEntityCatalog.userFavorite.api.delete(this.favorite); - }; - - public toggleMoreError() { - this.showMore = !this.showMore; - } - - private mapLinesToObservables(lines: [string, string | Observable][]) { - if (!lines) { - return []; - } - return lines.map(line => { - const [label, value] = line; - if (!isObservable(value)) { - return [ - label, - observableOf(value) - ] as [string, Observable]; - } - return line; - }); - } - -} diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.html index 0223537772..e275f7d2b1 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.html @@ -1,5 +1,5 @@ + [ngClass]="{'meta-card-pointer': clickAction, 'meta-card__plain': mode === 'plain'}" (click)="clickAction ? clickAction() : null">
Deleting
diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.scss b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.scss index fb7bf82696..6cbe51da8e 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.scss @@ -83,3 +83,17 @@ outline: 0; } } + +.mat-card.meta-card__plain { + border-radius: 0; + box-shadow: none; + padding: 5px 10px; + + .meta-card__header { + padding: 0; + } + + .meta-card__title { + margin: 0; + } +} diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.theme.scss b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.theme.scss index e414c3477a..e93de0d3e5 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.theme.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.theme.scss @@ -10,4 +10,22 @@ .meta-card__header__popup-separator { background-color: mat-color($foreground, divider); } + + .mat-card.meta-card__plain { + border: 1px solid mat-color($foreground, divider); + + .meta-card__title { + margin: 0; + + h3 { + font-size: 14px; + } + .fav-meta-card__type { + font-size: 12px; + } + } + .meta-card__favorite { + align-self: center; + } + } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.ts b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.ts index 039954d7f2..35e79c78c2 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.ts @@ -55,6 +55,9 @@ export class MetaCardComponent implements OnDestroy { @Input() statusBackground = false; + @Input() + mode: string; + @Input() clickAction: () => void = null; diff --git a/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.html b/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.html index 70e96a89cc..ad85e67465 100644 --- a/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.html +++ b/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.html @@ -28,11 +28,6 @@
{{ countedEntity.entity.name }}
-
{{ countedEntity.subText$ | async }}
diff --git a/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.ts b/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.ts index 17f69a6c3e..3d250cab68 100644 --- a/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.ts +++ b/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.ts @@ -23,14 +23,16 @@ class RenderableRecent { const catalogEntity = entityCatalog.getEntity(entity.endpointType, entity.entityType); this.icon = catalogEntity.definition.icon; this.iconFont = catalogEntity.definition.iconFont; - + if (!entity.endpointId) { + console.error(`Entity ${entity.guid} does not contain a value for endpointId - recent metadata is malformed`); + } if (entity.entityType === endpointEntityType) { this.subText$ = observableOf(entity.prettyType); } else { this.subText$ = this.store.select(endpointEntitiesSelector).pipe( map(endpoints => { - if (Object.keys(endpoints).length > 1) { - return `${entity.prettyType} - ${endpoints[entity.endpointId].name} (${entity.prettyEndpointType})`; + if (entity.endpointId && Object.keys(endpoints).length > 1) { + return `${entity.prettyType} - ${endpoints[entity.endpointId].name}`; } return entity.prettyType; }) diff --git a/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.html b/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.html index eefa5a8c67..27a7354ad6 100644 --- a/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.html +++ b/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.scss b/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.scss index 2a6328ad84..3eaf453929 100644 --- a/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.scss +++ b/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.scss @@ -79,4 +79,12 @@ &.smaller { @include create(160px, 60px); } + + &.compact { + @include create(160px, 60px); + h3 { + font-size: 14px; + margin: 10px; + } + } } diff --git a/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.ts b/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.ts index 5b11d6ac48..f8b9bf391f 100644 --- a/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.ts +++ b/src/frontend/packages/core/src/shared/components/tile-selector-tile/tile-selector-tile.component.ts @@ -15,6 +15,8 @@ export class TileSelectorTileComponent { @Input() smaller = false; + @Input() compact = false; + @Output() tileSelect = new EventEmitter(); public onClick(tile: ITileConfig) { diff --git a/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.html b/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.html index 7f05aa5e7d..3fbe888568 100644 --- a/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.html +++ b/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.html @@ -12,6 +12,6 @@ + [active]="selected === tile || !selected" [smaller]="smallerTiles" [compact]="compactTiles"> \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.ts b/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.ts index d3a34f1f11..6a55092a17 100644 --- a/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.ts +++ b/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.ts @@ -13,6 +13,7 @@ export class TileSelectorComponent { public hiddenOptions: ITileConfig[] = []; public showingMore = false; @Input() smallerTiles = false; + @Input() compactTiles = false; private pDynamicSmallerTiles = 0; @Input() set dynamicSmallerTiles(tiles: number) { this.pDynamicSmallerTiles = tiles; diff --git a/src/frontend/packages/core/src/shared/services/side-panel.service.ts b/src/frontend/packages/core/src/shared/services/side-panel.service.ts index 2b86182a17..c58dac3de2 100644 --- a/src/frontend/packages/core/src/shared/services/side-panel.service.ts +++ b/src/frontend/packages/core/src/shared/services/side-panel.service.ts @@ -11,6 +11,16 @@ import { Router } from '@angular/router'; import { asapScheduler, BehaviorSubject, Observable, Subject } from 'rxjs'; import { filter, observeOn, publishReplay, refCount, tap } from 'rxjs/operators'; +// Side Panel Modes +export enum SidePanelMode { + // Modal = spans the full height of the window and overlaps the title bar + Modal = 0, + // Normal = 600px width and height not overlapping title bar + Normal = 1, + // Narrow = 400px width and height not overlapping title bar + Narrow = 2, +} + /** * Service to allow the overlay side panel to be shown or hidden. * @@ -23,8 +33,8 @@ export class SidePanelService { private openedSubject: BehaviorSubject; public opened$: Observable; - private previewModeSubject: BehaviorSubject; - public previewMode$: Observable; + private previewModeSubject: BehaviorSubject; + public previewMode$: Observable; private container: ViewContainerRef; @@ -36,7 +46,7 @@ export class SidePanelService { this.openedSubject = new BehaviorSubject(false); this.opened$ = this.observeSubject(this.openedSubject); - this.previewModeSubject = new BehaviorSubject(false); + this.previewModeSubject = new BehaviorSubject(-1); this.previewMode$ = this.observeSubject(this.previewModeSubject); this.setupRouterListener(); @@ -55,29 +65,31 @@ export class SidePanelService { } /** - * Show the preview panel in a preview style - does not overlap title bar and colours are more muted + * Show the preview panel in the given mode */ - public show(component: object, props?: { [key: string]: any }, componentFactoryResolver?: ComponentFactoryResolver) { + public showMode( + mode: SidePanelMode, component: object, props?: { [key: string]: any }, componentFactoryResolver?: ComponentFactoryResolver) { if (!this.container) { throw new Error('SidePanelService: container must be set'); } this.render(component, props, componentFactoryResolver); - this.previewModeSubject.next(true); + this.previewModeSubject.next(mode); this.open(); } + /** + * Show the preview panel in a preview style - does not overlap title bar and colours are more muted + */ + public show(component: object, props?: { [key: string]: any }, componentFactoryResolver?: ComponentFactoryResolver) { + this.showMode(SidePanelMode.Normal, component, props, componentFactoryResolver); + } + /** * Show the preview panel in a modal style - full height overlaps title bar */ public showModal(component: object, props?: { [key: string]: any }, componentFactoryResolver?: ComponentFactoryResolver) { - if (!this.container) { - throw new Error('SidePanelService: container must be set'); - } - - this.render(component, props, componentFactoryResolver); - this.previewModeSubject.next(false); - this.open(); + this.showMode(SidePanelMode.Modal, component, props, componentFactoryResolver); } private open() { diff --git a/src/frontend/packages/core/src/shared/shared.module.ts b/src/frontend/packages/core/src/shared/shared.module.ts index cb3c000b5d..6b5989eda5 100644 --- a/src/frontend/packages/core/src/shared/shared.module.ts +++ b/src/frontend/packages/core/src/shared/shared.module.ts @@ -34,9 +34,6 @@ import { EditableDisplayValueComponent } from './components/editable-display-val import { EndpointsMissingComponent } from './components/endpoints-missing/endpoints-missing.component'; import { EntitySummaryTitleComponent } from './components/entity-summary-title/entity-summary-title.component'; import { EnumerateComponent } from './components/enumerate/enumerate.component'; -import { FavoritesEntityListComponent } from './components/favorites-entity-list/favorites-entity-list.component'; -import { FavoritesGlobalListComponent } from './components/favorites-global-list/favorites-global-list.component'; -import { FavoritesMetaCardComponent } from './components/favorites-meta-card/favorites-meta-card.component'; import { FileInputComponent } from './components/file-input/file-input.component'; import { FocusDirective } from './components/focus.directive'; import { IntroScreenComponent } from './components/intro-screen/intro-screen.component'; @@ -195,9 +192,6 @@ import { UserPermissionDirective } from './user-permission.directive'; MetricsParentRangeSelectorComponent, StackedInputActionsComponent, StackedInputActionComponent, - FavoritesGlobalListComponent, - FavoritesMetaCardComponent, - FavoritesEntityListComponent, MultilineTitleComponent, TileSelectorComponent, MarkdownPreviewComponent, @@ -289,8 +283,6 @@ import { UserPermissionDirective } from './user-permission.directive'; MetricsParentRangeSelectorComponent, StackedInputActionsComponent, StackedInputActionComponent, - FavoritesMetaCardComponent, - FavoritesGlobalListComponent, MultilineTitleComponent, PageSubNavComponent, BreadcrumbsComponent, diff --git a/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.html b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.html new file mode 100644 index 0000000000..bebe8b03a3 --- /dev/null +++ b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + +
+ +
+
+
+
diff --git a/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.scss b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.scss new file mode 100644 index 0000000000..31d2803d6c --- /dev/null +++ b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.scss @@ -0,0 +1,10 @@ +.k8s-home-card { + &__plain-tiles { + margin-left: 0; + margin-right: 1em; + margin-top: 1em; + } + &__shortcuts { + padding: 0 16px; + } +} diff --git a/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.spec.ts b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.spec.ts new file mode 100644 index 0000000000..392a169015 --- /dev/null +++ b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.spec.ts @@ -0,0 +1,32 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EndpointModel } from '../../../../store/src/types/endpoint.types'; +import { BaseKubeGuid } from '../kubernetes-page.types'; +import { KubernetesBaseTestModules } from '../kubernetes.testing.module'; +import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service'; +import { KubernetesHomeCardComponent } from './kubernetes-home-card.component'; + +describe('KubernetesHomeCardComponent', () => { + let component: KubernetesHomeCardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ KubernetesHomeCardComponent ], + imports: [...KubernetesBaseTestModules], + providers: [ KubernetesEndpointService, BaseKubeGuid ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubernetesHomeCardComponent); + component = fixture.componentInstance; + component.endpoint = {} as EndpointModel; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.ts b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.ts new file mode 100644 index 0000000000..1d4926219c --- /dev/null +++ b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.component.ts @@ -0,0 +1,105 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { combineLatest, Observable } from 'rxjs'; +import { first, map } from 'rxjs/operators'; + +import { HomePageCardLayout } from '../../../../core/src/features/home/home.types'; +import { HomeCardShortcut } from '../../../../store/src/entity-catalog/entity-catalog.types'; +import { EndpointModel } from '../../../../store/src/public-api'; +import { kubeEntityCatalog } from '../kubernetes-entity-catalog'; +import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service'; + +@Component({ + selector: 'app-k8s-home-card', + templateUrl: './kubernetes-home-card.component.html', + styleUrls: ['./kubernetes-home-card.component.scss'] +}) +export class KubernetesHomeCardComponent implements OnInit { + + @Input() endpoint: EndpointModel; + + _layout: HomePageCardLayout; + + get layout(): HomePageCardLayout { + return this._layout; + } + + @Input() set layout(value: HomePageCardLayout) { + if (value) { + this._layout = value; + } + }; + + public shortcuts: HomeCardShortcut[]; + + public podCount$: Observable; + public nodeCount$: Observable; + public namespaceCount$: Observable; + + constructor(private kubeEndpointService: KubernetesEndpointService) { } + + ngOnInit() { + const guid = this.endpoint.guid; + if (guid) { + this.kubeEndpointService.initialize(this.endpoint.guid); + } + + this.shortcuts = [ + { + title: 'View Nodes', + link: ['/kubernetes', guid, 'nodes'], + icon: 'node', + iconFont: 'stratos-icons' + }, + { + title: 'View Namespaces', + link: ['/kubernetes', guid, 'namespaces'], + icon: 'namespace', + iconFont: 'stratos-icons' + } + ]; + } + + // Card is instructed to load its view by the container, whn it is visible + load(): Observable { + const guid = this.endpoint.guid; + const podsObs = kubeEntityCatalog.pod.store.getPaginationService(guid); + const pods$ = podsObs.entities$; + const nodesObs = kubeEntityCatalog.node.store.getPaginationService(guid); + const nodes$ = nodesObs.entities$; + const namespacesObs = kubeEntityCatalog.namespace.store.getPaginationService(guid); + const namespaces$ = namespacesObs.entities$; + + this.podCount$ = pods$.pipe(map(entities => entities.length)); + this.nodeCount$ = nodes$.pipe(map(entities => entities.length)); + this.namespaceCount$ = namespaces$.pipe(map(entities => entities.length)); + + this.kubeEndpointService.kubeTerminalEnabled$.pipe(first()).subscribe(hasKubeTerminal => { + if (hasKubeTerminal) { + this.shortcuts.push( + { + title: 'Open Terminal', + link: ['/kubernetes', guid, 'terminal'], + icon: 'terminal', + iconFont: 'stratos-icons' + } + ); + } + }); + + this.kubeEndpointService.kubeDashboardConfigured$.pipe(first()).subscribe(hasKubeDashboard => { + if (hasKubeDashboard) { + this.shortcuts.push( + { + title: 'View Dashboard', + link: ['/kubernetes', guid, 'dashboard'], + icon: 'dashboard' + } + ); + } + }); + + return combineLatest([this.podCount$, this.nodeCount$, this.namespaceCount$]).pipe( + map(() => true) + ); + } +} diff --git a/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.module.ts b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.module.ts new file mode 100644 index 0000000000..099bdbb362 --- /dev/null +++ b/src/frontend/packages/kubernetes/src/kubernetes/home/kubernetes-home-card.module.ts @@ -0,0 +1,30 @@ +import { ComponentFactoryResolver, NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { CoreModule } from '../../../../core/src/core/core.module'; +import { HomeModule } from '../../../../core/src/features/home/home.module'; +import { SharedModule } from '../../../../core/src/public-api'; +import { MDAppModule } from './../../../../core/src/core/md.module'; +import { KubernetesHomeCardComponent } from './kubernetes-home-card.component'; + +@NgModule({ + imports: [ + CoreModule, + RouterModule, + MDAppModule, + SharedModule, + HomeModule, + ], + declarations: [ + KubernetesHomeCardComponent, + ], + exports: [ + KubernetesHomeCardComponent, + ], +}) +export class KubernetesHomeCardModule { + + public createHomeCard(componentFactoryResolver: ComponentFactoryResolver) { + return componentFactoryResolver.resolveComponentFactory(KubernetesHomeCardComponent); + } +} diff --git a/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-entity-generator.ts b/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-entity-generator.ts index 7f5c090d20..ecbc803a2e 100644 --- a/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-entity-generator.ts +++ b/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-entity-generator.ts @@ -1,3 +1,4 @@ +import { Compiler, Injector } from '@angular/core'; import { Validators } from '@angular/forms'; import { BaseEndpointAuth } from '../../../core/src/core/endpoint-auth'; @@ -7,12 +8,14 @@ import { StratosCatalogEntity, } from '../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { + IEntityMetadata, IStratosEntityDefinition, StratosEndpointExtensionDefinition, } from '../../../store/src/entity-catalog/entity-catalog.types'; import { EndpointAuthTypeConfig, EndpointType } from '../../../store/src/extension-types'; +import { FavoritesConfigMapper } from '../../../store/src/favorite-config-mapper'; import { metricEntityType } from '../../../store/src/helpers/stratos-entity-factory'; -import { IFavoriteMetadata } from '../../../store/src/types/user-favorites.types'; +import { IFavoriteMetadata, UserFavorite } from '../../../store/src/types/user-favorites.types'; import { KubernetesAWSAuthFormComponent } from './auth-forms/kubernetes-aws-auth-form/kubernetes-aws-auth-form.component'; import { KubernetesCertsAuthFormComponent, @@ -66,6 +69,13 @@ import { } from './store/kube.types'; import { generateWorkloadsEntities } from './workloads/store/workloads-entity-generator'; + +export interface IKubeResourceFavMetadata extends IFavoriteMetadata { + guid: string; + kubeGuid: string; + name: string; +} + const enum KubeEndpointAuthTypes { CERT_AUTH = 'kube-cert-auth', CONFIG = 'kubeconfig', @@ -136,6 +146,23 @@ const kubeAuthTypeMap: { [type: string]: EndpointAuthTypeConfig, } = { } }; +function k8sShortcuts(id: string) { + return [ + { + title: 'View Nodes', + link: ['/kubernetes', id, 'nodes'], + icon: 'node', + iconFont: 'stratos-icons' + }, + { + title: 'View Namespaces', + link: ['/kubernetes', id, 'namespaces'], + icon: 'namespace', + iconFont: 'stratos-icons' + } + ]; +} + export function generateKubernetesEntities(): StratosBaseCatalogEntity[] { const endpointDefinition: StratosEndpointExtensionDefinition = { type: KUBERNETES_ENDPOINT_TYPE, @@ -151,6 +178,7 @@ export function generateKubernetesEntities(): StratosBaseCatalogEntity[] { BaseEndpointAuth.UsernamePassword, kubeAuthTypeMap[KubeEndpointAuthTypes.TOKEN], ], + favoriteFromEntity: getFavoriteFromKubeEntity, renderPriority: 4, subTypes: [ { @@ -196,7 +224,17 @@ export function generateKubernetesEntities(): StratosBaseCatalogEntity[] { authTypes: [BaseEndpointAuth.UsernamePassword, kubeAuthTypeMap[KubeEndpointAuthTypes.TOKEN]], logoUrl: '/core/assets/custom/k3s.svg', renderPriority: 6 - }] + }], + homeCard: { + component: (compiler: Compiler, injector: Injector) => import('./home/kubernetes-home-card.module').then((m) => { + return compiler.compileModuleAndAllComponentsAsync(m.KubernetesHomeCardModule).then(cm => { + const mod = cm.ngModuleFactory.create(injector); + return mod.instance.createHomeCard(mod.componentFactoryResolver); + }); + }), + fullView: true + // shortcuts: k8sShortcuts + } }; return [ generateEndpointEntity(endpointDefinition), @@ -275,11 +313,27 @@ function generateNamespacesEntity(endpointDefinition: StratosEndpointExtensionDe const definition: IStratosEntityDefinition = { type: kubernetesNamespacesEntityType, schema: kubernetesEntityFactory(kubernetesNamespacesEntityType), - endpoint: endpointDefinition + endpoint: endpointDefinition, + label: 'Namespace', + icon: 'namespace', + iconFont: 'stratos-icons', }; - kubeEntityCatalog.namespace = new StratosCatalogEntity(definition, { - actionBuilders: kubeNamespaceActionBuilders - }); + kubeEntityCatalog.namespace = new StratosCatalogEntity( + definition, { + actionBuilders: kubeNamespaceActionBuilders, + entityBuilder: { + getMetadata: (namespace: any) => { + return { + endpointId: namespace.kubeGuid, + guid: namespace.metadata.uid, + kubeGuid: namespace.kubeGuid, + name: namespace.metadata.name, + }; + }, + getLink: metadata => `/kubernetes/${metadata.kubeGuid}/namespaces/${metadata.name}y`, + getGuid: metadata => metadata.guid, + } + }); return kubeEntityCatalog.namespace; } @@ -329,3 +383,16 @@ function generateMetricEntity(endpointDefinition: StratosEndpointExtensionDefini }; return new StratosCatalogEntity(definition); } + +function getFavoriteFromKubeEntity( + entity, + entityType: string, + favoritesConfigMapper: FavoritesConfigMapper +): UserFavorite { + return favoritesConfigMapper.getFavoriteFromEntity( + entityType, + KUBERNETES_ENDPOINT_TYPE, + entity.kubeGuid, + entity + ); +} diff --git a/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-namespace/kubernetes-namespace.component.html b/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-namespace/kubernetes-namespace.component.html index 8a042faa2a..749c7af66f 100644 --- a/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-namespace/kubernetes-namespace.component.html +++ b/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-namespace/kubernetes-namespace.component.html @@ -1,4 +1,4 @@ - +

{{ kubeNamespaceService.namespaceName }}

diff --git a/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts b/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts index 0a7eab7a1e..fe8ecff185 100644 --- a/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts +++ b/src/frontend/packages/kubernetes/src/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts @@ -1,14 +1,19 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; +import { IAppFavMetadata } from '../../../../cloud-foundry/src/cf-metadata-types'; import { IHeaderBreadcrumb } from '../../../../core/src/shared/components/page-header/page-header.types'; +import { FavoritesConfigMapper } from '../../../../store/src/favorite-config-mapper'; +import { getFavoriteFromEntity } from '../../../../store/src/user-favorite-helpers'; +import { kubernetesNamespacesEntityType } from '../kubernetes-entity-factory'; import { BaseKubeGuid } from '../kubernetes-page.types'; import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service'; import { KubernetesNamespaceService } from '../services/kubernetes-namespace.service'; import { KubernetesAnalysisService } from '../services/kubernetes.analysis.service'; import { KubernetesService } from '../services/kubernetes.service'; +import { KUBERNETES_ENDPOINT_TYPE } from './../kubernetes-entity-factory'; @Component({ selector: 'app-kubernetes-namespace', @@ -42,6 +47,7 @@ export class KubernetesNamespaceComponent { public kubeEndpointService: KubernetesEndpointService, public kubeNamespaceService: KubernetesNamespaceService, public analysisService: KubernetesAnalysisService, + private favoritesConfigMapper: FavoritesConfigMapper, ) { this.breadcrumbs$ = kubeEndpointService.endpoint$.pipe( map(endpoint => ([{ @@ -58,4 +64,18 @@ export class KubernetesNamespaceComponent { { link: 'analysis', label: 'Analysis', icon: 'assignment', hidden$: this.analysisService.hideAnalysis$ }, ]; } + + public favorite$ = this.kubeNamespaceService.namespace$.pipe( + filter(app => !!app), + map(namespace => getFavoriteFromEntity( + { + kubeGuid: this.kubeEndpointService.baseKube.guid, + ...namespace, + prettyText: 'Kubernetes Namespace', + }, + kubernetesNamespacesEntityType, + this.favoritesConfigMapper, + KUBERNETES_ENDPOINT_TYPE + )) + ); } diff --git a/src/frontend/packages/kubernetes/src/kubernetes/services/kubernetes-namespace.service.ts b/src/frontend/packages/kubernetes/src/kubernetes/services/kubernetes-namespace.service.ts index d6c62cf663..ef31dc8889 100644 --- a/src/frontend/packages/kubernetes/src/kubernetes/services/kubernetes-namespace.service.ts +++ b/src/frontend/packages/kubernetes/src/kubernetes/services/kubernetes-namespace.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { filter, first, map, publishReplay } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { getIdFromRoute } from '../../../../core/src/core/utils.service'; import { kubeEntityCatalog } from '../kubernetes-entity-catalog'; @@ -23,13 +23,6 @@ export class KubernetesNamespaceService { this.kubeGuid = kubeEndpointService.kubeGuid; const namespaceEntity = kubeEntityCatalog.namespace.store.getEntityService(this.namespaceName, this.kubeGuid); - - this.namespace$ = namespaceEntity.entityObs$.pipe( - filter(p => !!p), - map(p => p.entity), - publishReplay(1), - first() - ); - + this.namespace$ = namespaceEntity.waitForEntity$.pipe(map(e => e.entity)); } } diff --git a/src/frontend/packages/store/src/actions/dashboard-actions.ts b/src/frontend/packages/store/src/actions/dashboard-actions.ts index a2bc55101a..81634f7550 100644 --- a/src/frontend/packages/store/src/actions/dashboard-actions.ts +++ b/src/frontend/packages/store/src/actions/dashboard-actions.ts @@ -13,7 +13,8 @@ export const DISABLE_SIDE_NAV_MOBILE_MODE = '[Dashboard] Disable mobile nav'; export const TIMEOUT_SESSION = '[Dashboard] Timeout Session'; export const ENABLE_POLLING = '[Dashboard] Enable Polling'; export const SET_STRATOS_THEME = '[Dashboard] Set Theme'; -export const GRAVATAR_ENABLED = '[Dashboard] Gravatar ENabled'; +export const GRAVATAR_ENABLED = '[Dashboard] Gravatar Enabled'; +export const HOME_CARD_LAYOUT = '[Dashboard] Home Card Layout'; export const HYDRATE_DASHBOARD_STATE = '[Dashboard] Hydrate dashboard state'; @@ -56,6 +57,12 @@ export class SetGravatarEnabledAction implements Action { constructor(public enableGravatar = true) { } type = GRAVATAR_ENABLED; } + +export class SetHomeCardLayoutAction implements Action { + constructor(public id = 0) { } + type = HOME_CARD_LAYOUT; +} + export class HydrateDashboardStateAction implements Action { constructor(public dashboardState: DashboardState) { } type = HYDRATE_DASHBOARD_STATE; diff --git a/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts b/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts index e82fff15c7..ac1571cb94 100644 --- a/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts +++ b/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts @@ -1,6 +1,8 @@ +import { Compiler, ComponentFactory, Injector } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { HomePageEndpointCard } from '../../../core/src/features/home/home.types'; import { IListAction } from '../../../core/src/shared/components/list/list.component.types'; import { AppState, GeneralEntityAppState } from '../app-state'; import { @@ -60,6 +62,20 @@ export interface IEntityMetadata { [key: string]: string; } +export interface HomeCardShortcut { + title: string; + link: string[]; + icon: string; + iconFont?: string; +} + +// Metadata for Home Card +export interface HomeCardMetadata { + component?: (compiler: Compiler, injector: Injector) => Promise>; + shortcuts?: (endpointID: string) => HomeCardShortcut[]; + fullView?: boolean; +} + /** * Static information describing a base stratos entity. * @@ -161,6 +177,11 @@ export interface IStratosEndpointDefinition) => IListAction[]; + + /** + * Metadata for the card to show on the Home Page for this endpoint type + */ + readonly homeCard?: HomeCardMetadata; } export interface StratosEndpointExtensionDefinition extends Omit { } @@ -209,7 +230,6 @@ export type EntityRowBuilder = [string, (entity: T, store?: Store { getMetadata(entity: Y): T; - getStatusObservable?(entity: Y): Observable; // TODO This should be used in the entities schema. getGuid(entityMetadata: T): string; getLink?(entityMetadata: T): string; diff --git a/src/frontend/packages/store/src/favorite-config-mapper.ts b/src/frontend/packages/store/src/favorite-config-mapper.ts index 8fa3cd6890..f92a71dc95 100644 --- a/src/frontend/packages/store/src/favorite-config-mapper.ts +++ b/src/frontend/packages/store/src/favorite-config-mapper.ts @@ -139,6 +139,9 @@ export class FavoritesConfigMapper { const entityType = isEndpoint ? EntityCatalogHelpers.endpointType : entityDefinition.type; const metadata = catalogEntity.builders.entityBuilder.getMetadata(entity); const guid = isEndpoint ? null : catalogEntity.builders.entityBuilder.getGuid(metadata); + if (!endpointId) { + console.error('User favourite - buildFavoriteFromCatalogEntity - endpointId is undefined'); + } return new UserFavorite( endpointId, endpointType, diff --git a/src/frontend/packages/store/src/reducers/dashboard-reducer.ts b/src/frontend/packages/store/src/reducers/dashboard-reducer.ts index a1737e8cd9..f6a5c6422f 100644 --- a/src/frontend/packages/store/src/reducers/dashboard-reducer.ts +++ b/src/frontend/packages/store/src/reducers/dashboard-reducer.ts @@ -1,4 +1,3 @@ -import { GRAVATAR_ENABLED, SetGravatarEnabledAction } from './../actions/dashboard-actions'; import { CLOSE_SIDE_NAV, DISABLE_SIDE_NAV_MOBILE_MODE, @@ -14,6 +13,12 @@ import { TIMEOUT_SESSION, TOGGLE_SIDE_NAV, } from '../actions/dashboard-actions'; +import { + GRAVATAR_ENABLED, + HOME_CARD_LAYOUT, + SetGravatarEnabledAction, + SetHomeCardLayoutAction, +} from './../actions/dashboard-actions'; export interface DashboardState { timeoutSession: boolean; @@ -25,6 +30,7 @@ export interface DashboardState { themeKey: string; headerEventMinimized: boolean; gravatarEnabled: boolean; + homeLayout: number; } export const defaultDashboardState: DashboardState = { @@ -37,6 +43,7 @@ export const defaultDashboardState: DashboardState = { themeKey: null, headerEventMinimized: false, gravatarEnabled: false, + homeLayout: 0, }; export function dashboardReducer(state: DashboardState = defaultDashboardState, action): DashboardState { @@ -78,7 +85,13 @@ export function dashboardReducer(state: DashboardState = defaultDashboardState, ...state, gravatarEnabled: gravatarAction.enableGravatar }; - case HYDRATE_DASHBOARD_STATE: + case HOME_CARD_LAYOUT: + const layoutAction = action as SetHomeCardLayoutAction; + return { + ...state, + homeLayout: layoutAction.id + }; + case HYDRATE_DASHBOARD_STATE: const hydrateDashboardStateAction = action as HydrateDashboardStateAction; return { ...state, diff --git a/src/frontend/packages/store/src/user-favorite-manager.ts b/src/frontend/packages/store/src/user-favorite-manager.ts index a1e0d211e6..030ff5dc5b 100644 --- a/src/frontend/packages/store/src/user-favorite-manager.ts +++ b/src/frontend/packages/store/src/user-favorite-manager.ts @@ -106,7 +106,7 @@ export class UserFavoriteManager { }); } - private mapToHydrated = (favorite: UserFavorite): IHydrationResults => { + public mapToHydrated = (favorite: UserFavorite): IHydrationResults => { const catalogEntity = entityCatalog.getEntity(favorite.endpointType, favorite.entityType); return { @@ -130,4 +130,27 @@ export class UserFavoriteManager { public toggleFavorite(favorite: UserFavorite) { stratosEntityCatalog.userFavorite.api.toggle(favorite); } + + // Get all favorites for the given endpoint ID + public getFavoritesForEndpoint(endpointID: string): Observable[]> { + const waitForFavorites$ = this.getWaitForFavoritesObservable(); + const favoriteEntities$ = this.store.select(favoriteEntitiesSelector); + return waitForFavorites$.pipe(switchMap(() => favoriteEntities$)).pipe( + map(favs => { + const result = []; + Object.values(favs).forEach(f => { + if (f.endpointId === endpointID && f.entityId) { + result.push(f); + } + }) + return result; + }) + ) + } + + public getEndpointIDFromFavoriteID(id: string): string { + const p = id.split('-'); + const idParts = p.slice(0, p.length - 2); + return idParts.join('-'); + } } diff --git a/src/frontend/packages/store/testing/src/store-test-helper.ts b/src/frontend/packages/store/testing/src/store-test-helper.ts index 01f8bda343..79b65c66d9 100644 --- a/src/frontend/packages/store/testing/src/store-test-helper.ts +++ b/src/frontend/packages/store/testing/src/store-test-helper.ts @@ -167,6 +167,7 @@ function getDefaultInitialTestStratosStoreState() { themeKey: null, headerEventMinimized: true, gravatarEnabled: false, + homeLayout: 0, }, lists: {}, routing: { diff --git a/src/test-e2e/cloud-foundry/org-level/cf-org-delete-e2e.spec.ts b/src/test-e2e/cloud-foundry/org-level/cf-org-delete-e2e.spec.ts index 7ce53760c4..b22d92c86c 100644 --- a/src/test-e2e/cloud-foundry/org-level/cf-org-delete-e2e.spec.ts +++ b/src/test-e2e/cloud-foundry/org-level/cf-org-delete-e2e.spec.ts @@ -4,6 +4,7 @@ import { e2e } from '../../e2e'; import { CFHelpers } from '../../helpers/cf-e2e-helpers'; import { ConsoleUserType, E2EHelpers } from '../../helpers/e2e-helpers'; import { CFPage } from '../../po/cf-page.po'; +import { ListComponent } from '../../po/list.po'; import { SideNavMenuItem } from '../../po/side-nav.po'; import { CfTopLevelPage } from '../cf-level/cf-top-level-page.po'; @@ -51,6 +52,8 @@ describe('Delete Organization', () => { cfPage.waitForPageOrChildPage(); cfPage.loadingIndicator.waitUntilNotShown(); cfPage.goToOrgTab(); + const list = new ListComponent(); + list.header.refresh(); cfPage.deleteOrg(orgName); expect(element(by.tagName('app-cards')).getText()).not.toContain(orgName); }); diff --git a/src/test-e2e/cloud-foundry/space-level/cf-space-delete-e2e.spec.ts b/src/test-e2e/cloud-foundry/space-level/cf-space-delete-e2e.spec.ts index c6dc55efbc..2fe4c6f079 100644 --- a/src/test-e2e/cloud-foundry/space-level/cf-space-delete-e2e.spec.ts +++ b/src/test-e2e/cloud-foundry/space-level/cf-space-delete-e2e.spec.ts @@ -62,6 +62,7 @@ describe('Delete Space', () => { // Find the Org and click on it const list = new ListComponent(); + list.header.refresh(); return list.cards.findCardByTitle(orgName, MetaCardTitleType.CUSTOM, true).then(card => { expect(card).toBeDefined(); card.click();