Skip to content

Commit

Permalink
Migrate from localStorage to indexed DB. (microsoft#886)
Browse files Browse the repository at this point in the history
* Replace localstorage with WebStorageManager. (microsoft#882)

* Use singleton and merge data from localStorage.

* Add toast for displaying error messages.

* Fix build error.

* Fix typescript warning.

Co-authored-by: SimoTw <[email protected]>
  • Loading branch information
buddhawang and simotw authored Mar 9, 2021
1 parent 1b48fd2 commit 67e1903
Show file tree
Hide file tree
Showing 17 changed files with 174 additions and 79 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 53 additions & 0 deletions src/common/webStorage.ts
Original file line number Diff line number Diff line change
@@ -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();
58 changes: 30 additions & 28 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
})();
14 changes: 7 additions & 7 deletions src/react/components/pages/appSettings/appSettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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");
Expand All @@ -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");

Expand Down Expand Up @@ -106,14 +106,14 @@ 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(),
connections: [],
recentProjects: [],
};

return createReduxStore(initialState);
return await createReduxStore(initialState);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
4 changes: 2 additions & 2 deletions src/react/components/pages/editorPage/editorPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -529,15 +529,15 @@ 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(),
connections: [],
recentProjects: [project],
};

return createReduxStore(initialState);
return await createReduxStore(initialState);
}

async function waitForSelectedAsset(wrapper: ReactWrapper) {
Expand Down
8 changes: 4 additions & 4 deletions src/react/components/pages/homepage/homePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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(),
Expand All @@ -183,6 +183,6 @@ describe("Homepage Component", () => {
appError: null,
};

return createReduxStore(initialState);
return await createReduxStore(initialState);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
});

Expand All @@ -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", () => {
Expand All @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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) });
}
Expand Down
11 changes: 6 additions & 5 deletions src/react/components/pages/train/trainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/react/components/shell/mainContentRouter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
8 changes: 8 additions & 0 deletions src/redux/actions/connectionActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
};
Expand Down
Loading

0 comments on commit 67e1903

Please sign in to comment.