Migrate from localStorage to indexed DB. (#886)

* Replace localstorage with WebStorageManager. (#882)

* Use singleton and merge data from localStorage.

* Add toast for displaying error messages.

* Fix build error.

* Fix typescript warning.

Co-authored-by: SimoTw <36868079+SimoTw@users.noreply.github.com>
This commit is contained in:
Buddha Wang 2021-03-09 16:39:49 +08:00 коммит произвёл GitHub
Родитель 1b48fd220b
Коммит 67e19038fd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 174 добавлений и 79 удалений

Просмотреть файл

@ -25,6 +25,7 @@
"exif-js": "^2.3.0",
"file-type": "^14.6.2",
"jszip": "^3.5.0",
"localforage": "^1.9.0",
"lodash": "^4.17.20",
"ol": "^5.3.3",
"path-to-regexp": "^6.2.0",

53
src/common/webStorage.ts Normal file
Просмотреть файл

@ -0,0 +1,53 @@
import * as localforage from "localforage";
import { toast } from "react-toastify";
class WebStorage {
private isStorageReady: boolean = false;
constructor() {
localforage.config({
name: "FOTT-webStorage",
driver: [localforage.INDEXEDDB, localforage.LOCALSTORAGE],
});
}
private async waitForStorageReady() {
if (!this.isStorageReady) {
try {
await localforage.ready();
this.isStorageReady = true;
} catch (error) {
toast.error(`No usable storage driver is found.`);
}
}
}
public async getItem(key: string) {
try {
await this.waitForStorageReady();
return await localforage.getItem(key);
} catch (err) {
toast.error(`WebStorage getItem("${key}") error: ${err}`);
}
}
public async setItem(key: string, value: any) {
try {
await this.waitForStorageReady();
await localforage.setItem(key, value);
} catch (err) {
toast.error(`WebStorage setItem("${key}") error: ${err}`);
}
}
public async removeItem(key: string) {
try {
await this.waitForStorageReady();
await localforage.removeItem(key);
} catch (err) {
toast.error(`WebStorage removeItem("${key}") error: ${err}`);
}
}
}
export const webStorage = new WebStorage();

Просмотреть файл

@ -16,36 +16,38 @@ import { registerIcons } from "./registerIcons";
import registerProviders from "./registerProviders";
import registerMixins from "./registerMixins";
registerIcons();
registerMixins();
registerProviders();
const defaultState: IApplicationState = initialState;
const store = createReduxStore(defaultState, true);
(async () => {
registerIcons();
registerMixins();
registerProviders();
const defaultState: IApplicationState = initialState;
const store = await createReduxStore(defaultState, true);
let noFocusOutline = true;
document.body.classList.add("no-focus-outline");
let noFocusOutline = true;
document.body.classList.add("no-focus-outline");
document.body.addEventListener("mousedown", () => {
if (!noFocusOutline) {
noFocusOutline = true;
document.body.classList.add("no-focus-outline");
}
});
document.body.addEventListener("mousedown", () => {
if (!noFocusOutline) {
noFocusOutline = true;
document.body.classList.add("no-focus-outline");
}
});
document.body.addEventListener("keydown", (event) => {
if (event.keyCode === 9 && noFocusOutline) {
noFocusOutline = false;
document.body.classList.remove("no-focus-outline");
}
});
document.body.addEventListener("keydown", (event) => {
if (event.keyCode === 9 && noFocusOutline) {
noFocusOutline = false;
document.body.classList.remove("no-focus-outline");
}
});
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>
, document.getElementById("rootdiv"));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById("rootdiv"));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
})();

Просмотреть файл

