summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
Diffstat (limited to 'gui')
-rw-r--r--gui/package-lock.json44
-rw-r--r--gui/package.json2
-rw-r--r--gui/src/renderer/app.tsx20
-rw-r--r--gui/src/renderer/lib/history.ts112
-rw-r--r--gui/src/renderer/redux/store.ts2
-rw-r--r--gui/test/history.spec.ts168
6 files changed, 295 insertions, 53 deletions
diff --git a/gui/package-lock.json b/gui/package-lock.json
index 249ef3ddda..9507dcc279 100644
--- a/gui/package-lock.json
+++ b/gui/package-lock.json
@@ -604,9 +604,9 @@
"dev": true
},
"@types/history": {
- "version": "4.7.5",
- "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz",
- "integrity": "sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==",
+ "version": "4.7.8",
+ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz",
+ "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==",
"dev": true
},
"@types/hoist-non-react-statics": {
@@ -6872,18 +6872,6 @@
"integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg==",
"dev": true
},
- "history": {
- "version": "4.7.2",
- "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz",
- "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==",
- "requires": {
- "invariant": "^2.2.1",
- "loose-envify": "^1.2.0",
- "resolve-pathname": "^2.2.0",
- "value-equal": "^0.4.0",
- "warning": "^3.0.0"
- }
- },
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -7269,14 +7257,6 @@
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true
},
- "invariant": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
- "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
- "requires": {
- "loose-envify": "^1.0.0"
- }
- },
"invert-kv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
@@ -10616,11 +10596,6 @@
"value-or-function": "^3.0.0"
}
},
- "resolve-pathname": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz",
- "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg=="
- },
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@@ -12869,11 +12844,6 @@
"spdx-expression-parse": "^3.0.0"
}
},
- "value-equal": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
- "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw=="
- },
"value-or-function": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz",
@@ -12963,14 +12933,6 @@
}
}
},
- "warning": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
- "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
- "requires": {
- "loose-envify": "^1.0.0"
- }
- },
"warning-symbol": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/warning-symbol/-/warning-symbol-0.1.0.tgz",
diff --git a/gui/package.json b/gui/package.json
index 22593150b3..734b3f8a66 100644
--- a/gui/package.json
+++ b/gui/package.json
@@ -19,7 +19,6 @@
"electron-log": "^4.1.1",
"gettext-parser": "^4.0.3",
"google-protobuf": "^4.0.0-rc.2",
- "history": "^4.6.1",
"linux-app-list": "^1.0.1",
"mkdirp": "^1.0.3",
"moment": "^2.24.0",
@@ -47,6 +46,7 @@
"@types/enzyme-adapter-react-16": "^1.0.3",
"@types/gettext-parser": "^4.0.0",
"@types/google-protobuf": "^3.7.2",
+ "@types/history": "^4.7.8",
"@types/mkdirp": "^1.0.0",
"@types/mocha": "^5.2.6",
"@types/node": "^10.12.3",
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index b7c9b1a187..4d7b9e9931 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -5,7 +5,6 @@ import {
} from 'connected-react-router';
import { ipcRenderer, shell, webFrame } from 'electron';
import log from 'electron-log';
-import { createMemoryHistory } from 'history';
import * as React from 'react';
import { Provider } from 'react-redux';
import { bindActionCreators } from 'redux';
@@ -29,6 +28,7 @@ import { IpcRendererEventChannel, IRelayListPair } from '../shared/ipc-event-cha
import ISplitTunnelingApplication from '../shared/linux-split-tunneling-application';
import { getRendererLogFile, setupLogging } from '../shared/logging';
import consumePromise from '../shared/promise';
+import History from './lib/history';
import {
AccountToken,
@@ -76,8 +76,8 @@ const SUPPORTED_LOCALE_LIST = [
];
export default class AppRenderer {
- private memoryHistory = createMemoryHistory();
- private reduxStore = configureStore(this.memoryHistory);
+ private history = new History('/');
+ private reduxStore = configureStore(this.history);
private reduxActions = {
account: bindActionCreators(accountActions, this.reduxStore.dispatch),
connection: bindActionCreators(connectionActions, this.reduxStore.dispatch),
@@ -229,7 +229,7 @@ export default class AppRenderer {
return (
<AppContext.Provider value={{ app: this }}>
<Provider store={this.reduxStore}>
- <ConnectedRouter history={this.memoryHistory}>
+ <ConnectedRouter history={this.history}>
<ErrorBoundary>
<AppRoutes />
</ErrorBoundary>
@@ -450,7 +450,7 @@ export default class AppRenderer {
private redirectToConnect() {
// Redirect the user after some time to allow for the 'Logged in' screen to be visible
- this.loginTimer = global.setTimeout(() => this.memoryHistory.replace('/connect'), 1000);
+ this.loginTimer = global.setTimeout(() => this.history.resetWith('/connect'), 1000);
}
private loadTranslations(locale: string) {
@@ -530,12 +530,12 @@ export default class AppRenderer {
this.connectedToDaemon = true;
if (this.settings.accountToken) {
- this.memoryHistory.replace('/connect');
+ this.history.resetWith('/connect');
// try to autoconnect the tunnel
await this.autoConnect();
} else {
- this.memoryHistory.replace('/login');
+ this.history.resetWith('/login');
// show window when account is not set
ipcRenderer.send('show-window');
@@ -548,7 +548,7 @@ export default class AppRenderer {
this.connectedToDaemon = false;
if (error && wasConnected) {
- this.memoryHistory.replace('/');
+ this.history.resetWith('/');
}
}
@@ -660,12 +660,12 @@ export default class AppRenderer {
clearTimeout(this.loginTimer);
}
reduxAccount.loggedOut();
- this.memoryHistory.replace('/login');
+ this.history.resetWith('/login');
} else if (newAccount && oldAccount !== newAccount && !this.doingLogin) {
reduxAccount.updateAccountToken(newAccount);
reduxAccount.loggedIn();
if (!oldAccount) {
- this.memoryHistory.replace('/connect');
+ this.history.resetWith('/connect');
}
}
diff --git a/gui/src/renderer/lib/history.ts b/gui/src/renderer/lib/history.ts
new file mode 100644
index 0000000000..b726e72bbb
--- /dev/null
+++ b/gui/src/renderer/lib/history.ts
@@ -0,0 +1,112 @@
+import { Location, Action, LocationListener, LocationDescriptor } from 'history';
+
+// It currently isn't possible to implement this correctly with support for a generic state. State
+// can be added as a generic type (<S = unknown>) after this issue has been resolved:
+// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/49060
+type S = unknown;
+export default class History {
+ private listeners: LocationListener<S>[] = [];
+ private entries: Location<S>[];
+ private index = 0;
+ private lastAction: Action = 'POP';
+
+ public constructor(location: string | Location<S>, state?: S) {
+ this.entries = [this.createLocation(location, state)];
+ }
+
+ public get location(): Location<S> {
+ return this.entries[this.index];
+ }
+
+ public get length(): number {
+ return this.entries.length;
+ }
+
+ public get action(): Action {
+ return this.lastAction;
+ }
+
+ public push = (nextLocation: LocationDescriptor<S>, nextState?: S) => {
+ const location = this.createLocation(nextLocation, nextState);
+ this.lastAction = 'PUSH';
+ this.index += 1;
+ this.entries.splice(this.index, this.entries.length - this.index, location);
+ this.notify();
+ };
+
+ public replace = (nextLocation: LocationDescriptor<S>, nextState?: S) => {
+ this.entries[this.index] = this.createLocation(nextLocation, nextState);
+ this.lastAction = 'REPLACE';
+ this.notify();
+ };
+
+ public go = (n: number) => {
+ if (this.canGo(n)) {
+ this.index += n;
+ this.lastAction = 'POP';
+ this.notify();
+ }
+ };
+
+ public goBack = () => this.go(-1);
+ public goForward = () => this.go(1);
+
+ public reset = () => {
+ this.lastAction = 'POP';
+ this.index = 0;
+ this.notify();
+ };
+
+ public resetWith = (nextLocation: LocationDescriptor<S>, nextState?: S) => {
+ this.entries = [this.createLocation(nextLocation, nextState)];
+ this.lastAction = 'REPLACE';
+ this.index = 0;
+ this.notify();
+ };
+
+ public canGo(n: number) {
+ const nextIndex = this.index + n;
+ return nextIndex >= 0 && nextIndex < this.entries.length;
+ }
+
+ public listen(callback: LocationListener<S>) {
+ this.listeners.push(callback);
+ return () => (this.listeners = this.listeners.filter((listener) => listener !== callback));
+ }
+
+ public block(): () => void {
+ throw Error('Not implemented');
+ }
+
+ public createHref(): string {
+ throw Error('Not implemented');
+ }
+
+ private notify() {
+ this.listeners.forEach((listener) => listener(this.location, this.action));
+ }
+
+ private createLocation(location: LocationDescriptor<S>, state?: S): Location<S> {
+ if (typeof location === 'object') {
+ return {
+ pathname: location.pathname ?? this.location.pathname,
+ search: location.search ?? '',
+ hash: location.hash ?? '',
+ state: location.state,
+ key: location.key ?? this.getRandomKey(),
+ };
+ } else {
+ return {
+ pathname: location,
+ search: '',
+ hash: '',
+ state,
+ key: this.getRandomKey(),
+ };
+ }
+ }
+
+ private getRandomKey() {
+ return Math.random().toString(36).substr(8);
+ }
+}
diff --git a/gui/src/renderer/redux/store.ts b/gui/src/renderer/redux/store.ts
index bedaddf1bd..43ebc312ef 100644
--- a/gui/src/renderer/redux/store.ts
+++ b/gui/src/renderer/redux/store.ts
@@ -14,7 +14,7 @@ import userInterfaceReducer, { IUserInterfaceReduxState } from './userinterface/
import versionActions, { VersionAction } from './version/actions';
import versionReducer, { IVersionReduxState } from './version/reducers';
-import { History } from 'history';
+import History from '../lib/history';
export interface IReduxState {
account: IAccountReduxState;
diff --git a/gui/test/history.spec.ts b/gui/test/history.spec.ts
new file mode 100644
index 0000000000..1bf00dd4ba
--- /dev/null
+++ b/gui/test/history.spec.ts
@@ -0,0 +1,168 @@
+import { expect, spy } from 'chai';
+import { it, describe, beforeEach } from 'mocha';
+import History from '../src/renderer/lib/history';
+
+const BASE_PATH = '/';
+const FIRST_PATH = '/first-path';
+const SECOND_PATH = '/second-path';
+const THIRD_PATH = '/third-path';
+const FOURTH_PATH = '/fourth-path';
+const FIFTH_PATH = '/fifth-path';
+const SIXTH_PATH = '/sixth-path';
+
+describe('History', () => {
+ let history: History;
+
+ beforeEach(() => {
+ history = new History(BASE_PATH);
+ history.push(FIRST_PATH);
+ history.push(SECOND_PATH);
+ history.push(THIRD_PATH);
+ history.push(FOURTH_PATH);
+ });
+
+ it('should start at the correct location', () => {
+ const history2 = new History(BASE_PATH);
+
+ expect(history2.location.pathname).to.equal(BASE_PATH);
+ expect(history2.length).to.equal(1);
+ expect(history.location.pathname).to.equal(FOURTH_PATH);
+ expect(history.length).to.equal(5);
+ });
+
+ it('should go back', () => {
+ history.goBack();
+ expect(history.location.pathname).to.equal(THIRD_PATH);
+ expect(history.length).to.equal(5);
+ });
+
+ it('should go back three entries', () => {
+ history.go(-3);
+ expect(history.location.pathname).to.equal(FIRST_PATH);
+ expect(history.length).to.equal(5);
+ });
+
+ it('should go forward', () => {
+ history.go(-3);
+ history.goForward();
+ expect(history.location.pathname).to.equal(SECOND_PATH);
+ expect(history.length).to.equal(5);
+ });
+
+ it('should go forward two entries', () => {
+ history.go(-3);
+ history.go(2);
+ expect(history.location.pathname).to.equal(THIRD_PATH);
+ expect(history.length).to.equal(5);
+ });
+
+ it('should fail to go forward', () => {
+ history.goForward();
+ expect(history.location.pathname).to.equal(FOURTH_PATH);
+ expect(history.length).to.equal(5);
+ });
+
+ it('should push', () => {
+ history.push(FIFTH_PATH);
+ history.goBack();
+ expect(history.location.pathname).to.equal(FOURTH_PATH);
+ expect(history.length).to.equal(6);
+ });
+
+ it('should replace', () => {
+ history.replace(FIFTH_PATH);
+ history.goBack();
+ expect(history.location.pathname).to.equal(THIRD_PATH);
+ expect(history.length).to.equal(5);
+ });
+
+ it('should fail to go backwards further than base path', () => {
+ history.go(-5);
+ expect(history.location.pathname).to.equal(FOURTH_PATH);
+ expect(history.length).to.equal(5);
+ });
+
+ it('should go backward to base path', () => {
+ history.reset();
+ expect(history.location.pathname).to.equal(BASE_PATH);
+ expect(history.length).to.equal(5);
+ });
+
+ it('should reset entries with path', () => {
+ history.resetWith(THIRD_PATH);
+ expect(history.location.pathname).to.equal(THIRD_PATH);
+ expect(history.length).to.equal(1);
+
+ history.goBack();
+ expect(history.location.pathname).to.equal(THIRD_PATH);
+ expect(history.length).to.equal(1);
+
+ history.goForward();
+ expect(history.location.pathname).to.equal(THIRD_PATH);
+ expect(history.length).to.equal(1);
+ });
+
+ it('should fail to go forward after navigating', () => {
+ history.goBack();
+ history.push(FIFTH_PATH);
+ history.goForward();
+ expect(history.location.pathname).to.equal(FIFTH_PATH);
+
+ history.goBack();
+ history.replace(SIXTH_PATH);
+ history.goForward();
+ expect(history.location.pathname).to.equal(FIFTH_PATH);
+ });
+
+ it('should add a listener', () => {
+ const listenerA = spy();
+ history.listen(listenerA);
+ history.goBack();
+ history.goForward();
+
+ const listenerB = spy();
+ history.listen(listenerB);
+ history.reset();
+ history.push(FIRST_PATH);
+ history.replace(SECOND_PATH);
+
+ expect(listenerA).to.have.been.called.exactly(5);
+ expect(listenerB).to.have.been.called.exactly(3);
+ });
+
+ it('should remove a listener', () => {
+ const listenerA = spy();
+ const removeListenerA = history.listen(listenerA);
+ history.goBack();
+ history.goForward();
+
+ const listenerB = spy();
+ history.listen(listenerB);
+ history.reset();
+
+ removeListenerA();
+ history.push(FIRST_PATH);
+ history.replace(SECOND_PATH);
+
+ expect(listenerA).to.have.been.called.exactly(3);
+ expect(listenerB).to.have.been.called.exactly(3);
+ });
+
+ it('should only remove listener once', () => {
+ const listenerA = spy();
+ const removeListenerA = history.listen(listenerA);
+ history.goBack();
+
+ const listenerB = spy();
+ history.listen(listenerB);
+ history.goForward();
+
+ removeListenerA();
+ removeListenerA();
+
+ history.reset();
+
+ expect(listenerA).to.have.been.called.exactly(2);
+ expect(listenerB).to.have.been.called.exactly(2);
+ });
+});