From 7214914f394256d4745d295b072e740dc070ca35 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Wed, 16 Nov 2022 11:28:19 +0300 Subject: [PATCH 1/5] Move history utility to cache module --- .../services/actions/query-action-creators.ts | 4 +- .../request-history-action-creators.ts | 6 +- src/app/views/sidebar/history/History.tsx | 6 +- .../views/sidebar/history/history-utils.ts | 49 --------------- src/index.tsx | 4 +- .../cache}/history-utils.spec.ts | 14 ++--- src/modules/cache/history-utils.ts | 59 +++++++++++++++++++ 7 files changed, 78 insertions(+), 64 deletions(-) delete mode 100644 src/app/views/sidebar/history/history-utils.ts rename src/{app/views/sidebar/history => modules/cache}/history-utils.spec.ts (85%) create mode 100644 src/modules/cache/history-utils.ts diff --git a/src/app/services/actions/query-action-creators.ts b/src/app/services/actions/query-action-creators.ts index 6aeea6276a..ef56958891 100644 --- a/src/app/services/actions/query-action-creators.ts +++ b/src/app/services/actions/query-action-creators.ts @@ -6,7 +6,7 @@ import { IQuery } from '../../../types/query-runner'; import { IStatus } from '../../../types/status'; import { ClientError } from '../../utils/error-utils/ClientError'; import { setStatusMessage } from '../../utils/status-message'; -import { writeHistoryData } from '../../views/sidebar/history/history-utils'; +import { historyCache } from '../../../modules/cache/history-utils'; import { anonymousRequest, authenticatedRequest, @@ -180,7 +180,7 @@ async function createHistory( result }; - writeHistoryData(historyItem); + historyCache.writeHistoryData(historyItem); dispatch(addHistoryItem(historyItem)); return result; diff --git a/src/app/services/actions/request-history-action-creators.ts b/src/app/services/actions/request-history-action-creators.ts index 995e5996fa..e048ca8394 100644 --- a/src/app/services/actions/request-history-action-creators.ts +++ b/src/app/services/actions/request-history-action-creators.ts @@ -1,7 +1,7 @@ import { AppAction } from '../../../types/action'; import { IHistoryItem } from '../../../types/history'; -import { bulkRemoveHistoryData, removeHistoryData } from '../../views/sidebar/history/history-utils'; +import { historyCache } from '../../../modules/cache/history-utils'; import { ADD_HISTORY_ITEM_SUCCESS, REMOVE_ALL_HISTORY_ITEMS_SUCCESS, @@ -35,7 +35,7 @@ export function removeHistoryItem(historyItem: IHistoryItem) { delete historyItem.category; return async (dispatch: Function) => { - return removeHistoryData(historyItem) + return historyCache.removeHistoryData(historyItem) .then(() => { dispatch({ type: REMOVE_HISTORY_ITEM_SUCCESS, @@ -53,7 +53,7 @@ export function bulkRemoveHistoryItems(historyItems: IHistoryItem[]) { }); return async (dispatch: Function) => { - return bulkRemoveHistoryData(listOfKeys) + return historyCache.bulkRemoveHistoryData(listOfKeys) .then(() => { dispatch({ type: REMOVE_ALL_HISTORY_ITEMS_SUCCESS, diff --git a/src/app/views/sidebar/history/History.tsx b/src/app/views/sidebar/history/History.tsx index 44f7d24205..71500b1572 100644 --- a/src/app/views/sidebar/history/History.tsx +++ b/src/app/views/sidebar/history/History.tsx @@ -4,7 +4,7 @@ import { DialogFooter, DialogType, getId, getTheme, IColumn, IconButton, Label, MessageBar, MessageBarType, PrimaryButton, SearchBox, SelectionMode, styled, TooltipHost } from '@fluentui/react'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; @@ -94,6 +94,10 @@ const History = (props: any) => { const classes = classNames(props); + useEffect(() => { + setHistoryItems(history); + }, [history]) + if (!history || history.length === 0) { return NoResultsFound('We did not find any history items'); } diff --git a/src/app/views/sidebar/history/history-utils.ts b/src/app/views/sidebar/history/history-utils.ts deleted file mode 100644 index ecad09d557..0000000000 --- a/src/app/views/sidebar/history/history-utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import localforage from 'localforage'; -import { IHistoryItem } from '../../../../types/history'; - -const historyStorage = localforage.createInstance({ - storeName: 'history', - name: 'GE_V4' -}); - -export async function writeHistoryData(historyItem: IHistoryItem) { - historyStorage.setItem(historyItem.createdAt, historyItem); -} - -export async function readHistoryData(): Promise { - let historyData: IHistoryItem[] = []; - const keys = await historyStorage.keys(); - for (const creationTime of keys) { - if (historyItemHasExpired(creationTime)) { - historyStorage.removeItem(creationTime); - } else { - const historyItem = await historyStorage.getItem(creationTime)! as IHistoryItem; - historyData = [...historyData, historyItem]; - } - } - return historyData; -} - -function historyItemHasExpired(createdAt: string): boolean { - const ageInDays: number = 30; - const dateToCompare = new Date(); - dateToCompare.setDate(dateToCompare.getDate() - ageInDays); - const expiryDate = dateToCompare.getTime(); - const createdTime = new Date(createdAt).getTime(); - return (createdTime < expiryDate); -} - -export const removeHistoryData = async (historyItem: IHistoryItem) => { - await historyStorage.removeItem(historyItem.createdAt); - return true; -}; - -export const bulkRemoveHistoryData = async (listOfKeys: string[]) => { - historyStorage.iterate((_value, key) => { - if (listOfKeys.includes(key)) { - historyStorage.removeItem(key); - } - }).then(() => { - return true; - }); -}; diff --git a/src/index.tsx b/src/index.tsx index 97963c2419..625e8d48ab 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,7 +23,7 @@ import { bulkAddHistoryItems } from './app/services/actions/request-history-acti import { changeThemeSuccess } from './app/services/actions/theme-action-creator'; import { isValidHttpsUrl } from './app/utils/external-link-validation'; import App from './app/views/App'; -import { readHistoryData } from './app/views/sidebar/history/history-utils'; +import { historyCache } from './modules/cache/history-utils'; import { geLocale } from './appLocale'; import messages from './messages'; import { authenticationWrapper } from './modules/authentication'; @@ -137,7 +137,7 @@ if (devxApiUrl && isValidHttpsUrl(devxApiUrl)) { appStore.dispatch(setDevxApiUrl(devxApi)); } -readHistoryData().then((data: any) => { +historyCache.readHistoryData().then((data: any) => { if (data.length > 0) { appStore.dispatch(bulkAddHistoryItems(data)); } diff --git a/src/app/views/sidebar/history/history-utils.spec.ts b/src/modules/cache/history-utils.spec.ts similarity index 85% rename from src/app/views/sidebar/history/history-utils.spec.ts rename to src/modules/cache/history-utils.spec.ts index dd5006db44..ebd2549eb9 100644 --- a/src/app/views/sidebar/history/history-utils.spec.ts +++ b/src/modules/cache/history-utils.spec.ts @@ -1,5 +1,5 @@ -import { IHistoryItem } from '../../../../types/history'; -import { writeHistoryData, readHistoryData, removeHistoryData } from './history-utils'; +import { IHistoryItem } from '../../types/history'; +import { historyCache } from './history-utils'; let historyItems: IHistoryItem[] = []; @@ -43,8 +43,8 @@ describe('History utils should', () => { status: 200 } expect(historyItems.length).toBe(0); - await writeHistoryData(historyItem); - const historyData = await readHistoryData(); + await historyCache.writeHistoryData(historyItem); + const historyData = await historyCache.readHistoryData(); expect(historyData.length).toBe(1); }); @@ -61,9 +61,9 @@ describe('History utils should', () => { duration: 200, status: 200 } - await writeHistoryData(historyItem); + await historyCache.writeHistoryData(historyItem); expect(historyItems.length).toBe(2); - await removeHistoryData(historyItem); + await historyCache.removeHistoryData(historyItem); expect(historyItems.length).toBe(1); }); @@ -83,7 +83,7 @@ describe('History utils should', () => { } historyItems.push(historyItem); expect(historyItems.length).toBe(1); - const historyData = await readHistoryData(); + const historyData = await historyCache.readHistoryData(); expect(historyData.length).toBe(0); }); }) \ No newline at end of file diff --git a/src/modules/cache/history-utils.ts b/src/modules/cache/history-utils.ts new file mode 100644 index 0000000000..a5c6edc413 --- /dev/null +++ b/src/modules/cache/history-utils.ts @@ -0,0 +1,59 @@ +import localforage from 'localforage'; +import { IHistoryItem } from '../../types/history'; + +const historyStorage = localforage.createInstance({ + storeName: 'history', + name: 'GE_V4' +}); + +function historyItemHasExpired(createdAt: string): boolean { + const ageInDays: number = 30; + const dateToCompare = new Date(); + dateToCompare.setDate(dateToCompare.getDate() - ageInDays); + const expiryDate = dateToCompare.getTime(); + const createdTime = new Date(createdAt).getTime(); + return (createdTime < expiryDate); +} + +export const historyCache = (function () { + + const writeHistoryData = (historyItem: IHistoryItem) => { + historyStorage.setItem(historyItem.createdAt, historyItem); + } + + const readHistoryData = async (): Promise => { + let historyData: IHistoryItem[] = []; + const keys = await historyStorage.keys(); + for (const creationTime of keys) { + if (historyItemHasExpired(creationTime)) { + historyStorage.removeItem(creationTime); + } else { + const historyItem = await historyStorage.getItem(creationTime)! as IHistoryItem; + historyData = [...historyData, historyItem]; + } + } + return historyData; + } + + const removeHistoryData = async (historyItem: IHistoryItem) => { + await historyStorage.removeItem(historyItem.createdAt); + return true; + }; + + const bulkRemoveHistoryData = async (listOfKeys: string[]) => { + historyStorage.iterate((_value, key) => { + if (listOfKeys.includes(key)) { + historyStorage.removeItem(key); + } + }).then(() => { + return true; + }); + }; + + return { + writeHistoryData, + bulkRemoveHistoryData, + removeHistoryData, + readHistoryData + } +})(); \ No newline at end of file From 83f57a5195f4174bb41dde78185c43dc4004aa71 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Wed, 16 Nov 2022 13:52:19 +0300 Subject: [PATCH 2/5] create samples cache module --- src/modules/cache/samples.cache.spec.ts | 33 +++++++++++++++++++++++++ src/modules/cache/samples.cache.ts | 29 ++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/modules/cache/samples.cache.spec.ts create mode 100644 src/modules/cache/samples.cache.ts diff --git a/src/modules/cache/samples.cache.spec.ts b/src/modules/cache/samples.cache.spec.ts new file mode 100644 index 0000000000..eb640a251d --- /dev/null +++ b/src/modules/cache/samples.cache.spec.ts @@ -0,0 +1,33 @@ +import { ISampleQuery } from '../../types/query-runner'; +import { samplesCache } from './samples.cache'; + +jest.mock('localforage', () => ({ + // eslint-disable-next-line @typescript-eslint/no-empty-function + config: () => { }, + createInstance: () => ({ + // eslint-disable-next-line @typescript-eslint/no-empty-function + getItem: () => { }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + setItem: () => { } + }) +})); + +const queries: ISampleQuery[] = []; +describe('Samples Cache should', () => { + it('return the same queries after queries added', async () => { + expect(queries.length).toBe(0); + queries.push( + { + category: 'Getting Started', + method: 'GET', + humanName: 'my profile', + requestUrl: '/v1.0/me', + docLink: 'https://learn.microsoft.com/en-us/graph/api/user-get', + skipTest: false + }, + ); + samplesCache.saveSamples(queries); + const samplesData = await samplesCache.readSamples(); + expect(samplesData).not.toBe(queries); + }) +}) diff --git a/src/modules/cache/samples.cache.ts b/src/modules/cache/samples.cache.ts new file mode 100644 index 0000000000..33989a0875 --- /dev/null +++ b/src/modules/cache/samples.cache.ts @@ -0,0 +1,29 @@ +import localforage from 'localforage'; +import { ISampleQuery } from '../../types/query-runner'; + +const samplesStorage = localforage.createInstance({ + storeName: 'samples', + name: 'GE_V4' +}); + +const SAMPLE_KEY = 'sample-queries'; + +export const samplesCache = (function () { + + const saveSamples = async (queries: ISampleQuery[]) => { + await samplesStorage.setItem(SAMPLE_KEY, JSON.stringify(queries)); + } + + const readSamples = async (): Promise => { + const items = await samplesStorage.getItem(SAMPLE_KEY) as string; + if (items) { + return JSON.parse(items); + } + return []; + } + + return { + saveSamples, + readSamples + } +})(); \ No newline at end of file From d8340d20f2db2ceeabc7afd23408710777c5e05a Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Wed, 16 Nov 2022 13:55:33 +0300 Subject: [PATCH 3/5] utilise sample cache --- src/app/middleware/localStorageMiddleware.ts | 18 +++++++++++++++--- .../actions/samples-action-creators.ts | 8 +++++++- src/app/services/reducers/samples-reducers.ts | 4 +--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/app/middleware/localStorageMiddleware.ts b/src/app/middleware/localStorageMiddleware.ts index c86a4bc2bf..213c5cb410 100644 --- a/src/app/middleware/localStorageMiddleware.ts +++ b/src/app/middleware/localStorageMiddleware.ts @@ -1,10 +1,22 @@ +import { samplesCache } from '../../modules/cache/samples.cache'; import { saveTheme } from '../../themes/theme-utils'; import { AppAction } from '../../types/action'; -import { CHANGE_THEME_SUCCESS } from '../services/redux-constants'; +import { + CHANGE_THEME_SUCCESS, SAMPLES_FETCH_SUCCESS +} from '../services/redux-constants'; const localStorageMiddleware = () => (next: any) => (action: AppAction) => { - if (action.type === CHANGE_THEME_SUCCESS) { - saveTheme(action.response); + switch (action.type) { + case CHANGE_THEME_SUCCESS: + saveTheme(action.response); + break; + + case SAMPLES_FETCH_SUCCESS: + samplesCache.saveSamples(action.response); + break; + + default: + break; } return next(action); }; diff --git a/src/app/services/actions/samples-action-creators.ts b/src/app/services/actions/samples-action-creators.ts index 92318ba26f..b9f80a854e 100644 --- a/src/app/services/actions/samples-action-creators.ts +++ b/src/app/services/actions/samples-action-creators.ts @@ -1,7 +1,9 @@ import { geLocale } from '../../../appLocale'; +import { samplesCache } from '../../../modules/cache/samples.cache'; import { AppDispatch } from '../../../store'; import { AppAction } from '../../../types/action'; import { IRequestOptions } from '../../../types/request'; +import { queries } from '../../views/sidebar/sample-queries/queries'; import { SAMPLES_FETCH_ERROR, SAMPLES_FETCH_PENDING, @@ -55,7 +57,11 @@ export function fetchSamples() { const res = await response.json(); return dispatch(fetchSamplesSuccess(res.sampleQueries)); } catch (error) { - return dispatch(fetchSamplesError({ error })); + let cachedSamples = await samplesCache.readSamples(); + if (cachedSamples.length === 0) { + cachedSamples = queries; + } + return dispatch(fetchSamplesError(cachedSamples)); } }; } diff --git a/src/app/services/reducers/samples-reducers.ts b/src/app/services/reducers/samples-reducers.ts index 007bd07ab9..5b6d0eed51 100644 --- a/src/app/services/reducers/samples-reducers.ts +++ b/src/app/services/reducers/samples-reducers.ts @@ -1,5 +1,4 @@ import { AppAction } from '../../../types/action'; -import { queries } from '../../views/sidebar/sample-queries/queries'; import { SAMPLES_FETCH_ERROR, SAMPLES_FETCH_PENDING, SAMPLES_FETCH_SUCCESS } from '../redux-constants'; const initialState = { @@ -25,8 +24,7 @@ export function samples(state = initialState, action: AppAction): any { return { ...state, pending: false, - queries, - error: action.response + queries: action.response }; default: return state; From 83129f9f7ae0e6506fdc2073e0b86ad3e3cd7fbd Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Wed, 16 Nov 2022 14:17:51 +0300 Subject: [PATCH 4/5] ensure user sees cached samples message --- src/app/services/reducers/samples-reducers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/services/reducers/samples-reducers.ts b/src/app/services/reducers/samples-reducers.ts index 5b6d0eed51..62e2d7121e 100644 --- a/src/app/services/reducers/samples-reducers.ts +++ b/src/app/services/reducers/samples-reducers.ts @@ -24,7 +24,8 @@ export function samples(state = initialState, action: AppAction): any { return { ...state, pending: false, - queries: action.response + queries: action.response, + error: 'error' }; default: return state; From 7dccf6651c593b6ed0a5f02fcf985d325a380838 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Wed, 16 Nov 2022 14:41:26 +0300 Subject: [PATCH 5/5] fix reducer test --- src/app/services/reducers/samples-reducers.spec.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/services/reducers/samples-reducers.spec.ts b/src/app/services/reducers/samples-reducers.spec.ts index 736b4199de..7c86f0030a 100644 --- a/src/app/services/reducers/samples-reducers.spec.ts +++ b/src/app/services/reducers/samples-reducers.spec.ts @@ -54,15 +54,11 @@ describe('Samples Reducer', () => { error: null }; - const mockResponse = { - status: 400 - }; - const newState = { ...initialState }; - newState.error = mockResponse; + newState.error = 'error'; newState.queries = queries; - const queryAction = { type: SAMPLES_FETCH_ERROR, response: mockResponse }; + const queryAction = { type: SAMPLES_FETCH_ERROR, response: queries }; const state = samples(initialState, queryAction); expect(state).toEqual(newState);