@ -35,8 +35,8 @@ describe("App Settings Page", () => {
toast.success = jest.fn(() => 2);
});
it("renders correctly", () => {
const store = createStore();
it("renders correctly", async () => {
const store = await createStore();
const wrapper = createComponent(store);
expect(wrapper.find(AppSettingsForm).length).toEqual(1);
expect(wrapper.find("button#toggleDevTools").length).toEqual(1);
@ -45,7 +45,7 @@ describe("App Settings Page", () => {
it("Saves app settings when click on save button", async () => {
const appSettings = MockFactory.appSettings();
const store = createStore(appSettings);
const store = await createStore(appSettings);
const props = createProps();
const saveAppSettingsSpy = jest.spyOn(props.actions, "saveAppSettings");
const goBackSpy = jest.spyOn(props.history, "goBack");
@ -59,8 +59,8 @@ describe("App Settings Page", () => {
expect(goBackSpy).toBeCalled();
});
it("Navigates the user back to the previous page on cancel", () => {
const store = createStore();
it("Navigates the user back to the previous page on cancel", async () => {
const store = await createStore();
const props = createProps();
const goBackSpy = jest.spyOn(props.history, "goBack");
@ -106,7 +106,7 @@ describe("App Settings Page", () => {
};
}
function createStore(appSettings: IAppSettings = null): Store<IApplicationState, AnyAction> {
async function createStore(appSettings: IAppSettings = null): Promise<Store<IApplicationState, AnyAction>> {
const initialState: IApplicationState = {
currentProject: null,
appSettings: appSettings || MockFactory.appSettings(),
@ -114,6 +114,6 @@ describe("App Settings Page", () => {
recentProjects: [],
};
return createReduxStore(initialState);
return await createReduxStore(initialState);
}
});

Просмотреть файл

@ -320,6 +320,6 @@ function createProps(route: string): IConnectionPageProps {
};
}
function createStore(state?: IApplicationState): Store<any, AnyAction> {
return createReduxStore(state);
async function createStore(state?: IApplicationState): Promise<Store<any, AnyAction>> {
return await createReduxStore(state);
}

Просмотреть файл

@ -529,7 +529,7 @@ describe("Editor Page Component", () => {
});
});
function createStore(project: IProject, setCurrentProject: boolean = false): Store<any, AnyAction> {
async function createStore(project: IProject, setCurrentProject: boolean = false): Promise<Store<any, AnyAction>> {
const initialState: IApplicationState = {
currentProject: setCurrentProject ? project : null,
appSettings: MockFactory.appSettings(),
@ -537,7 +537,7 @@ function createStore(project: IProject, setCurrentProject: boolean = false): Sto
recentProjects: [project],
};
return createReduxStore(initialState);
return await createReduxStore(initialState);
}
async function waitForSelectedAsset(wrapper: ReactWrapper) {

Просмотреть файл

@ -53,12 +53,12 @@ describe("Homepage Component", () => {
toast.info = jest.fn(() => 2);
});
beforeEach(() => {
beforeEach(async () => {
const projectServiceMock = ProjectService as jest.Mocked<typeof ProjectService>;
projectServiceMock.prototype.load = jest.fn((project) => Promise.resolve(project));
projectServiceMock.prototype.delete = jest.fn(() => Promise.resolve());
store = createStore(recentProjects);
store = await createStore(recentProjects);
props = createProps();
deleteProjectSpy = jest.spyOn(props.actions, "deleteProject");
closeProjectSpy = jest.spyOn(props.actions, "closeProject");
@ -174,7 +174,7 @@ describe("Homepage Component", () => {
};
}
function createStore(recentProjects: IProject[]): Store<IApplicationState, AnyAction> {
async function createStore(recentProjects: IProject[]): Promise<Store<IApplicationState, AnyAction>> {
const initialState: IApplicationState = {
currentProject: null,
appSettings: MockFactory.appSettings(),
@ -183,6 +183,6 @@ describe("Homepage Component", () => {
appError: null,
};
return createReduxStore(initialState);
return await createReduxStore(initialState);
}
});

Просмотреть файл

@ -5,6 +5,7 @@ import { mount, ReactWrapper } from "enzyme";
import React from "react";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import localforage from "localforage";
import MockFactory from "../../../../common/mockFactory";
import createReduxStore from "../../../../redux/store/store";
import ProjectSettingsPage, { IProjectSettingsPageProps, IProjectSettingsPageState } from "./projectSettingsPage";
@ -134,7 +135,7 @@ describe("Project settings page", () => {
securityToken: `${project.name} Token`,
});
expect(localStorage.removeItem).toBeCalledWith("projectForm");
expect(localforage.removeItem).toBeCalledWith("projectForm");
});
describe("project does not exists", () => {
@ -181,7 +182,7 @@ describe("Project settings page", () => {
.find(ProjectSettingsPage)
.childAt(0) as ReactWrapper<IProjectSettingsPageProps, IProjectSettingsPageState>;
expect(localStorage.getItem).toBeCalledWith("projectForm");
expect(localforage.getItem).toBeCalledWith("projectForm");
expect(projectSettingsPage.state().project).toEqual(partialProject);
});
@ -195,7 +196,7 @@ describe("Project settings page", () => {
const projectForm = wrapper.find(ProjectForm) as ReactWrapper<IProjectFormProps>;
projectForm.props().onChange(partialProject);
expect(localStorage.setItem).toBeCalledWith("projectForm", JSON.stringify(partialProject));
expect(localforage.setItem).toBeCalledWith("projectForm", JSON.stringify(partialProject));
});
it("Does NOT store empty project in local storage", () => {
@ -209,7 +210,7 @@ describe("Project settings page", () => {
const projectForm = wrapper.find(ProjectForm) as ReactWrapper<IProjectFormProps>;
projectForm.props().onChange(emptyProject);
expect(localStorage.setItem).not.toBeCalled();
expect(localforage.setItem).not.toBeCalled();
});
});
});

Просмотреть файл

@ -161,8 +161,8 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
}
}
private newProjectSetting = (): void => {
const projectJson = getStorageItem(constants.projectFormTempKey);
private newProjectSetting = async (): Promise<void> => {
const projectJson = await getStorageItem(constants.projectFormTempKey);
if (projectJson) {
this.setState({ project: JSON.parse(projectJson) });
}

Просмотреть файл

@ -29,6 +29,7 @@ import TrainPanel from "./trainPanel";
import {ITrainRecordProps} from "./trainRecord";
import TrainTable from "./trainTable";
import { getAPIVersion } from "../../../../common/utils";
import { webStorage } from "../../../../common/webStorage";
export interface ITrainPageProps extends RouteComponentProps, React.Props<TrainPage> {
connections: IConnection[];
@ -258,11 +259,11 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
);
}
private checkAndUpdateInputsInLocalStorage = (projectId: string) => {
const storedTrainInputs = JSON.parse(window.localStorage.getItem("trainPage_inputs"));
private checkAndUpdateInputsInLocalStorage = async (projectId: string) => {
const storedTrainInputs = JSON.parse(await webStorage.getItem("trainPage_inputs") as string);
if (storedTrainInputs?.id !== projectId) {
window.localStorage.removeItem("trainPage_inputs");
await webStorage.removeItem("trainPage_inputs");
UseLocalStorage.setItem("trainPage_inputs", "projectId", projectId);
}
@ -339,8 +340,8 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
currTrainRecord: this.getProjectTrainRecord(),
modelName: "",
}));
// reset localStorage successful train process
localStorage.setItem("trainPage_inputs", "{}");
// reset webStorage successful train process
await webStorage.setItem("trainPage_inputs", "{}");
}).catch((err) => {
this.setState({
isTraining: false,

Просмотреть файл

@ -55,6 +55,6 @@ describe("Main Content Router", () => {
});
});
function createStore(state?: IApplicationState): Store<any, AnyAction> {
return createReduxStore(state);
async function createStore(state?: IApplicationState): Promise<Store<any, AnyAction>> {
return await createReduxStore(state);
}

Просмотреть файл

@ -6,6 +6,8 @@ import { ActionTypes } from "./actionTypes";
import { IPayloadAction, createPayloadAction } from "./actionCreators";
import { Dispatch } from "redux";
import ConnectionService from "../../services/connectionService";
import { webStorage } from "../../common/webStorage";
import { constants } from "../../common/constants";
/**
* Actions to be performed in relation to connections
@ -35,6 +37,12 @@ export function saveConnection(connection: IConnection): (dispatch: Dispatch) =>
return async (dispatch: Dispatch) => {
const connectionService = new ConnectionService();
await connectionService.save(connection);
const projectJson = await webStorage.getItem(constants.projectFormTempKey);
if (projectJson) {
const project = JSON.parse(projectJson as string);
project.sourceConnection = connection;
await webStorage.setItem(constants.projectFormTempKey, JSON.stringify(project));
}
dispatch(saveConnectionAction(connection));
return Promise.resolve(connection);
};

Просмотреть файл

@ -3,6 +3,7 @@
import { Middleware, Dispatch, AnyAction, MiddlewareAPI } from "redux";
import { constants } from "../../common/constants";
import { webStorage } from "../../common/webStorage";
export interface ILocalStorageMiddlewareOptions {
paths: string[];
@ -24,26 +25,44 @@ export function createLocalStorage(config: ILocalStorageMiddlewareOptions): Midd
};
}
export function mergeInitialState(state: any, paths: string[]) {
export async function mergeInitialState(state: any, paths: string[]) {
const initialState = { ...state };
paths.forEach((path) => {
const json = getStorageItem(path);
for (const path of paths) {
const isArray = Array.isArray(initialState[path]);
let legacyItem = isArray ? [] : {};
let item = isArray ? [] : {};
const json = (await getStorageItem(path)) as string;
if (json) {
initialState[path] = JSON.parse(json);
item = JSON.parse(json);
}
});
const legacyJson = localStorage.getItem(`${constants.version}_${path}`);
if (legacyJson) {
legacyItem = JSON.parse(legacyJson);
localStorage.removeItem(`${constants.version}_${path}`);
}
if (isArray) {
initialState[path] = [].concat(legacyItem, item);
} else {
initialState[path] = { ...item, ...legacyItem };
}
}
return initialState;
}
export function setStorageItem(key: string, value: string) {
localStorage.setItem(`${constants.version}_${key}`, value);
webStorage.setItem(`${constants.version}_${key}`, value);
}
export function getStorageItem(key: string) {
return localStorage.getItem(`${constants.version}_${key}`);
export async function getStorageItem(key: string): Promise<string> {
return (await webStorage.getItem(`${constants.version}_${key}`)) as string;
}
export function removeStorageItem(key: string) {
return localStorage.removeItem(`${constants.version}_${key}`);
return webStorage.removeItem(`${constants.version}_${key}`);
}

Просмотреть файл

@ -1,11 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { constants } from "../../common/constants";
import { ActionTypes } from "../actions/actionTypes";
import { IConnection } from "../../models/applicationState";
import { AnyAction } from "../actions/actionCreators";
import { getStorageItem, setStorageItem } from "../middleware/localStorage";
/**
* Reducer for application connections. Actions handled:
@ -22,12 +20,6 @@ export const reducer = (state: IConnection[] = [], action: AnyAction): IConnecti
switch (action.type) {
case ActionTypes.SAVE_CONNECTION_SUCCESS:
const projectJson = getStorageItem(constants.projectFormTempKey);
if (projectJson) {
const project = JSON.parse(projectJson);
project.sourceConnection = action.payload;
setStorageItem(constants.projectFormTempKey, JSON.stringify(project));
}
return [
{ ...action.payload },
...state.filter((connection) => connection.id !== action.payload.id),

Просмотреть файл

@ -13,9 +13,9 @@ import { Env } from "../../common/environment";
* @param initialState - Initial state of application
* @param useLocalStorage - Whether or not to use localStorage middleware
*/
export default function createReduxStore(
export default async function createReduxStore(
initialState?: IApplicationState,
useLocalStorage: boolean = false): Store {
useLocalStorage: boolean = false): Promise<Store> {
const paths: string[] = ["appSettings", "connections", "recentProjects", "prebuiltSettings"];
let middlewares = [thunk];
@ -39,9 +39,11 @@ export default function createReduxStore(
];
}
const mergedInitialState = await mergeInitialState(initialState, paths);
return createStore(
rootReducer,
useLocalStorage ? mergeInitialState(initialState, paths) : initialState,
useLocalStorage ? mergedInitialState : initialState,
applyMiddleware(...middlewares),
);
}

Просмотреть файл

@ -1,7 +1,9 @@
import { webStorage } from "../common/webStorage";
export default class UseLocalStorage {
public static setItem(name: string, key: string, value: string) {
public static setItem = async (name: string, key: string, value: string) => {
// Get the existing data
const existingData = window.localStorage.getItem(name);
const existingData = await webStorage.getItem(name) as string;
// If no existing data, create an {}
// Otherwise, convert the localStorage string to an {}
@ -11,6 +13,6 @@ export default class UseLocalStorage {
newLsData[key] = value;
// Save back to localStorage
window.localStorage.setItem(name, JSON.stringify(newLsData));
webStorage.setItem(name, JSON.stringify(newLsData));
}
}

Просмотреть файл

@ -8012,6 +8012,13 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
dependencies:
immediate "~3.0.5"
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
@ -8086,6 +8093,13 @@ loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4
emojis-list "^3.0.0"
json5 "^1.0.1"
localforage@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1"
integrity sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==
dependencies:
lie "3.1.1"
localized-strings@^0.2.0:
version "0.2.4"
resolved "https://registry.yarnpkg.com/localized-strings/-/localized-strings-0.2.4.tgz#9d61c06b60cc7b5edf7c46e6c7f2d1ecb84aeb2c"