From b078bc6d510148d6979b9d2eeacd426f330d192e Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 26 Jun 2020 17:17:59 +0100 Subject: [PATCH 01/23] WIP --- .../store/src/actions/list.actions.ts | 10 +- .../store/src/actions/pagination.actions.ts | 8 +- src/frontend/packages/store/src/app-state.ts | 1 + .../store/src/effects/auth.effects.ts | 21 +--- .../src/helpers/local-storage-service.ts | 98 +++++++++++++++++++ .../store/src/helpers/store-helpers.ts | 1 + .../packages/store/src/reducers.module.ts | 29 ++++-- .../store/src/reducers/list.reducer.ts | 9 +- .../pagination-reducer/pagination.reducer.ts | 42 +++++++- 9 files changed, 189 insertions(+), 30 deletions(-) create mode 100644 src/frontend/packages/store/src/helpers/local-storage-service.ts diff --git a/src/frontend/packages/store/src/actions/list.actions.ts b/src/frontend/packages/store/src/actions/list.actions.ts index 8827fc0f36..0d3dc73e50 100644 --- a/src/frontend/packages/store/src/actions/list.actions.ts +++ b/src/frontend/packages/store/src/actions/list.actions.ts @@ -1,6 +1,7 @@ import { SortDirection } from '@angular/material/sort'; import { Action } from '@ngrx/store'; +import { ListsState } from '../reducers/list.reducer'; import { defaultClientPaginationPageSize } from '../reducers/pagination-reducer/pagination-reducer-reset-pagination'; @@ -25,7 +26,8 @@ export class ListFilter { export const ListStateActionTypes = { SET: '[List] Set', - SET_VIEW: '[List] Set View' + SET_VIEW: '[List] Set View', + HYDRATE: '[List] Hydrate' }; export type ListView = 'table' | 'cards'; @@ -39,3 +41,9 @@ export class SetListViewAction implements Action { constructor(public key: string, public view: ListView) { } type = ListStateActionTypes.SET_VIEW; } + + +export class HydrateListsStateAction implements Action { + constructor(public listsState: ListsState) { } + type = ListStateActionTypes.HYDRATE; +} \ No newline at end of file diff --git a/src/frontend/packages/store/src/actions/pagination.actions.ts b/src/frontend/packages/store/src/actions/pagination.actions.ts index b36603d1e6..492399bfca 100644 --- a/src/frontend/packages/store/src/actions/pagination.actions.ts +++ b/src/frontend/packages/store/src/actions/pagination.actions.ts @@ -1,7 +1,7 @@ import { Action } from '@ngrx/store'; import { EntityCatalogEntityConfig, extractEntityCatalogEntityConfig } from '../entity-catalog/entity-catalog.types'; -import { PaginationClientFilter, PaginationParam } from '../types/pagination.types'; +import { PaginationClientFilter, PaginationParam, PaginationState } from '../types/pagination.types'; export const CLEAR_PAGINATION_OF_TYPE = '[Pagination] Clear all pages of type'; export const CLEAR_PAGINATION_OF_ENTITY = '[Pagination] Clear pagination of entity'; @@ -22,6 +22,7 @@ export const SET_PAGE_BUSY = '[Pagination] Set Page Busy'; export const REMOVE_ID_FROM_PAGINATION = '[Pagination] Remove id from pagination'; export const UPDATE_MAXED_STATE = '[Pagination] Update maxed state'; export const IGNORE_MAXED_STATE = '[Pagination] Ignore maxed state'; +export const HYDRATE_PAGINATION_STATE = '[Pagination] Hydrate pagination state'; export function getPaginationKey(type: string, id: string, endpointGuid?: string) { const key = `${type}-${id}`; @@ -218,3 +219,8 @@ export class IgnorePaginationMaxedState implements Action, EntityCatalogEntityCo public forcedEntityKey?: string ) { } } + +export class HydratePaginationStateAction implements Action { + constructor(public paginationState: PaginationState) { } + type = HYDRATE_PAGINATION_STATE; +} diff --git a/src/frontend/packages/store/src/app-state.ts b/src/frontend/packages/store/src/app-state.ts index 6974c9c510..e2c4f173ba 100644 --- a/src/frontend/packages/store/src/app-state.ts +++ b/src/frontend/packages/store/src/app-state.ts @@ -23,6 +23,7 @@ export interface IRequestEntityTypeState { export type BaseRequestState = Record>; export type BaseRequestDataState = Record>; + export abstract class AppState< T extends Record = any > { diff --git a/src/frontend/packages/store/src/effects/auth.effects.ts b/src/frontend/packages/store/src/effects/auth.effects.ts index 70f117622a..ab2c08360f 100644 --- a/src/frontend/packages/store/src/effects/auth.effects.ts +++ b/src/frontend/packages/store/src/effects/auth.effects.ts @@ -23,11 +23,10 @@ import { VERIFY_SESSION, VerifySession, } from '../actions/auth.actions'; -import { HydrateDashboardStateAction } from '../actions/dashboard-actions'; import { GET_ENDPOINTS_SUCCESS, GetAllEndpointsSuccess } from '../actions/endpoint.actions'; import { DispatchOnlyAppState } from '../app-state'; import { BrowserStandardEncoder } from '../browser-encoder'; -import { getDashboardStateSessionId } from '../helpers/store-helpers'; +import { LocalStorageService } from '../helpers/local-storage-service'; import { stratosEntityCatalog } from '../stratos-entity-catalog'; import { SessionData } from '../types/auth.types'; @@ -81,7 +80,7 @@ export class AuthEffect { mergeMap(response => { const sessionData = response.body; sessionData.sessionExpiresOn = parseInt(response.headers.get('x-cap-session-expires-on'), 10) * 1000; - this.rehydrateDashboardState(this.store, sessionData); + LocalStorageService.storageToStore(this.store, sessionData) return [ stratosEntityCatalog.systemInfo.actions.getSystemInfo(true), new VerifiedSession(sessionData, action.updateEndpoints) @@ -155,19 +154,5 @@ export class AuthEffect { return false; } - private rehydrateDashboardState(store: Store, sessionData: SessionData) { - const storage = localStorage || window.localStorage; - // We use the username to key the session storage. We could replace this with the users id? - if (storage && sessionData.user) { - const sessionId = getDashboardStateSessionId(sessionData.user.name); - if (sessionId) { - try { - const dashboardData = JSON.parse(storage.getItem(sessionId)); - store.dispatch(new HydrateDashboardStateAction(dashboardData)); - } catch (e) { - console.warn('Failed to parse user settings from session storage, consider clearing them manually', e); - } - } - } - } + } diff --git a/src/frontend/packages/store/src/helpers/local-storage-service.ts b/src/frontend/packages/store/src/helpers/local-storage-service.ts new file mode 100644 index 0000000000..40418b40ba --- /dev/null +++ b/src/frontend/packages/store/src/helpers/local-storage-service.ts @@ -0,0 +1,98 @@ +import { Store } from '@ngrx/store'; + +import { HydrateDashboardStateAction } from '../actions/dashboard-actions'; +import { HydrateListsStateAction } from '../actions/list.actions'; +import { HydratePaginationStateAction } from '../actions/pagination.actions'; +import { DispatchOnlyAppState } from '../app-state'; +import { SessionData } from '../types/auth.types'; +import { PaginationState } from '../types/pagination.types'; +import { getDashboardStateSessionId } from './store-helpers'; + +export enum LocalStorageSyncTypes { + DASHBOARD = 'dashboard', + PAGINATION = 'pagination', + LISTS = 'lists', +} +export class LocalStorageService { + + // TODO: RC applying filters that aren't in lists + // TODO: RC applying cf filter works, but not org/space + // TODO: RC cleaning sessions storage (entities that don't exist, etc, can be done by storeagesync??) + // TODO: RC backward compatible (load 'user-dashboard' into 'user') + // TODO: RC todos! + /** + * Allow for selective persistence of data + */ + public static parseForStorage(storePart: T, type: LocalStorageSyncTypes): Object { + // TODO: RC Tidy + switch (type) { + case LocalStorageSyncTypes.PAGINATION: + const pagination: PaginationState = storePart as unknown as PaginationState; + const abs = Object.keys(pagination).reduce((res, entityTypes) => { + const perEntity = Object.keys(pagination[entityTypes]).reduce((res2, paginationKeysOfEntityType) => { + const paginationSection = pagination[entityTypes][paginationKeysOfEntityType] + res2[paginationKeysOfEntityType] = { + params: paginationSection.params, + clientPagination: paginationSection.clientPagination + } + return res2; + }, {}); + if (Object.keys(perEntity).length > 0) { + res[entityTypes] = perEntity; + } + return res; + }, {}); + return abs; + } + return storePart; + } + public static storageToStore(store: Store, sessionData: SessionData) { + return LocalStorageService.rehydrateDashboardState(store, sessionData); + } + + private static rehydrateDashboardState(store: Store, sessionData: SessionData) { + const storage = localStorage || window.localStorage; + // We use the username to key the session storage. We could replace this with the users id? + if (storage && sessionData.user) { + const sessionId = getDashboardStateSessionId(sessionData.user.name); + if (sessionId) { + LocalStorageService.handleHydrate( + LocalStorageSyncTypes.DASHBOARD, + data => store.dispatch(new HydrateDashboardStateAction(data)), + storage, + sessionId + ) + LocalStorageService.handleHydrate( + LocalStorageSyncTypes.PAGINATION, + data => store.dispatch(new HydratePaginationStateAction(data)), + storage, + sessionId + ) + LocalStorageService.handleHydrate( + LocalStorageSyncTypes.LISTS, + data => store.dispatch(new HydrateListsStateAction(data)), + storage, + sessionId + ) + } + } + } + + private static handleHydrate( + type: LocalStorageSyncTypes, + dispatch: (data: any) => void, + storage: Storage, + sessionId: string + ) { + const key = LocalStorageService.makeKey(sessionId, type); + try { + dispatch(JSON.parse(storage.getItem(key))); + } catch (e) { + console.warn(`Failed to parse user settings with key '${key}' from session storage, consider clearing manually`, e); + } + } + + public static makeKey(userId: string, storeKey: string) { + return userId + '-' + storeKey + } +} \ No newline at end of file diff --git a/src/frontend/packages/store/src/helpers/store-helpers.ts b/src/frontend/packages/store/src/helpers/store-helpers.ts index cac7ba4e0a..011e83986d 100644 --- a/src/frontend/packages/store/src/helpers/store-helpers.ts +++ b/src/frontend/packages/store/src/helpers/store-helpers.ts @@ -9,6 +9,7 @@ import { APIResource } from '../types/api.types'; import { IFavoritesInfo } from '../types/user-favorites.types'; +// TODO: RC bring into LocalStorageService, rename export function getDashboardStateSessionId(username?: string) { const prefix = 'stratos-'; if (username) { diff --git a/src/frontend/packages/store/src/reducers.module.ts b/src/frontend/packages/store/src/reducers.module.ts index d5268b68bf..e35a6c399c 100644 --- a/src/frontend/packages/store/src/reducers.module.ts +++ b/src/frontend/packages/store/src/reducers.module.ts @@ -2,7 +2,8 @@ import { NgModule } from '@angular/core'; import { ActionReducer, ActionReducerMap, StoreModule } from '@ngrx/store'; import { localStorageSync } from 'ngrx-store-localstorage'; -import { getDashboardStateSessionId } from './helpers/store-helpers'; +import { LocalStorageService, LocalStorageSyncTypes } from './helpers/local-storage-service'; +import { getDashboardStateSessionId } from './public-api'; import { actionHistoryReducer } from './reducers/action-history-reducer'; import { requestReducer } from './reducers/api-request-reducers.generator'; import { authReducer } from './reducers/auth.reducer'; @@ -16,6 +17,8 @@ import { listReducer } from './reducers/list.reducer'; import { requestPaginationReducer } from './reducers/pagination-reducer.generator'; import { routingReducer } from './reducers/routing.reducer'; import { uaaSetupReducer } from './reducers/uaa-setup.reducers'; +import { PaginationState } from './types/pagination.types'; + // NOTE: Revisit when ngrx-store-logger supports Angular 7 (https://github.com/btroncone/ngrx-store-logger) @@ -27,7 +30,7 @@ import { uaaSetupReducer } from './reducers/uaa-setup.reducers'; // return storeLogger()(reducer); // } -export const appReducers = { +export const appReducers: ActionReducerMap<{}> = { auth: authReducer, uaaSetup: uaaSetupReducer, endpoints: endpointsReducer, @@ -43,15 +46,14 @@ export const appReducers = { currentUserRoles: currentUserRolesReducer, userFavoritesGroups: userFavoriteGroupsReducer, recentlyVisited: recentlyVisitedReducer, -} as ActionReducerMap<{}>; +}; export function localStorageSyncReducer(reducer: ActionReducer): ActionReducer { // This is done to ensure we don't accidentally apply state from session storage from another user. let globalUserId = null; return localStorageSync({ - storageKeySerializer: (id) => { - return globalUserId || id; - }, + // Decide the key to store each section by + storageKeySerializer: (storeKey: LocalStorageSyncTypes) => LocalStorageService.makeKey(globalUserId, storeKey), syncCondition: () => { if (globalUserId) { return true; @@ -63,7 +65,20 @@ export function localStorageSyncReducer(reducer: ActionReducer): ActionRedu } return false; }, - keys: ['dashboard'], + keys: [ + LocalStorageSyncTypes.DASHBOARD, + LocalStorageSyncTypes.LISTS, + { + [LocalStorageSyncTypes.PAGINATION]: { + serialize: (pagination: PaginationState) => LocalStorageService.parseForStorage( + pagination, + LocalStorageSyncTypes.PAGINATION + ), + } + }, + // encrypt: // TODO: RC only store guids, so shouldn't need + // decrypt: // TODO: RC + ], rehydrate: false, })(reducer); diff --git a/src/frontend/packages/store/src/reducers/list.reducer.ts b/src/frontend/packages/store/src/reducers/list.reducer.ts index 392f627b69..3348067c41 100644 --- a/src/frontend/packages/store/src/reducers/list.reducer.ts +++ b/src/frontend/packages/store/src/reducers/list.reducer.ts @@ -1,9 +1,9 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { ListStateActionTypes, ListView, SetListViewAction } from '../actions/list.actions'; -import { mergeState } from '../helpers/reducer.helper'; +import { HydrateListsStateAction, ListStateActionTypes, ListView, SetListViewAction } from '../actions/list.actions'; import { ListsOnlyAppState } from '../app-state'; +import { mergeState } from '../helpers/reducer.helper'; export class ListsState { [key: string]: ListState; @@ -34,6 +34,11 @@ export function listReducer(state = defaultListsState, action): ListsState { 'view', listView ? listView.toString() : '' ); + case ListStateActionTypes.HYDRATE: + const hydrate = action as HydrateListsStateAction; + return { + ...hydrate.listsState + }; default: return state; } diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts index f39aa853f0..b60170fa2d 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts @@ -10,6 +10,8 @@ import { CLEAR_PAGINATION_OF_TYPE, ClearPaginationOfType, CREATE_PAGINATION, + HYDRATE_PAGINATION_STATE, + HydratePaginationStateAction, IGNORE_MAXED_STATE, IgnorePaginationMaxedState, REMOVE_PARAMS, @@ -100,7 +102,7 @@ function paginationReducer(updatePagination) { }; } -function paginate(action, state = {}, updatePagination) { +function paginate(action, state: PaginationState = {}, updatePagination) { if (action.type === ApiActionTypes.API_REQUEST_START) { return state; } @@ -143,9 +145,47 @@ function paginate(action, state = {}, updatePagination) { return paginationIgnoreMaxed(state, action as IgnorePaginationMaxedState); } + if (action.type === HYDRATE_PAGINATION_STATE) { + return hydratePagination(state, action as HydratePaginationStateAction); + } + return enterPaginationReducer(state, action, updatePagination); } +// TODO: RC tidy the hell out of this +function hydratePagination(state: PaginationState, action: HydratePaginationStateAction): PaginationState { + const hydrate = (action.paginationState || {}); // TODO: RC don't wrap when parseing + const entityKeys = Object.keys(hydrate || {}); + if (entityKeys.length === 0) { + return state; + } + + // TODO: RC how to remove entries (pagination and list) that no longer exist (endpoint not connected, deleted app (bindings), etc)? + + const newState = entityKeys.reduce((res, entityKey) => { + const existingEntityState = state[entityKey] || {}; + const hydrateEntityState = action.paginationState[entityKey]; // TODO: RC don't wrap when parseing + + // TODO: RC change Object.keys to Object.entries + res[entityKey] = Object.keys(hydrateEntityState).reduce((res2, paginationKey) => { + const existingPageState = existingEntityState[paginationKey] || getDefaultPaginationEntityState() + const hydratePagSection = hydrateEntityState[paginationKey]; + res2[paginationKey] = { + ...existingPageState, + ...hydratePagSection + } + return res2; + }, { + ...existingEntityState + }); + + return res; + }, { + ...state + }); + return newState; +} + function isEndpointAction(action) { // ... that we care about. return action.type === DISCONNECT_ENDPOINTS_SUCCESS || From d87bd68224ee2643a8c59af13760afc6e8d41a3e Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 3 Jul 2020 19:47:51 +0100 Subject: [PATCH 02/23] Add list reset button & list button tooltips --- .../components/list/list.component.html | 23 ++++++++---- .../shared/components/list/list.component.ts | 19 +++++++++- .../polling-indicator.component.html | 6 ++-- .../store/src/actions/pagination.actions.ts | 13 ++++++- .../pagination-reducer-reset-sort-filter.ts | 36 +++++++++++++++++++ .../pagination-reducer/pagination.reducer.ts | 7 ++++ 6 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-sort-filter.ts diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.html b/src/frontend/packages/core/src/shared/components/list/list.component.html index 28ee1e36ef..cd02b03098 100644 --- a/src/frontend/packages/core/src/shared/components/list/list.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list.component.html @@ -105,12 +105,14 @@ - - @@ -143,14 +145,23 @@ add +
+ +
- - diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.ts b/src/frontend/packages/core/src/shared/components/list/list.component.ts index 318bd0dd7e..52374e0ab5 100644 --- a/src/frontend/packages/core/src/shared/components/list/list.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list.component.ts @@ -49,12 +49,13 @@ import { ListView, SetListViewAction, } from '../../../../../store/src/actions/list.actions'; -import { SetClientFilterKey, SetPage } from '../../../../../store/src/actions/pagination.actions'; +import { ResetPaginationSortFilter, SetClientFilterKey, SetPage } from '../../../../../store/src/actions/pagination.actions'; import { GeneralAppState } from '../../../../../store/src/app-state'; import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; import { EntityCatalogEntityConfig } from '../../../../../store/src/entity-catalog/entity-catalog.types'; import { ActionState } from '../../../../../store/src/reducers/api-request-reducer/types'; import { getListStateObservables } from '../../../../../store/src/reducers/list.reducer'; +import { PaginatedAction } from '../../../../../store/src/types/pagination.types'; import { safeUnsubscribe } from '../../../core/utils.service'; import { EntitySelectConfig, @@ -594,6 +595,22 @@ export class ListComponent implements OnInit, OnChanges, OnDestroy, AfterView } } + resetFilteringAndSort() { + // TODO: RC check for better way of doing this + const pAction: PaginatedAction = this.dataSource.action['length'] ? this.dataSource.action[0] : this.dataSource.action + this.store.dispatch(new ResetPaginationSortFilter(pAction)); + // Ensure that the changes are pushed back to the multi filter controls + this.multiFilterManagers.forEach(manager => { + manager.value + manager.hasOneItem$.pipe(first()).subscribe(hasOneItem => { + if (hasOneItem) { + return; + } + manager.selectItem('') + }) + }) + } + updateListView(listView: ListView) { this.store.dispatch(new SetListViewAction(this.listViewKey, listView)); } diff --git a/src/frontend/packages/core/src/shared/components/polling-indicator/polling-indicator.component.html b/src/frontend/packages/core/src/shared/components/polling-indicator/polling-indicator.component.html index 3c350ac46b..d4a15a557d 100644 --- a/src/frontend/packages/core/src/shared/components/polling-indicator/polling-indicator.component.html +++ b/src/frontend/packages/core/src/shared/components/polling-indicator/polling-indicator.component.html @@ -1,10 +1,12 @@ - - \ No newline at end of file diff --git a/src/frontend/packages/store/src/actions/pagination.actions.ts b/src/frontend/packages/store/src/actions/pagination.actions.ts index 492399bfca..53934c803f 100644 --- a/src/frontend/packages/store/src/actions/pagination.actions.ts +++ b/src/frontend/packages/store/src/actions/pagination.actions.ts @@ -1,11 +1,12 @@ import { Action } from '@ngrx/store'; import { EntityCatalogEntityConfig, extractEntityCatalogEntityConfig } from '../entity-catalog/entity-catalog.types'; -import { PaginationClientFilter, PaginationParam, PaginationState } from '../types/pagination.types'; +import { PaginatedAction, PaginationClientFilter, PaginationParam, PaginationState } from '../types/pagination.types'; export const CLEAR_PAGINATION_OF_TYPE = '[Pagination] Clear all pages of type'; export const CLEAR_PAGINATION_OF_ENTITY = '[Pagination] Clear pagination of entity'; export const RESET_PAGINATION = '[Pagination] Reset pagination'; +export const RESET_PAGINATION_SORT_FILTER = '[Pagination] Reset pagination sort & filter'; export const CREATE_PAGINATION = '[Pagination] Create pagination'; export const CLEAR_PAGES = '[Pagination] Clear pages only'; export const SET_PAGE = '[Pagination] Set page'; @@ -58,6 +59,16 @@ export class ResetPagination extends BasePaginationAction implements Action { type = RESET_PAGINATION; } +/** + * Reset filter & sorting like ResetPagination except retain the results + */ +export class ResetPaginationSortFilter extends BasePaginationAction implements Action { + constructor(public pAction: PaginatedAction) { + super(pAction); + } + type = RESET_PAGINATION_SORT_FILTER; +} + export class CreatePagination extends BasePaginationAction implements Action { /** * @param seed The pagination key for the section we should use as a seed when creating the new pagination section. diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-sort-filter.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-sort-filter.ts new file mode 100644 index 0000000000..3d2ab4aaed --- /dev/null +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-sort-filter.ts @@ -0,0 +1,36 @@ +import { ResetPaginationSortFilter } from '../../actions/pagination.actions'; +import { entityCatalog } from '../../entity-catalog/entity-catalog'; +import { PaginationEntityState, PaginationState } from '../../types/pagination.types'; + +export function paginationResetToStart(state: PaginationState, action: ResetPaginationSortFilter): PaginationState { + const { pAction } = action; + const entityKey = entityCatalog.getEntityKey(pAction); + const pKey = action.pAction.paginationKey; + + if (!state[entityKey] || !state[entityKey][pKey]) { + return state; + } + const pSection: PaginationEntityState = state[entityKey][pKey] + // TODO: RC it would be nice to try to also reset page & page size... but we've lost the latter + const res: PaginationEntityState = { + ...pSection, + clientPagination: { + ...pSection.clientPagination, + filter: { + items: [], + string: '' + }, + }, + // maxedState:// TODO: RC test + params: { + ...pAction.initialParams + }, + } + return { + ...state, + [entityKey]: { + ...state[entityKey], + [pKey]: res + } + }; +} \ No newline at end of file diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts index b60170fa2d..d87c88cf0d 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts @@ -16,6 +16,8 @@ import { IgnorePaginationMaxedState, REMOVE_PARAMS, RESET_PAGINATION, + RESET_PAGINATION_SORT_FILTER, + ResetPaginationSortFilter, SET_CLIENT_FILTER, SET_CLIENT_FILTER_KEY, SET_CLIENT_PAGE, @@ -42,6 +44,7 @@ import { createNewPaginationSection } from './pagination-reducer-create-paginati import { paginationIgnoreMaxed, paginationMaxReached } from './pagination-reducer-max-reached'; import { paginationRemoveParams } from './pagination-reducer-remove-params'; import { getDefaultPaginationEntityState, paginationResetPagination } from './pagination-reducer-reset-pagination'; +import { paginationResetToStart } from './pagination-reducer-reset-sort-filter'; import { paginationSetClientFilter } from './pagination-reducer-set-client-filter'; import { paginationSetClientFilterKey } from './pagination-reducer-set-client-filter-key'; import { paginationSetClientPage } from './pagination-reducer-set-client-page'; @@ -149,6 +152,10 @@ function paginate(action, state: PaginationState = {}, updatePagination) { return hydratePagination(state, action as HydratePaginationStateAction); } + if (action.type === RESET_PAGINATION_SORT_FILTER) { + return paginationResetToStart(state, action as ResetPaginationSortFilter); + } + return enterPaginationReducer(state, action, updatePagination); } From ee41b63870928ba8d43b4e5636de973bb33d8463 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 31 Jul 2020 17:29:59 +0100 Subject: [PATCH 03/23] Few tidy ups, add clear local storage feature --- .../profile-info/profile-info.component.html | 18 +- .../profile-info/profile-info.component.scss | 10 + .../profile-info/profile-info.component.ts | 36 +++- .../shared/components/list/list.component.ts | 8 +- .../src/helpers/local-storage-service.ts | 190 ++++++++++++++---- .../store/src/helpers/store-helpers.ts | 13 -- .../packages/store/src/reducers.module.ts | 49 +---- .../pagination-reducer/pagination.reducer.ts | 3 + 8 files changed, 223 insertions(+), 104 deletions(-) diff --git a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html index c08c7704c5..db05ea02b4 100644 --- a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html +++ b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html @@ -101,7 +101,7 @@

User Profile

- diff --git a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.scss b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.scss index ba56f7161a..3be8be5d30 100644 --- a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.scss +++ b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.scss @@ -49,4 +49,14 @@ $user-profile-avatar-size: 48px; &__content { margin: 2% 24px; } + + &__local-storage { + &--div { + padding-bottom: 10px; + } + + button { + margin-left: 15px; + } + } } diff --git a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts index 915ecd6aea..8d35d1ea3a 100644 --- a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts +++ b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts @@ -1,15 +1,18 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { filter, first, map } from 'rxjs/operators'; import { SetPollingEnabledAction, SetSessionTimeoutAction } from '../../../../../store/src/actions/dashboard-actions'; -import { DashboardOnlyAppState } from '../../../../../store/src/app-state'; +import { AppState } from '../../../../../store/src/app-state'; +import { LocalStorageService } from '../../../../../store/src/helpers/local-storage-service'; +import { selectSessionData } from '../../../../../store/src/reducers/auth.reducer'; import { selectDashboardState } from '../../../../../store/src/selectors/dashboard.selectors'; import { ThemeService } from '../../../../../store/src/theme.service'; import { UserProfileInfo } from '../../../../../store/src/types/user-profile.types'; import { UserProfileService } from '../../../core/user-profile.service'; import { UserService } from '../../../core/user.service'; +import { ConfirmationDialogService } from '../../../shared/components/confirmation-dialog.service'; import { SetGravatarEnabledAction } from './../../../../../store/src/actions/dashboard-actions'; @Component({ @@ -19,19 +22,22 @@ import { SetGravatarEnabledAction } from './../../../../../store/src/actions/das }) export class ProfileInfoComponent { - public timeoutSession$ = this.store.select(selectDashboardState).pipe( + private dashboardState$ = this.store.select(selectDashboardState); + private sessionData$ = this.store.select(selectSessionData()); + + public timeoutSession$ = this.dashboardState$.pipe( map(dashboardState => dashboardState.timeoutSession ? 'true' : 'false') ); - public pollingEnabled$ = this.store.select(selectDashboardState).pipe( + public pollingEnabled$ = this.dashboardState$.pipe( map(dashboardState => dashboardState.pollingEnabled ? 'true' : 'false') ); - public gravatarEnabled$ = this.store.select(selectDashboardState).pipe( + public gravatarEnabled$ = this.dashboardState$.pipe( map(dashboardState => dashboardState.gravatarEnabled ? 'true' : 'false') ); - public allowGravatar$ = this.store.select(selectDashboardState).pipe( + public allowGravatar$ = this.dashboardState$.pipe( map(dashboardState => dashboardState.gravatarEnabled) ); @@ -42,6 +48,8 @@ export class ProfileInfoComponent { primaryEmailAddress$: Observable; hasMultipleThemes: boolean; + localStorageSize$: Observable; + public updateSessionKeepAlive(timeoutSession: string) { const newVal = !(timeoutSession === 'true'); this.setSessionTimeout(newVal); @@ -69,10 +77,11 @@ export class ProfileInfoComponent { } constructor( - private userProfileService: UserProfileService, - private store: Store, + userProfileService: UserProfileService, + private store: Store, public userService: UserService, - public themeService: ThemeService + public themeService: ThemeService, + private confirmationService: ConfirmationDialogService ) { this.isError$ = userProfileService.isError$; this.userProfile$ = userProfileService.userProfile$; @@ -83,6 +92,15 @@ export class ProfileInfoComponent { ); this.hasMultipleThemes = themeService.getThemes().length > 1; + + this.localStorageSize$ = this.sessionData$.pipe( + map(sessionData => LocalStorageService.localStorageSize(sessionData)), + filter(bytes => bytes != -1), + ) + } + + clearLocalStorage() { + this.sessionData$.pipe(first()).subscribe(sessionData => LocalStorageService.clear(sessionData, this.confirmationService)) } } diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.ts b/src/frontend/packages/core/src/shared/components/list/list.component.ts index 52374e0ab5..d8d76bf64e 100644 --- a/src/frontend/packages/core/src/shared/components/list/list.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list.component.ts @@ -596,13 +596,15 @@ export class ListComponent implements OnInit, OnChanges, OnDestroy, AfterView } resetFilteringAndSort() { - // TODO: RC check for better way of doing this + // TODO: RC multi entity type lists const pAction: PaginatedAction = this.dataSource.action['length'] ? this.dataSource.action[0] : this.dataSource.action this.store.dispatch(new ResetPaginationSortFilter(pAction)); - // Ensure that the changes are pushed back to the multi filter controls + // Ensure that the changes are pushed back to the multi filter controls. Ideally the field to store relationship would be two way.. + // but it's not for the moment this.multiFilterManagers.forEach(manager => { - manager.value manager.hasOneItem$.pipe(first()).subscribe(hasOneItem => { + // TODO: RC Test + // const selectItem = hasOneItem || '' if (hasOneItem) { return; } diff --git a/src/frontend/packages/store/src/helpers/local-storage-service.ts b/src/frontend/packages/store/src/helpers/local-storage-service.ts index 40418b40ba..e18f021ba6 100644 --- a/src/frontend/packages/store/src/helpers/local-storage-service.ts +++ b/src/frontend/packages/store/src/helpers/local-storage-service.ts @@ -1,18 +1,22 @@ -import { Store } from '@ngrx/store'; +import { ActionReducer, Store } from '@ngrx/store'; +import { localStorageSync } from 'ngrx-store-localstorage'; +import { ConfirmationDialogConfig } from '../../../core/src/shared/components/confirmation-dialog.config'; +import { ConfirmationDialogService } from '../../../core/src/shared/components/confirmation-dialog.service'; import { HydrateDashboardStateAction } from '../actions/dashboard-actions'; import { HydrateListsStateAction } from '../actions/list.actions'; import { HydratePaginationStateAction } from '../actions/pagination.actions'; import { DispatchOnlyAppState } from '../app-state'; import { SessionData } from '../types/auth.types'; import { PaginationState } from '../types/pagination.types'; -import { getDashboardStateSessionId } from './store-helpers'; + export enum LocalStorageSyncTypes { DASHBOARD = 'dashboard', PAGINATION = 'pagination', LISTS = 'lists', } + export class LocalStorageService { // TODO: RC applying filters that aren't in lists @@ -20,57 +24,39 @@ export class LocalStorageService { // TODO: RC cleaning sessions storage (entities that don't exist, etc, can be done by storeagesync??) // TODO: RC backward compatible (load 'user-dashboard' into 'user') // TODO: RC todos! + // TODO: RC add clear session data from local storage button + /** - * Allow for selective persistence of data + * Normally used on app init, move local storage data into the console's store */ - public static parseForStorage(storePart: T, type: LocalStorageSyncTypes): Object { - // TODO: RC Tidy - switch (type) { - case LocalStorageSyncTypes.PAGINATION: - const pagination: PaginationState = storePart as unknown as PaginationState; - const abs = Object.keys(pagination).reduce((res, entityTypes) => { - const perEntity = Object.keys(pagination[entityTypes]).reduce((res2, paginationKeysOfEntityType) => { - const paginationSection = pagination[entityTypes][paginationKeysOfEntityType] - res2[paginationKeysOfEntityType] = { - params: paginationSection.params, - clientPagination: paginationSection.clientPagination - } - return res2; - }, {}); - if (Object.keys(perEntity).length > 0) { - res[entityTypes] = perEntity; - } - return res; - }, {}); - return abs; - } - return storePart; - } public static storageToStore(store: Store, sessionData: SessionData) { return LocalStorageService.rehydrateDashboardState(store, sessionData); } + /** + * For the current user dispatch actions that will populate the store with the contents of local storage + */ private static rehydrateDashboardState(store: Store, sessionData: SessionData) { - const storage = localStorage || window.localStorage; + const storage = LocalStorageService.getStorage(); // We use the username to key the session storage. We could replace this with the users id? if (storage && sessionData.user) { - const sessionId = getDashboardStateSessionId(sessionData.user.name); + const sessionId = LocalStorageService.getDashboardStateSessionId(sessionData.user.name); if (sessionId) { LocalStorageService.handleHydrate( LocalStorageSyncTypes.DASHBOARD, - data => store.dispatch(new HydrateDashboardStateAction(data)), + dataForStore => store.dispatch(new HydrateDashboardStateAction(dataForStore)), storage, sessionId ) LocalStorageService.handleHydrate( LocalStorageSyncTypes.PAGINATION, - data => store.dispatch(new HydratePaginationStateAction(data)), + dataForStore => store.dispatch(new HydratePaginationStateAction(dataForStore)), storage, sessionId ) LocalStorageService.handleHydrate( LocalStorageSyncTypes.LISTS, - data => store.dispatch(new HydrateListsStateAction(data)), + dataForStore => store.dispatch(new HydrateListsStateAction(dataForStore)), storage, sessionId ) @@ -78,9 +64,17 @@ export class LocalStorageService { } } + private static getStorage(): Storage { + return localStorage || window.localStorage; + } + + /** + * For a given storage type fetch it's data for the given user from local storage and dispatch an action that will + * be handled by the reducers for that storage type (dashboard, pagination, etc) + */ private static handleHydrate( type: LocalStorageSyncTypes, - dispatch: (data: any) => void, + dispatch: (dataForStore: any) => void, storage: Storage, sessionId: string ) { @@ -92,7 +86,135 @@ export class LocalStorageService { } } - public static makeKey(userId: string, storeKey: string) { + private static makeKey(userId: string, storeKey: string) { return userId + '-' + storeKey } -} \ No newline at end of file + + /** + * This will ensure changes in the store are selectively pushed to local storage + */ + public static localStorageSyncReducer(reducer: ActionReducer): ActionReducer { + // This is done to ensure we don't accidentally apply state from session storage from another user. + let globalUserId = null; + return localStorageSync({ + // Decide the key to store each section by + storageKeySerializer: (storeKey: LocalStorageSyncTypes) => LocalStorageService.makeKey(globalUserId, storeKey), + syncCondition: () => { + if (globalUserId) { + return true; + } + const userId = LocalStorageService.getDashboardStateSessionId(); + if (userId) { + globalUserId = userId; + return true; + } + return false; + }, + keys: [ + LocalStorageSyncTypes.DASHBOARD, + LocalStorageSyncTypes.LISTS, + { + [LocalStorageSyncTypes.PAGINATION]: { + serialize: (pagination: PaginationState) => LocalStorageService.parseForStorage( + pagination, + LocalStorageSyncTypes.PAGINATION + ), + } + }, + // encrypt: // TODO: RC only store guids, so shouldn't need + // decrypt: // TODO: RC + ], + // Don't push local storage state into store on start up... we need the logged in user's id before we can do that + rehydrate: false, + + })(reducer); + } + + private static getDashboardStateSessionId(username?: string) { + const prefix = 'stratos-'; + if (username) { + return prefix + username; + } + const idElement = document.getElementById('__stratos-userid__'); + if (idElement) { + return prefix + idElement.innerText; + } + return null; + } + + /** + * Allow for selective persistence of data. For pagination we only store params and clientPagination + */ + private static parseForStorage(storePart: T, type: LocalStorageSyncTypes): Object { + // TODO: RC Tidy + switch (type) { + case LocalStorageSyncTypes.PAGINATION: + const pagination: PaginationState = storePart as unknown as PaginationState; + // TODO: RC Ignore if not from a list!!!! + const abs = Object.keys(pagination).reduce((res, entityTypes) => { + const perEntity = Object.keys(pagination[entityTypes]).reduce((res2, paginationKeysOfEntityType) => { + const paginationSection = pagination[entityTypes][paginationKeysOfEntityType] + res2[paginationKeysOfEntityType] = { + params: paginationSection.params, + clientPagination: paginationSection.clientPagination + } + return res2; + }, {}); + if (Object.keys(perEntity).length > 0) { + res[entityTypes] = perEntity; + } + return res; + }, {}); + return abs; + } + return storePart; + } + + public static localStorageSize(sessionData: SessionData): number { + const storage = LocalStorageService.getStorage() + const sessionId = LocalStorageService.getDashboardStateSessionId(sessionData.user.name); + if (storage && sessionId) { + return Object.values(LocalStorageSyncTypes).reduce((total, type) => { + const key = LocalStorageService.makeKey(sessionId, type); + const content = storage.getItem(key); + // We're getting an approximate size in bytes, so just assume a character is one byte + return total + content.length; + }, 0); + } + return -1; + } + + /** + * Clear local storage and the store + */ + public static clear(sessionData: SessionData, confirmationService: ConfirmationDialogService, reloadTo = '/user-profile') { + const config: ConfirmationDialogConfig = { + message: 'This will clear your local storage for this console and reload this window', + confirm: 'Clear', + critical: true, + title: 'Are you sure?' + } + + const successAction = res => { + if (!res) { + return; + } + + const storage = LocalStorageService.getStorage() + const sessionId = LocalStorageService.getDashboardStateSessionId(sessionData.user.name); + if (storage && sessionId) { + Object.values(LocalStorageSyncTypes).forEach(type => { + const key = LocalStorageService.makeKey(sessionId, type); + storage.removeItem(key); + }, 0); + + // This is a brutal approach but is a lot easier than reverting all user changes in the store + window.location.assign(reloadTo); + } else { + console.warn('Unable to clear local storage, either storage or session id is missing'); + } + } + + confirmationService.openWithCancel(config, successAction, () => { }); + } +} diff --git a/src/frontend/packages/store/src/helpers/store-helpers.ts b/src/frontend/packages/store/src/helpers/store-helpers.ts index 011e83986d..601f9777ed 100644 --- a/src/frontend/packages/store/src/helpers/store-helpers.ts +++ b/src/frontend/packages/store/src/helpers/store-helpers.ts @@ -9,19 +9,6 @@ import { APIResource } from '../types/api.types'; import { IFavoritesInfo } from '../types/user-favorites.types'; -// TODO: RC bring into LocalStorageService, rename -export function getDashboardStateSessionId(username?: string) { - const prefix = 'stratos-'; - if (username) { - return prefix + username; - } - const idElement = document.getElementById('__stratos-userid__'); - if (idElement) { - return prefix + idElement.innerText; - } - return null; -} - export function getFavoriteInfoObservable(store: Store): Observable { return combineLatest( store.select(fetchingFavoritesSelector), diff --git a/src/frontend/packages/store/src/reducers.module.ts b/src/frontend/packages/store/src/reducers.module.ts index e35a6c399c..669215c7f7 100644 --- a/src/frontend/packages/store/src/reducers.module.ts +++ b/src/frontend/packages/store/src/reducers.module.ts @@ -1,9 +1,7 @@ import { NgModule } from '@angular/core'; -import { ActionReducer, ActionReducerMap, StoreModule } from '@ngrx/store'; -import { localStorageSync } from 'ngrx-store-localstorage'; +import { ActionReducerMap, StoreModule } from '@ngrx/store'; -import { LocalStorageService, LocalStorageSyncTypes } from './helpers/local-storage-service'; -import { getDashboardStateSessionId } from './public-api'; +import { LocalStorageService } from './helpers/local-storage-service'; import { actionHistoryReducer } from './reducers/action-history-reducer'; import { requestReducer } from './reducers/api-request-reducers.generator'; import { authReducer } from './reducers/auth.reducer'; @@ -17,7 +15,6 @@ import { listReducer } from './reducers/list.reducer'; import { requestPaginationReducer } from './reducers/pagination-reducer.generator'; import { routingReducer } from './reducers/routing.reducer'; import { uaaSetupReducer } from './reducers/uaa-setup.reducers'; -import { PaginationState } from './types/pagination.types'; // NOTE: Revisit when ngrx-store-logger supports Angular 7 (https://github.com/btroncone/ngrx-store-logger) @@ -48,50 +45,14 @@ export const appReducers: ActionReducerMap<{}> = { recentlyVisited: recentlyVisitedReducer, }; -export function localStorageSyncReducer(reducer: ActionReducer): ActionReducer { - // This is done to ensure we don't accidentally apply state from session storage from another user. - let globalUserId = null; - return localStorageSync({ - // Decide the key to store each section by - storageKeySerializer: (storeKey: LocalStorageSyncTypes) => LocalStorageService.makeKey(globalUserId, storeKey), - syncCondition: () => { - if (globalUserId) { - return true; - } - const userId = getDashboardStateSessionId(); - if (userId) { - globalUserId = userId; - return true; - } - return false; - }, - keys: [ - LocalStorageSyncTypes.DASHBOARD, - LocalStorageSyncTypes.LISTS, - { - [LocalStorageSyncTypes.PAGINATION]: { - serialize: (pagination: PaginationState) => LocalStorageService.parseForStorage( - pagination, - LocalStorageSyncTypes.PAGINATION - ), - } - }, - // encrypt: // TODO: RC only store guids, so shouldn't need - // decrypt: // TODO: RC - ], - rehydrate: false, - - })(reducer); -} - -const metaReducers = [localStorageSyncReducer]; - @NgModule({ imports: [ StoreModule.forRoot( appReducers, { - metaReducers, + metaReducers: [ + LocalStorageService.localStorageSyncReducer + ], runtimeChecks: { strictStateImmutability: true, strictActionImmutability: false diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts index d87c88cf0d..23424648a9 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts @@ -160,6 +160,9 @@ function paginate(action, state: PaginationState = {}, updatePagination) { } // TODO: RC tidy the hell out of this +/** + * Push data (in the action) from local storage back into the pagination state + */ function hydratePagination(state: PaginationState, action: HydratePaginationStateAction): PaginationState { const hydrate = (action.paginationState || {}); // TODO: RC don't wrap when parseing const entityKeys = Object.keys(hydrate || {}); From 04547180f1937a6013d39b655ee013d8b9a40088 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 31 Jul 2020 18:20:58 +0100 Subject: [PATCH 04/23] Tweak profile settings section --- .../profile-info/profile-info.component.html | 13 ++++++------- .../store/src/helpers/local-storage-service.ts | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html index db05ea02b4..d812642de4 100644 --- a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html +++ b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html @@ -50,7 +50,7 @@

User Profile

- Settings + Local Settings