From ed9c6aed8075522698871a901c950f2b77922719 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Sun, 5 Jul 2020 17:32:39 +0100 Subject: [PATCH 01/11] Improve recent and favourites display --- .../cloud-foundry/src/cf-entity-generator.ts | 13 +- src/frontend/packages/core/src/app.module.ts | 4 +- .../favorites-meta-card.component.html | 7 +- .../favorites-meta-card.component.scss | 9 +- .../favorites-meta-card.component.ts | 22 +++ .../page-header.component.theme.scss | 3 - .../page-header/page-header.component.ts | 2 + .../recent-entities.component.html | 51 ++---- .../recent-entities.component.scss | 11 ++ .../recent-entities.component.ts | 165 ++++-------------- .../src/actions/recently-visited.actions.ts | 12 +- .../entity-catalog/entity-catalog.types.ts | 1 + .../recently-visited.reducer.helpers.ts | 150 +++++----------- .../recently-visited.reducer.ts | 14 +- .../store/src/types/recently-visited.types.ts | 20 +-- 15 files changed, 174 insertions(+), 310 deletions(-) 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 89727e826e..07b5489a05 100644 --- a/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts +++ b/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts @@ -1143,10 +1143,11 @@ function generateCfApplicationEntity(endpointDefinition: StratosEndpointExtensio label: 'Application', labelPlural: 'Applications', endpoint: endpointDefinition, + icon: 'apps', tableConfig: { rowBuilders: [ ['Name', (entity) => entity.entity.name], - ['Creation Date', (entity) => entity.metadata.created_at] + ['Created', (entity) => entity.metadata.created_at] ] } }; @@ -1172,7 +1173,7 @@ function generateCfApplicationEntity(endpointDefinition: StratosEndpointExtensio getLink: metadata => `/applications/${metadata.cfGuid}/${metadata.guid}/summary`, getGuid: metadata => metadata.guid, getLines: () => ([ - ['Creation Date', (meta) => meta.createdAt] + ['Created', (meta) => meta.createdAt] ]) }, actionBuilders: applicationActionBuilder @@ -1191,6 +1192,8 @@ function generateCfSpaceEntity(endpointDefinition: StratosEndpointExtensionDefin label: 'Space', labelPlural: 'Spaces', endpoint: endpointDefinition, + icon: 'virtual_space', + iconFont: 'stratos-icons' }; cfEntityCatalog.space = new StratosCatalogEntity, SpaceActionBuilders>( spaceDefinition, @@ -1210,7 +1213,7 @@ function generateCfSpaceEntity(endpointDefinition: StratosEndpointExtensionDefin createdAt: moment(space.metadata.created_at).format('LLL'), }), getLines: () => ([ - ['Creation Date', (meta) => meta.createdAt] + ['Created', (meta) => meta.createdAt] ]), getLink: metadata => `/cloud-foundry/${metadata.cfGuid}/organizations/${metadata.orgGuid}/spaces/${metadata.guid}/summary`, getGuid: metadata => metadata.guid @@ -1227,6 +1230,8 @@ function generateCfOrgEntity(endpointDefinition: StratosEndpointExtensionDefinit label: 'Organization', labelPlural: 'Organizations', endpoint: endpointDefinition, + icon: 'organization', + iconFont: 'stratos-icons' }; cfEntityCatalog.org = new StratosCatalogEntity< IOrgFavMetadata, @@ -1252,7 +1257,7 @@ function generateCfOrgEntity(endpointDefinition: StratosEndpointExtensionDefinit }), getLink: metadata => `/cloud-foundry/${metadata.cfGuid}/organizations/${metadata.guid}`, getLines: () => ([ - ['Creation Date', (meta) => meta.createdAt] + ['Created', (meta) => meta.createdAt] ]), getGuid: metadata => metadata.guid } diff --git a/src/frontend/packages/core/src/app.module.ts b/src/frontend/packages/core/src/app.module.ts index 37614cdb52..840ed60377 100644 --- a/src/frontend/packages/core/src/app.module.ts +++ b/src/frontend/packages/core/src/app.module.ts @@ -231,11 +231,12 @@ export class AppModule { } ); + // This updates the names of any recents debouncedApiRequestData$.pipe( withLatestFrom(recents$) ).subscribe( ([entities, recents]) => { - Object.values(recents.entities).forEach(recentEntity => { + Object.values(recents).forEach(recentEntity => { const mapper = this.favoritesConfigMapper.getMapperFunction(recentEntity); const entityKey = entityCatalog.getEntityKey(recentEntity); if (entities[entityKey] && entities[entityKey][recentEntity.entityId]) { @@ -243,6 +244,7 @@ export class AppModule { const entityToMetadata = this.favoritesConfigMapper.getEntityMetadata(recentEntity, entity); const name = mapper(entityToMetadata).name; if (name && name !== recentEntity.name) { + // Update the entity name this.store.dispatch(new SetRecentlyVisitedEntityAction({ ...recentEntity, name 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 index d9e2c31a66..3c28d55952 100644 --- 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 @@ -3,8 +3,11 @@ [entityConfig]="entityConfig">
-
- +
+ +
+
+ {{ icon.icon }}

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 index 7a93a9edb2..0a144cfe98 100644 --- 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 @@ -35,18 +35,21 @@ } &__header { display: flex; - white-space: nowrap; } - &__icon-panel { + &__logo-panel { display: flex; justify-content: center; width: 56px; } - &__icon { + &__logo { height: 48px; margin-right: 8px; width: auto; } + &__icon { + margin-right: 4px; + opacity: .7; + } &__panel { display: flex; :first-child { 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 index 40d15028a1..1b00ca09a0 100644 --- 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 @@ -14,6 +14,12 @@ 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', @@ -64,6 +70,9 @@ export class FavoritesMetaCardComponent { // 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) { @@ -88,6 +97,19 @@ export class FavoritesMetaCardComponent { this.iconUrl$ = observableOf(''); } + console.log(favoriteEntity); + const entityDef = entityCatalog.getEntity(this.favorite.endpointType, this.favorite.entityType); + console.log(entityDef); + + this.icon = { + hasIcon: !!entityDef.definition.logoUrl || !!entityDef.definition.icon, + icon: entityDef.definition.icon, + iconFont: entityDef.definition.iconFont, + logoUrl: entityDef.definition.logoUrl, + }; + + console.log(this.icon); + this.setConfirmation(this.prettyName, favorite); const config = cardMapper && favorite && favorite.metadata ? cardMapper(favorite.metadata) : null; diff --git a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss index 1ee29645e9..a66c052f39 100644 --- a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss +++ b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss @@ -39,9 +39,6 @@ &__menu-separator { background-color: mat-color($foreground, divider); } - &__history { - color: $subdued; - } &__underflow { background-color: $underflow-background; color: $underflow-foreground; diff --git a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.ts b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.ts index 6e836e74b3..a0ee69b500 100644 --- a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.ts +++ b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.ts @@ -2,6 +2,7 @@ import { TemplatePortal } from '@angular/cdk/portal'; import { AfterViewInit, Component, Input, OnDestroy, TemplateRef, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Store } from '@ngrx/store'; +import * as moment from 'moment'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; @@ -96,6 +97,7 @@ export class PageHeaderComponent implements OnDestroy, AfterViewInit { const { name, routerLink } = mapperFunction(favorite.metadata); this.store.dispatch(new AddRecentlyVisitedEntityAction({ guid: favorite.guid, + date: moment().valueOf(), entityType: favorite.entityType, endpointType: favorite.endpointType, entityId: favorite.entityId, 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 4aa002471d..70e96a89cc 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 @@ -1,27 +1,7 @@ + + - - -
-
-
- {{ - countedEntity.entity.name }} - -
{{ countedEntity.entity.name }}
-
-
- {{ countedEntity.subText$ | async }} -
-
-
-
-
- - - -
+
@@ -39,16 +19,23 @@
- {{ - countedEntity.entity.name }} - -
{{ countedEntity.entity.name }}
-
-
- {{ countedEntity.mostRecentHit | amTimeAgo }} +
+ {{ countedEntity.icon }}
-
- {{ countedEntity.subText$ | async }} +
+ {{ + countedEntity.entity.name }} + +
{{ countedEntity.entity.name }}
+
+ +
+ {{ countedEntity.subText$ | async }} +
diff --git a/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.scss b/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.scss index 88f583825a..eb5f3810aa 100644 --- a/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.scss +++ b/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.scss @@ -5,6 +5,7 @@ $spacing: 10px; opacity: .6; } .recent-entity { + display: flex; outline: 0; padding: $spacing; & + & { @@ -21,6 +22,16 @@ $spacing: 10px; &--name { word-break: break-all; } + &__icon { + opacity: .7; + } + &__icon-container { + display: flex; + height: 24px; + margin-right: 4px; + text-align: center; + width: 24px; + } } .clickable { 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 86af699f5c..21450712c2 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 @@ -1,34 +1,29 @@ import { Component, Input } from '@angular/core'; import { Store } from '@ngrx/store'; +import { entityCatalog } from 'frontend/packages/store/src/entity-catalog/entity-catalog'; +import { + MAX_RECENT_COUNT, +} from 'frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers'; import * as moment from 'moment'; import { Observable, of as observableOf } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { first, map } from 'rxjs/operators'; import { AppState } from '../../../../../store/src/app-state'; import { endpointEntityType } from '../../../../../store/src/helpers/stratos-entity-factory'; import { endpointEntitiesSelector } from '../../../../../store/src/selectors/endpoint.selectors'; import { recentlyVisitedSelector } from '../../../../../store/src/selectors/recently-visitied.selectors'; -import { - IEntityHit, - IRecentlyVisitedEntity, - IRecentlyVisitedState, -} from '../../../../../store/src/types/recently-visited.types'; - - -interface IRelevanceModifier { - time: number; - modifier: number; -} -interface IRelevanceModifiers { - high: IRelevanceModifier; - medium: IRelevanceModifier; - low: IRelevanceModifier; -} +import { IRecentlyVisitedEntity } from '../../../../../store/src/types/recently-visited.types'; class RenderableRecent { public mostRecentHit: moment.Moment; public subText$: Observable; + public icon: string; + public iconFont: string; constructor(readonly entity: IRecentlyVisitedEntity, private store: Store) { + const catalogEntity = entityCatalog.getEntity(entity.endpointType, entity.entityType); + this.icon = catalogEntity.definition.icon;; + this.iconFont = catalogEntity.definition.iconFont; + if (entity.entityType === endpointEntityType) { this.subText$ = observableOf(entity.prettyType); } else { @@ -44,110 +39,6 @@ class RenderableRecent { } } -class CountedRecentEntity { - public count = 0; - public mostRecentHitUnix: number; - public guid: string; - private checkAndSetDate(date: number) { - if (!this.mostRecentHitUnix || date > this.mostRecentHitUnix) { - this.mostRecentHitUnix = date; - } - } - public applyHit(hit: IEntityHit, modifier?: number) { - const amount = modifier ? 1 * modifier : 1; - this.count += amount; - this.checkAndSetDate(hit.date); - } - constructor(hit: IEntityHit) { - this.guid = hit.guid; - this.checkAndSetDate(hit.date); - } -} - -class CountedRecentEntitiesManager { - private countedRecentEntities: CountedRecentEntities = {}; - private relevanceModifiers: IRelevanceModifiers; - private renderableRecents: { - [guid: string]: RenderableRecent - }; - - constructor(recentState: IRecentlyVisitedState, private store: Store) { - const { entities, hits } = recentState; - const mostRecentTime = hits[0] ? moment(hits[0].date) : moment(); - - this.relevanceModifiers = { - high: { - time: mostRecentTime.subtract(30, 'minute').unix(), - modifier: 2 - }, - medium: { - time: mostRecentTime.subtract(1, 'day').unix(), - modifier: 1.5 - }, - low: { - time: mostRecentTime.subtract(1, 'week').unix(), - modifier: 1 - } - }; - - this.renderableRecents = Object.keys(entities).reduce((renderableRecents, recentGuid) => { - renderableRecents[recentGuid] = new RenderableRecent(entities[recentGuid], store); - return renderableRecents; - }, {}); - - this.addHits(hits); - } - - private addHits(hits: IEntityHit[]) { - hits.forEach(hit => { - this.addHit(hit); - }); - Object.keys(this.renderableRecents).forEach( - guid => { - if (this.countedRecentEntities[guid]) { - this.renderableRecents[guid].mostRecentHit = moment(this.countedRecentEntities[guid].mostRecentHitUnix); - } - } - ); - } - - private getModifier(recentEntity: IEntityHit) { - if (recentEntity.date < this.relevanceModifiers.low.time) { - return this.relevanceModifiers.low.modifier; - } - if (recentEntity.date < this.relevanceModifiers.medium.time) { - return this.relevanceModifiers.medium.modifier; - } - return this.relevanceModifiers.high.modifier; - } - - public addHit(recentEntity: IEntityHit) { - const modifier = this.getModifier(recentEntity); - if (!this.countedRecentEntities[recentEntity.guid]) { - this.countedRecentEntities[recentEntity.guid] = new CountedRecentEntity(recentEntity); - } - this.countedRecentEntities[recentEntity.guid].applyHit(recentEntity, modifier); - } - - private sort(sortKey: 'count' | 'mostRecentHitUnix' = 'count') { - const sortedHits = Object.values(this.countedRecentEntities) - .sort((countedA, countedB) => countedB[sortKey] - countedA[sortKey]) - .map(counted => counted); - return sortedHits.map(entity => this.renderableRecents[entity.guid]); - } - - public getFrecentEntities(): RenderableRecent[] { - return this.sort(); - } - - public getRecentEntities(): RenderableRecent[] { - return this.sort('mostRecentHitUnix'); - } -} -interface CountedRecentEntities { - [entityId: string]: CountedRecentEntity; -} - @Component({ selector: 'app-recent-entities', templateUrl: './recent-entities.component.html', @@ -160,22 +51,30 @@ export class RecentEntitiesComponent { @Input() mode: string; public recentEntities$: Observable; - public frecentEntities$: Observable; public hasHits$: Observable; constructor(store: Store) { const recentEntities$ = store.select(recentlyVisitedSelector); - this.hasHits$ = recentEntities$.pipe( - map(recentEntities => recentEntities && !!recentEntities.hits && recentEntities.hits.length > 0) - ); - const entitiesManager$ = recentEntities$.pipe( - filter(recentEntities => recentEntities && !!recentEntities.hits && recentEntities.hits.length > 0), - map(recentEntities => new CountedRecentEntitiesManager(recentEntities, store)), - ); - this.frecentEntities$ = entitiesManager$.pipe( - map(manager => manager.getFrecentEntities()), + + recentEntities$.pipe(first()).subscribe(e => { + console.log('recent entities'); + console.log(e); + console.log(Object.keys(e).length); + }); + + this.recentEntities$ = recentEntities$.pipe( + map(entities => Object.values(entities)), + map((entities: IRecentlyVisitedEntity[]) => { + // Sort them - most recent first + // Cap the list at the maximum we can display + const sorted = entities.sort((a, b) => b.date - a.date).slice(0, MAX_RECENT_COUNT); + return sorted.map(entity => new RenderableRecent(entity, store)); + }) ); - this.recentEntities$ = entitiesManager$.pipe( - map(manager => manager.getRecentEntities()) + + this.hasHits$ = this.recentEntities$.pipe( + map(recentEntities => recentEntities && recentEntities.length > 0) ); + } } + diff --git a/src/frontend/packages/store/src/actions/recently-visited.actions.ts b/src/frontend/packages/store/src/actions/recently-visited.actions.ts index 14dc141893..4da740f100 100644 --- a/src/frontend/packages/store/src/actions/recently-visited.actions.ts +++ b/src/frontend/packages/store/src/actions/recently-visited.actions.ts @@ -1,19 +1,11 @@ import { Action } from '@ngrx/store'; -import * as moment from 'moment'; -import { IRecentlyVisitedEntity, IRecentlyVisitedEntityDated } from '../types/recently-visited.types'; +import { IRecentlyVisitedEntity } from '../types/recently-visited.types'; export class AddRecentlyVisitedEntityAction implements Action { static ACTION_TYPE = '[Recently visited] Add'; public type = AddRecentlyVisitedEntityAction.ACTION_TYPE; - public date: number; - public recentlyVisited: IRecentlyVisitedEntityDated; - constructor(recentlyVisited: IRecentlyVisitedEntity) { - this.recentlyVisited = { - ...recentlyVisited, - date: moment().valueOf() - }; - } + constructor(public recentlyVisited: IRecentlyVisitedEntity) {} } export class SetRecentlyVisitedEntityAction implements Action { 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 d218f7ba89..21ec6ec6e0 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 @@ -51,6 +51,7 @@ export interface EntityCatalogSchemas { export interface IStratosEntityWithIcons { icon?: string; iconFont?: string; + logoUrl?: string; } export interface IEntityMetadata { diff --git a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts index 435401a404..f1e84bafca 100644 --- a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts +++ b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts @@ -1,118 +1,64 @@ import { AddRecentlyVisitedEntityAction } from '../../actions/recently-visited.actions'; -import { - IEntityHit, - IRecentlyVisitedEntities, - IRecentlyVisitedEntity, - IRecentlyVisitedEntityDated, - IRecentlyVisitedState, -} from '../../types/recently-visited.types'; +import { IRecentlyVisitedEntity, IRecentlyVisitedState } from '../../types/recently-visited.types'; -const MAX_RECENT_COUNT = 100; +// Maximum number of recent entities to show to the user +export const MAX_RECENT_COUNT = 100; -function getEntities(entities: IRecentlyVisitedEntities, newHit: IEntityHit, recentlyVisited: IRecentlyVisitedEntityDated) { - if (!entities[newHit.guid]) { - return { - ...entities, - [newHit.guid]: recentlyVisited - }; - } - return entities; -} - -function trimRecent(hits: IEntityHit[]) { - if (shouldTrim(hits)) { - return hits.slice(0, MAX_RECENT_COUNT - 1); - } - return hits; -} +// When the recent count goes above this, reduce it back down to the max +// This avoids us having to constantly trim the list once the max is hit +// We only ever show the max count number in the lists in the UI +const FLUSH_RECENT_COUNT = 150; -function shouldTrim(hits: IEntityHit[]) { - return hits.length >= MAX_RECENT_COUNT; +function recentArrayToMap(map: IRecentlyVisitedState, obj: IRecentlyVisitedEntity): IRecentlyVisitedState { + map[obj.guid] = obj; + return map; } -const endpointIdIsInList = () => { - const recentCache = {}; - return (recent: IRecentlyVisitedEntity, endpointGuids: string[]) => { - const { endpointId } = recent; - const cached = recentCache[endpointId]; - if (cached === true || cached === false) { - return cached; - } - recentCache[endpointId] = endpointGuids.includes(recent.endpointId); - return recentCache[endpointId]; - }; -}; - - -const idExistsInEntityList = (id: string, recents: IRecentlyVisitedEntities) => { - return !!recents[id]; -}; - -export const getDefaultRecentState = () => ({ - entities: {}, - hits: [] -}); +// Default recent state is an empty object map +export const getDefaultRecentState = () => ({}); -export function addNewHit(state: IRecentlyVisitedState, action: AddRecentlyVisitedEntityAction): IRecentlyVisitedState { - const entities = state.entities; - const newHit = { - guid: action.recentlyVisited.guid, - date: action.recentlyVisited.date - } as IEntityHit; - const hits = [ - newHit, - ...trimRecent(state.hits), - ]; - const newEntities = getEntities(entities, newHit, action.recentlyVisited); +// An entity has been 'hit', so update the access date or add it to the recent history +export function addRecentlyVisitedEntity(state: IRecentlyVisitedState, action: AddRecentlyVisitedEntityAction): IRecentlyVisitedState { const newState = { - entities: newEntities, - hits + ...state, + [action.recentlyVisited.guid]: action.recentlyVisited }; - return shouldTrim(state.hits) ? cleanEntities(newState) : newState; + + // Trim old data to keep the list in a manageable size + return trimRecentsList(newState); } -export function cleanRecentsList(state: IRecentlyVisitedState, endpointGuids?: string[], inclusive = false): IRecentlyVisitedState { - const isInList = endpointIdIsInList(); - if (!endpointGuids) { - return state; - } - if (!endpointGuids.length) { - return inclusive ? getDefaultRecentState() : state; +// Ensure the recents list stays at a manageable size +function trimRecentsList(state: IRecentlyVisitedState): IRecentlyVisitedState { + if (Object.keys(state).length > FLUSH_RECENT_COUNT) { + // The list size has gone over the flush count + const entities = Object.values(state); + // Cap the list at the maximum we can display + const sorted = entities.sort((a, b) => b.date - a.date).slice(0, MAX_RECENT_COUNT); + + // Turn array back into a map + return sorted.reduce(recentArrayToMap, {}); } - const entities = Object.keys(state.entities).reduce((reducedRecents, currentRecentGuid) => { - const currentRecent = state.entities[currentRecentGuid]; - const inList = isInList(currentRecent, endpointGuids); - if ( - (!inList && !inclusive) || - (inList && inclusive) - ) { - reducedRecents[currentRecentGuid] = currentRecent; - } - return reducedRecents; - }, {}); - const hits = state.hits.reduce((reducedHits, hit) => { - if (idExistsInEntityList(hit.guid, entities)) { - reducedHits.push(hit); - } - return reducedHits; - }, []); - return { - entities, - hits - }; + return state; } -export function cleanEntities(state: IRecentlyVisitedState) { - const entities = Object.keys(state.entities).reduce((reducedRecents, currentRecentGuid) => { - const currentRecent = state.entities[currentRecentGuid]; - const hasHit = state.hits.find(hit => hit.guid === currentRecentGuid); - if (hasHit) { - reducedRecents[currentRecentGuid] = currentRecent; - } - return reducedRecents; +// Update the recents list - either removing any that reference and endpoint in the list OR keeping only those +// that reference an endpoint in the list +export function cleanRecentsList(state: IRecentlyVisitedState, endpointGuids: string[], inclusive = false): IRecentlyVisitedState { + + // Turn the guids into a map, for easier lookup of testing if an endpoint shold be kept or not + const endpointMap = endpointGuids.reduce((m, obj) => { + m[obj] = true; + return m; }, {}); - return { - entities, - hits: state.hits - }; + + // Filter all of the recent entities + const filtered = Object.values(state).filter(entity => { + // Was this endpoint in the list? + const exists = endpointMap[entity.endpointId]; + return exists ? inclusive : !inclusive; + }); + + // Convert the array back into a map + return filtered.reduce(recentArrayToMap, {}); } diff --git a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts index ead7fd092e..79fd5eaee4 100644 --- a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts +++ b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts @@ -11,7 +11,7 @@ import { AddRecentlyVisitedEntityAction, SetRecentlyVisitedEntityAction } from ' import { entityCatalog } from '../../entity-catalog/entity-catalog'; import { endpointEntityType, STRATOS_ENDPOINT_TYPE } from '../../helpers/stratos-entity-factory'; import { IRecentlyVisitedState } from '../../types/recently-visited.types'; -import { addNewHit, cleanRecentsList, getDefaultRecentState } from './recently-visited.reducer.helpers'; +import { addRecentlyVisitedEntity, cleanRecentsList, getDefaultRecentState } from './recently-visited.reducer.helpers'; export function recentlyVisitedReducer( state: IRecentlyVisitedState = getDefaultRecentState(), @@ -19,16 +19,14 @@ export function recentlyVisitedReducer( ): IRecentlyVisitedState { switch (action.type) { case AddRecentlyVisitedEntityAction.ACTION_TYPE: - return addNewHit(state, action as AddRecentlyVisitedEntityAction); + return addRecentlyVisitedEntity(state, action as AddRecentlyVisitedEntityAction); case SetRecentlyVisitedEntityAction.ACTION_TYPE: const setAction = action as SetRecentlyVisitedEntityAction; - return { - hits: state.hits, - entities: { - ...state.entities, - [setAction.recentlyVisited.guid]: setAction.recentlyVisited - } + const newState = { + ...state, + [setAction.recentlyVisited.guid]: setAction.recentlyVisited }; + return newState; case DISCONNECT_ENDPOINTS_SUCCESS: case UNREGISTER_ENDPOINTS_SUCCESS: const removeEndpointAction = action as DisconnectEndpoint; diff --git a/src/frontend/packages/store/src/types/recently-visited.types.ts b/src/frontend/packages/store/src/types/recently-visited.types.ts index d9f8e12b56..95e581663c 100644 --- a/src/frontend/packages/store/src/types/recently-visited.types.ts +++ b/src/frontend/packages/store/src/types/recently-visited.types.ts @@ -1,12 +1,11 @@ import { IFavoriteTypeInfo } from './user-favorites.types'; -export interface IEntityHit { - guid: string; - date: number; -} +// Types used for maintaining the list of recent entities visited by the user + export interface IRecentlyVisitedEntity extends IFavoriteTypeInfo { guid: string; name: string; + date: number; entityId: string; prettyType: string; prettyEndpointType: string; @@ -14,14 +13,11 @@ export interface IRecentlyVisitedEntity extends IFavoriteTypeInfo { routerLink?: string; } -export interface IRecentlyVisitedEntities { - [guid: string]: IRecentlyVisitedEntity; -} -export interface IRecentlyVisitedState { - entities: IRecentlyVisitedEntities; - hits: IEntityHit[]; +export interface IRecentlyVisitedEntityWithIcon extends IRecentlyVisitedEntity { + icon: string; + iconFont: string; } -export interface IRecentlyVisitedEntityDated extends IRecentlyVisitedEntity { - date: number; +export interface IRecentlyVisitedState { + [guid: string]: IRecentlyVisitedEntity; } From c2e713d3ffb710bdb119f12f9c904cfe12b10e6e Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Sun, 5 Jul 2020 17:34:43 +0100 Subject: [PATCH 02/11] Remove debug logging --- .../favorites-meta-card/favorites-meta-card.component.ts | 5 ----- 1 file changed, 5 deletions(-) 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 index 1b00ca09a0..16797fb932 100644 --- 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 @@ -97,10 +97,7 @@ export class FavoritesMetaCardComponent { this.iconUrl$ = observableOf(''); } - console.log(favoriteEntity); const entityDef = entityCatalog.getEntity(this.favorite.endpointType, this.favorite.entityType); - console.log(entityDef); - this.icon = { hasIcon: !!entityDef.definition.logoUrl || !!entityDef.definition.icon, icon: entityDef.definition.icon, @@ -108,8 +105,6 @@ export class FavoritesMetaCardComponent { logoUrl: entityDef.definition.logoUrl, }; - console.log(this.icon); - this.setConfirmation(this.prettyName, favorite); const config = cardMapper && favorite && favorite.metadata ? cardMapper(favorite.metadata) : null; From 9581421cd26b988ec7b55cefaad22a969b65e5ba Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Mon, 6 Jul 2020 09:49:50 +0100 Subject: [PATCH 03/11] Remove debug logging/subscription leak --- .../recent-entities/recent-entities.component.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 21450712c2..a44f13294b 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 @@ -6,7 +6,7 @@ import { } from 'frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers'; import * as moment from 'moment'; import { Observable, of as observableOf } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { AppState } from '../../../../../store/src/app-state'; import { endpointEntityType } from '../../../../../store/src/helpers/stratos-entity-factory'; @@ -54,13 +54,6 @@ export class RecentEntitiesComponent { public hasHits$: Observable; constructor(store: Store) { const recentEntities$ = store.select(recentlyVisitedSelector); - - recentEntities$.pipe(first()).subscribe(e => { - console.log('recent entities'); - console.log(e); - console.log(Object.keys(e).length); - }); - this.recentEntities$ = recentEntities$.pipe( map(entities => Object.values(entities)), map((entities: IRecentlyVisitedEntity[]) => { From b142e2253a91589eb6127e23295c7d23042c3bc7 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Mon, 6 Jul 2020 09:55:29 +0100 Subject: [PATCH 04/11] Unit test fix --- src/frontend/packages/store/testing/src/store-test-helper.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 98731e61fe..327474a530 100644 --- a/src/frontend/packages/store/testing/src/store-test-helper.ts +++ b/src/frontend/packages/store/testing/src/store-test-helper.ts @@ -123,10 +123,7 @@ export const testSessionData: SessionData = { function getDefaultInitialTestStratosStoreState() { return { - recentlyVisited: { - entities: {}, - hits: [] - }, + recentlyVisited: {}, userFavoritesGroups: { busy: false, error: false, From 17b026def749f370f621e3472a60dd2c987c339d Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 7 Jul 2020 16:07:01 +0100 Subject: [PATCH 05/11] Remove recents and favorites when entities are deleted --- .../src/actions/entity.delete.actions.ts | 33 +++++++++++++++++ .../packages/store/src/effects/api.effects.ts | 36 +++++++++++++++++-- .../src/effects/user-favorites-effect.ts | 1 - .../recently-visited.reducer.helpers.ts | 9 +++++ .../recently-visited.reducer.ts | 10 +++++- 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/frontend/packages/store/src/actions/entity.delete.actions.ts diff --git a/src/frontend/packages/store/src/actions/entity.delete.actions.ts b/src/frontend/packages/store/src/actions/entity.delete.actions.ts new file mode 100644 index 0000000000..4fe69e1825 --- /dev/null +++ b/src/frontend/packages/store/src/actions/entity.delete.actions.ts @@ -0,0 +1,33 @@ +import { Action } from '@ngrx/store'; + +import { IFavoriteMetadata, UserFavorite } from '../types/user-favorites.types'; + + +export class EntityDeleteCompleteAction implements Action { + + public static ACTION_TYPE = '[Entity] Entity delete complete'; + public type = EntityDeleteCompleteAction.ACTION_TYPE; + public entityGuid: string; + public entityType: string; + public endpointType: string; + public endpointGuid: string; + public apiAction; + + // Try and parse an action to see if it contains all of the entity properties we expect + public static parse(action: any): EntityDeleteCompleteAction { + const apiAction = action.apiAction ? action.apiAction : action; + if (apiAction.guid && apiAction.entityType && apiAction.endpointType && apiAction.endpointGuid) { + const entityDeleteAction = new EntityDeleteCompleteAction(); + entityDeleteAction.entityGuid = apiAction.guid; + entityDeleteAction.entityType = apiAction.entityType; + entityDeleteAction.endpointGuid = apiAction.endpointGuid; + entityDeleteAction.endpointType = apiAction.endpointType; + return entityDeleteAction; + } + return null; + } + + public asFavorite(): UserFavorite { + return new UserFavorite(this.endpointGuid, this.endpointType, this.entityType, this.entityGuid); + } +} diff --git a/src/frontend/packages/store/src/effects/api.effects.ts b/src/frontend/packages/store/src/effects/api.effects.ts index 493fe31634..3785cc7bd4 100644 --- a/src/frontend/packages/store/src/effects/api.effects.ts +++ b/src/frontend/packages/store/src/effects/api.effects.ts @@ -3,14 +3,17 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { mergeMap, withLatestFrom } from 'rxjs/operators'; +import { RemoveUserFavoriteAction } from '../actions/user-favourites.actions'; import { baseRequestPipelineFactory } from '../entity-request-pipeline/base-single-entity-request.pipeline'; import { basePaginatedRequestPipeline } from '../entity-request-pipeline/entity-pagination-request-pipeline'; import { apiRequestPipelineFactory } from '../entity-request-pipeline/entity-request-pipeline'; import { PipelineHttpClient } from '../entity-request-pipeline/pipline-http-client.service'; import { PaginatedAction } from '../types/pagination.types'; -import { ICFAction } from '../types/request.types'; -import { ApiActionTypes } from './../actions/request.actions'; +import { ICFAction, WrapperRequestActionSuccess } from '../types/request.types'; +import { UserFavorite } from '../types/user-favorites.types'; +import { ApiActionTypes, RequestTypes } from './../actions/request.actions'; import { InternalAppState } from './../app-state'; +import { EntityDeleteCompleteAction } from './user-favorites-effect'; @Injectable() export class APIEffect { @@ -44,4 +47,33 @@ export class APIEffect { }), ); + + // Whenever we spot a delete success operation, look to see if the action + // fulfils the entity delete requirements and dispatch an entity delete action if it does + // Dispatch an action to remove the favorite + @Effect() + apiDeleteRequest$ = this.actions$.pipe( + ofType(RequestTypes.SUCCESS), + withLatestFrom(this.store), + mergeMap(([action, appState]) => { + if (action.requestType === 'delete') { + const deleteAction = EntityDeleteCompleteAction.parse(action); + if (deleteAction) { + this.store.dispatch(deleteAction); + + // Delete the favorite if there is one + const favorite = new UserFavorite( + deleteAction.endpointGuid, + deleteAction.endpointType, + deleteAction.entityType, + deleteAction.entityGuid + ); + const rem = new RemoveUserFavoriteAction(favorite); + this.store.dispatch(rem); + } + } + return []; + }) + ); + } diff --git a/src/frontend/packages/store/src/effects/user-favorites-effect.ts b/src/frontend/packages/store/src/effects/user-favorites-effect.ts index 02240e52af..946e4ea948 100644 --- a/src/frontend/packages/store/src/effects/user-favorites-effect.ts +++ b/src/frontend/packages/store/src/effects/user-favorites-effect.ts @@ -27,7 +27,6 @@ import { UserFavoriteManager } from '../user-favorite-manager'; const favoriteUrlPath = `/pp/${proxyAPIVersion}/favorites`; - @Injectable() export class UserFavoritesEffect { diff --git a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts index f1e84bafca..c42a033fa7 100644 --- a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts +++ b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts @@ -1,5 +1,6 @@ import { AddRecentlyVisitedEntityAction } from '../../actions/recently-visited.actions'; import { IRecentlyVisitedEntity, IRecentlyVisitedState } from '../../types/recently-visited.types'; +import { EntityDeleteCompleteAction } from './../../effects/user-favorites-effect'; // Maximum number of recent entities to show to the user export const MAX_RECENT_COUNT = 100; @@ -62,3 +63,11 @@ export function cleanRecentsList(state: IRecentlyVisitedState, endpointGuids: st // Convert the array back into a map return filtered.reduce(recentArrayToMap, {}); } + +export function clearEntityFromRecentsList(state: IRecentlyVisitedState, action: EntityDeleteCompleteAction): IRecentlyVisitedState { + // Remove entity from the map if it exists + const fav = action.asFavorite(); + const newState = { ...state }; + delete newState[fav.guid]; + return newState; +} \ No newline at end of file diff --git a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts index 79fd5eaee4..19e21f17d9 100644 --- a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts +++ b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts @@ -7,17 +7,25 @@ import { GetAllEndpointsSuccess, UNREGISTER_ENDPOINTS_SUCCESS, } from '../../actions/endpoint.actions'; +import { EntityDeleteCompleteAction } from '../../actions/entity.delete.actions'; import { AddRecentlyVisitedEntityAction, SetRecentlyVisitedEntityAction } from '../../actions/recently-visited.actions'; import { entityCatalog } from '../../entity-catalog/entity-catalog'; import { endpointEntityType, STRATOS_ENDPOINT_TYPE } from '../../helpers/stratos-entity-factory'; import { IRecentlyVisitedState } from '../../types/recently-visited.types'; -import { addRecentlyVisitedEntity, cleanRecentsList, getDefaultRecentState } from './recently-visited.reducer.helpers'; +import { + addRecentlyVisitedEntity, + cleanRecentsList, + clearEntityFromRecentsList, + getDefaultRecentState, +} from './recently-visited.reducer.helpers'; export function recentlyVisitedReducer( state: IRecentlyVisitedState = getDefaultRecentState(), action: Action ): IRecentlyVisitedState { switch (action.type) { + case EntityDeleteCompleteAction.ACTION_TYPE: + return clearEntityFromRecentsList(state, action as EntityDeleteCompleteAction); case AddRecentlyVisitedEntityAction.ACTION_TYPE: return addRecentlyVisitedEntity(state, action as AddRecentlyVisitedEntityAction); case SetRecentlyVisitedEntityAction.ACTION_TYPE: From 754b6002d17357109b4c8ff276f4a4db0b9c177b Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Thu, 9 Jul 2020 15:01:37 +0100 Subject: [PATCH 06/11] Fix merge issues --- src/frontend/packages/store/src/effects/api.effects.ts | 2 +- .../recently-visited.reducer.helpers.ts | 2 +- .../current-user-roles-reducer/recently-visited.reducer.ts | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/frontend/packages/store/src/effects/api.effects.ts b/src/frontend/packages/store/src/effects/api.effects.ts index 3785cc7bd4..f2cfb6c6d4 100644 --- a/src/frontend/packages/store/src/effects/api.effects.ts +++ b/src/frontend/packages/store/src/effects/api.effects.ts @@ -3,6 +3,7 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { mergeMap, withLatestFrom } from 'rxjs/operators'; +import { EntityDeleteCompleteAction } from '../actions/entity.delete.actions'; import { RemoveUserFavoriteAction } from '../actions/user-favourites.actions'; import { baseRequestPipelineFactory } from '../entity-request-pipeline/base-single-entity-request.pipeline'; import { basePaginatedRequestPipeline } from '../entity-request-pipeline/entity-pagination-request-pipeline'; @@ -13,7 +14,6 @@ import { ICFAction, WrapperRequestActionSuccess } from '../types/request.types'; import { UserFavorite } from '../types/user-favorites.types'; import { ApiActionTypes, RequestTypes } from './../actions/request.actions'; import { InternalAppState } from './../app-state'; -import { EntityDeleteCompleteAction } from './user-favorites-effect'; @Injectable() export class APIEffect { diff --git a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts index c42a033fa7..f63f5a5781 100644 --- a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts +++ b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts @@ -1,6 +1,6 @@ +import { EntityDeleteCompleteAction } from '../../actions/entity.delete.actions'; import { AddRecentlyVisitedEntityAction } from '../../actions/recently-visited.actions'; import { IRecentlyVisitedEntity, IRecentlyVisitedState } from '../../types/recently-visited.types'; -import { EntityDeleteCompleteAction } from './../../effects/user-favorites-effect'; // Maximum number of recent entities to show to the user export const MAX_RECENT_COUNT = 100; diff --git a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts index 8c03edec5c..19e21f17d9 100644 --- a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts +++ b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts @@ -12,7 +12,12 @@ import { AddRecentlyVisitedEntityAction, SetRecentlyVisitedEntityAction } from ' import { entityCatalog } from '../../entity-catalog/entity-catalog'; import { endpointEntityType, STRATOS_ENDPOINT_TYPE } from '../../helpers/stratos-entity-factory'; import { IRecentlyVisitedState } from '../../types/recently-visited.types'; -import { addRecentlyVisitedEntity, cleanRecentsList, getDefaultRecentState } from './recently-visited.reducer.helpers'; +import { + addRecentlyVisitedEntity, + cleanRecentsList, + clearEntityFromRecentsList, + getDefaultRecentState, +} from './recently-visited.reducer.helpers'; export function recentlyVisitedReducer( state: IRecentlyVisitedState = getDefaultRecentState(), From 7260a94d18a90bf29d0322b4fc801f24ee440bd9 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Thu, 9 Jul 2020 15:32:11 +0100 Subject: [PATCH 07/11] Fix merge issue --- .../recent-entities/recent-entities.component.scss | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.scss b/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.scss index d28a147a8a..dd660731ee 100644 --- a/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.scss +++ b/src/frontend/packages/core/src/shared/components/recent-entities/recent-entities.component.scss @@ -25,16 +25,6 @@ $spacing: 10px; &__icon { opacity: .7; } - &__icon-container { - display: flex; - height: 24px; - margin-right: 4px; - text-align: center; - width: 24px; - } - &__icon { - opacity: .7; - } &__icon-container { display: flex; height: 24px; From c98b6b6ae354aa8aa475d9e86b9c3dfb9817dc38 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Fri, 10 Jul 2020 15:44:04 +0100 Subject: [PATCH 08/11] Tidy up and improve following PR feedback --- .../src/actions/entity.delete.actions.ts | 30 +++++++++---------- .../packages/store/src/effects/api.effects.ts | 20 ++----------- .../src/effects/user-favorites-effect.ts | 12 ++++++++ 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/frontend/packages/store/src/actions/entity.delete.actions.ts b/src/frontend/packages/store/src/actions/entity.delete.actions.ts index 4fe69e1825..0e023ff567 100644 --- a/src/frontend/packages/store/src/actions/entity.delete.actions.ts +++ b/src/frontend/packages/store/src/actions/entity.delete.actions.ts @@ -1,28 +1,25 @@ import { Action } from '@ngrx/store'; +import { EntityRequestAction } from '../types/request.types'; import { IFavoriteMetadata, UserFavorite } from '../types/user-favorites.types'; - export class EntityDeleteCompleteAction implements Action { public static ACTION_TYPE = '[Entity] Entity delete complete'; public type = EntityDeleteCompleteAction.ACTION_TYPE; - public entityGuid: string; - public entityType: string; - public endpointType: string; - public endpointGuid: string; - public apiAction; - // Try and parse an action to see if it contains all of the entity properties we expect - public static parse(action: any): EntityDeleteCompleteAction { - const apiAction = action.apiAction ? action.apiAction : action; - if (apiAction.guid && apiAction.entityType && apiAction.endpointType && apiAction.endpointGuid) { - const entityDeleteAction = new EntityDeleteCompleteAction(); - entityDeleteAction.entityGuid = apiAction.guid; - entityDeleteAction.entityType = apiAction.entityType; - entityDeleteAction.endpointGuid = apiAction.endpointGuid; - entityDeleteAction.endpointType = apiAction.endpointType; - return entityDeleteAction; + constructor( + public entityGuid: string, + public entityType: string, + public endpointType: string, + public endpointGuid: string, + public action: EntityRequestAction, + ) {} + + // Create an entity delete action if we have all of the properties we need + public static parse(action: EntityRequestAction): EntityDeleteCompleteAction { + if (action.guid && action.entityType && action.endpointType && action.endpointGuid) { + return new EntityDeleteCompleteAction(action.guid, action.entityType, action.endpointGuid, action.endpointType, action); } return null; } @@ -30,4 +27,5 @@ export class EntityDeleteCompleteAction implements Action { public asFavorite(): UserFavorite { return new UserFavorite(this.endpointGuid, this.endpointType, this.entityType, this.entityGuid); } + } diff --git a/src/frontend/packages/store/src/effects/api.effects.ts b/src/frontend/packages/store/src/effects/api.effects.ts index f2cfb6c6d4..db57321b33 100644 --- a/src/frontend/packages/store/src/effects/api.effects.ts +++ b/src/frontend/packages/store/src/effects/api.effects.ts @@ -4,14 +4,12 @@ import { Store } from '@ngrx/store'; import { mergeMap, withLatestFrom } from 'rxjs/operators'; import { EntityDeleteCompleteAction } from '../actions/entity.delete.actions'; -import { RemoveUserFavoriteAction } from '../actions/user-favourites.actions'; import { baseRequestPipelineFactory } from '../entity-request-pipeline/base-single-entity-request.pipeline'; import { basePaginatedRequestPipeline } from '../entity-request-pipeline/entity-pagination-request-pipeline'; import { apiRequestPipelineFactory } from '../entity-request-pipeline/entity-request-pipeline'; import { PipelineHttpClient } from '../entity-request-pipeline/pipline-http-client.service'; import { PaginatedAction } from '../types/pagination.types'; import { ICFAction, WrapperRequestActionSuccess } from '../types/request.types'; -import { UserFavorite } from '../types/user-favorites.types'; import { ApiActionTypes, RequestTypes } from './../actions/request.actions'; import { InternalAppState } from './../app-state'; @@ -21,9 +19,7 @@ export class APIEffect { private actions$: Actions, private store: Store, private httpClient: PipelineHttpClient - ) { - - } + ) { } @Effect() apiRequest$ = this.actions$.pipe( @@ -47,7 +43,6 @@ export class APIEffect { }), ); - // Whenever we spot a delete success operation, look to see if the action // fulfils the entity delete requirements and dispatch an entity delete action if it does // Dispatch an action to remove the favorite @@ -57,19 +52,10 @@ export class APIEffect { withLatestFrom(this.store), mergeMap(([action, appState]) => { if (action.requestType === 'delete') { - const deleteAction = EntityDeleteCompleteAction.parse(action); + const deleteAction = EntityDeleteCompleteAction.parse(action.apiAction); if (deleteAction) { + // Dispatch a delete action for the entity this.store.dispatch(deleteAction); - - // Delete the favorite if there is one - const favorite = new UserFavorite( - deleteAction.endpointGuid, - deleteAction.endpointType, - deleteAction.entityType, - deleteAction.entityGuid - ); - const rem = new RemoveUserFavoriteAction(favorite); - this.store.dispatch(rem); } } return []; diff --git a/src/frontend/packages/store/src/effects/user-favorites-effect.ts b/src/frontend/packages/store/src/effects/user-favorites-effect.ts index 946e4ea948..0fbe4a090f 100644 --- a/src/frontend/packages/store/src/effects/user-favorites-effect.ts +++ b/src/frontend/packages/store/src/effects/user-favorites-effect.ts @@ -4,6 +4,7 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { catchError, first, mergeMap, switchMap } from 'rxjs/operators'; +import { EntityDeleteCompleteAction } from '../actions/entity.delete.actions'; import { ClearPaginationOfEntity } from '../actions/pagination.actions'; import { GetUserFavoritesAction, @@ -143,4 +144,15 @@ export class UserFavoritesEffect { ); }) ); + + @Effect() + entityDeleteRequest$ = this.actions$.pipe( + ofType(EntityDeleteCompleteAction.ACTION_TYPE), + mergeMap((action: EntityDeleteCompleteAction) => { + // Delete the favorite if there is one + this.store.dispatch(new RemoveUserFavoriteAction(action.asFavorite())); + return []; + }) + ); + } From d605ac70c6f8ca575023629076390712de1839a8 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Fri, 10 Jul 2020 17:13:42 +0100 Subject: [PATCH 09/11] Remove out of date comment --- src/frontend/packages/store/src/effects/api.effects.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/packages/store/src/effects/api.effects.ts b/src/frontend/packages/store/src/effects/api.effects.ts index db57321b33..34ee572143 100644 --- a/src/frontend/packages/store/src/effects/api.effects.ts +++ b/src/frontend/packages/store/src/effects/api.effects.ts @@ -45,7 +45,6 @@ export class APIEffect { // Whenever we spot a delete success operation, look to see if the action // fulfils the entity delete requirements and dispatch an entity delete action if it does - // Dispatch an action to remove the favorite @Effect() apiDeleteRequest$ = this.actions$.pipe( ofType(RequestTypes.SUCCESS), From 98e887f6d259f1a58b76d018d8faffec52742553 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Thu, 23 Jul 2020 21:56:32 +0100 Subject: [PATCH 10/11] Fixed problem where clear was not working --- .../packages/store/src/actions/entity.delete.actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/packages/store/src/actions/entity.delete.actions.ts b/src/frontend/packages/store/src/actions/entity.delete.actions.ts index 0e023ff567..2b69ee601b 100644 --- a/src/frontend/packages/store/src/actions/entity.delete.actions.ts +++ b/src/frontend/packages/store/src/actions/entity.delete.actions.ts @@ -11,8 +11,8 @@ export class EntityDeleteCompleteAction implements Action { constructor( public entityGuid: string, public entityType: string, - public endpointType: string, public endpointGuid: string, + public endpointType: string, public action: EntityRequestAction, ) {} From 076ae8aba74679ccce4d0ba9bace319165296b84 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Fri, 24 Jul 2020 09:54:47 +0100 Subject: [PATCH 11/11] Check favorite exists before deleting --- .../store/src/effects/user-favorites-effect.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/frontend/packages/store/src/effects/user-favorites-effect.ts b/src/frontend/packages/store/src/effects/user-favorites-effect.ts index 0fbe4a090f..76e31662f1 100644 --- a/src/frontend/packages/store/src/effects/user-favorites-effect.ts +++ b/src/frontend/packages/store/src/effects/user-favorites-effect.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; -import { catchError, first, mergeMap, switchMap } from 'rxjs/operators'; +import { catchError, first, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators'; import { EntityDeleteCompleteAction } from '../actions/entity.delete.actions'; import { ClearPaginationOfEntity } from '../actions/pagination.actions'; @@ -18,13 +18,14 @@ import { UpdateUserFavoriteMetadataAction, UpdateUserFavoriteMetadataSuccessAction, } from '../actions/user-favourites.actions'; -import { DispatchOnlyAppState } from '../app-state'; +import { InternalAppState } from '../app-state'; import { entityCatalog } from '../entity-catalog/entity-catalog'; import { proxyAPIVersion } from '../jetstream'; import { NormalizedResponse } from '../types/api.types'; import { StartRequestAction, WrapperRequestActionFailed, WrapperRequestActionSuccess } from '../types/request.types'; import { IFavoriteMetadata, UserFavorite, userFavoritesPaginationKey } from '../types/user-favorites.types'; import { UserFavoriteManager } from '../user-favorite-manager'; +import { STRATOS_ENDPOINT_TYPE, userFavouritesEntityType } from './../helpers/stratos-entity-factory'; const favoriteUrlPath = `/pp/${proxyAPIVersion}/favorites`; @@ -34,7 +35,7 @@ export class UserFavoritesEffect { constructor( private http: HttpClient, private actions$: Actions, - private store: Store, + private store: Store, private userFavoriteManager: UserFavoriteManager ) { } @@ -148,9 +149,14 @@ export class UserFavoritesEffect { @Effect() entityDeleteRequest$ = this.actions$.pipe( ofType(EntityDeleteCompleteAction.ACTION_TYPE), - mergeMap((action: EntityDeleteCompleteAction) => { - // Delete the favorite if there is one - this.store.dispatch(new RemoveUserFavoriteAction(action.asFavorite())); + withLatestFrom(this.store), + mergeMap(([action, appState]) => { + // If there is a favorite, delete it + const fav = action.asFavorite(); + const entityKey = entityCatalog.getEntityKey(STRATOS_ENDPOINT_TYPE, userFavouritesEntityType); + if (appState.requestData && appState.requestData[entityKey] && appState.requestData[entityKey][fav.guid]) { + this.store.dispatch(new RemoveUserFavoriteAction(fav)); + } return []; }) );