Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/component/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';

const crashStyle = {
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '24px',
backgroundColor: 'var(--bg-primary)',
color: 'var(--text-primary)',
textAlign: 'center',
};

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError() {
return { hasError: true };
}

render() {
const { hasError } = this.state;
const { children } = this.props;

if (hasError) {
return (
<div style={crashStyle}>
<div>
<h2 style={{ marginBottom: '8px' }}>Something went wrong</h2>
<p style={{ marginBottom: '16px', opacity: 0.9 }}>
The app hit an unexpected error. Reload to continue.
</p>
<button
type="button"
className="btn btn-primary"
onClick={() => window.location.reload()}
>
Reload
</button>
</div>
</div>
);
}

return children;
}
}

export default ErrorBoundary;
60 changes: 51 additions & 9 deletions src/graph-builder/local-storage-manager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import { toast } from 'react-toastify';

const encodeBase64 = (value) => {
const bytes = new TextEncoder().encode(value);
let binary = '';
bytes.forEach((byte) => {
binary += String.fromCharCode(byte);
});
return window.btoa(binary);
};

const decodeBase64 = (value) => {
const binary = window.atob(value);
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
};

const parseStoredJson = (raw) => {
try {
return JSON.parse(decodeBase64(raw));
} catch (e) {
return null;
}
};

const localStorageGet = (key) => {
try {
return window.localStorage.getItem(key);
Expand Down Expand Up @@ -27,11 +50,16 @@ const localStorageRemove = (key) => {

const getSet = (ALL_GRAPHS) => {
if (!localStorageGet(ALL_GRAPHS)) {
localStorageSet(ALL_GRAPHS, window.btoa(JSON.stringify([])));
localStorageSet(ALL_GRAPHS, encodeBase64(JSON.stringify([])));
}
const raw = localStorageGet(ALL_GRAPHS);
if (!raw) return new Set();
return new Set(JSON.parse(window.atob(raw)));
const parsed = parseStoredJson(raw);
if (!Array.isArray(parsed)) {
localStorageSet(ALL_GRAPHS, encodeBase64(JSON.stringify([])));
return new Set();
}
return new Set(parsed);
};

const localStorageManager = {
Expand All @@ -41,24 +69,29 @@ const localStorageManager = {
allgs: getSet(window.btoa('ALL_GRAPHS')),

saveAllgs() {
localStorageSet(this.ALL_GRAPHS, window.btoa(JSON.stringify(Array.from(this.allgs))));
localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify(Array.from(this.allgs))));
},

addEmptyIfNot() {
if (!localStorageGet(this.ALL_GRAPHS)) {
localStorageSet(this.ALL_GRAPHS, window.btoa(JSON.stringify([])));
localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify([])));
}
},

get(id) {
const raw = localStorageGet(id);
if (raw === null) return null;
return JSON.parse(window.atob(raw));
const parsed = parseStoredJson(raw);
if (parsed === null) {
localStorageRemove(id);
return null;
}
return parsed;
},
save(id, graphContent) {
this.addGraph(id);
const serializedJson = JSON.stringify(graphContent);
localStorageSet(id, window.btoa(serializedJson));
localStorageSet(id, encodeBase64(serializedJson));
},
remove(id) {
if (this.allgs.delete(id)) this.saveAllgs();
Expand All @@ -72,16 +105,25 @@ const localStorageManager = {
getAllGraphs() {
const raw = localStorageGet(this.ALL_GRAPHS);
if (!raw) return [];
return JSON.parse(window.atob(raw));
const parsed = parseStoredJson(raw);
if (!Array.isArray(parsed)) {
localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify([])));
return [];
}
return parsed;
},
addToFront(id) {
if (this.allgs.has(id)) return;
this.allgs.add(id);
const raw = localStorageGet(this.ALL_GRAPHS);
if (!raw) return;
const Garr = JSON.parse(window.atob(raw));
const Garr = parseStoredJson(raw);
if (!Array.isArray(Garr)) {
this.saveAllgs();
return;
}
Garr.unshift(id);
localStorageSet(this.ALL_GRAPHS, window.btoa(JSON.stringify(Garr)));
localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify(Garr)));
},
getAuthorName() {
return localStorageGet(this.AUTHOR_NAME) || '';
Expand Down
5 changes: 4 additions & 1 deletion src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import ErrorBoundary from './component/ErrorBoundary';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
<React.StrictMode>
<App />
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root'),
);
Expand Down
29 changes: 12 additions & 17 deletions src/serverCon/crud_http.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import ec from './config';

function readTextOrThrow(x) {
return x.text().then((text) => {
if (x.ok) return text;
throw new Error(text || `Request failed with status ${x.status}`);
});
}

function getGraph(serverID) {
return fetch(`${ec.baseURL + ec.getGraph(serverID)}`).then((x) => x.text());
return fetch(`${ec.baseURL + ec.getGraph(serverID)}`).then((x) => readTextOrThrow(x));
}

function getGraphWithHashCheck(serverID, latestHash) {
return fetch(`${ec.baseURL + ec.getGraph(serverID)}`, {
headers: {
'X-Latest-Hash': latestHash,
},
}).then((x) => {
if (x.status === 200) return x.text();
return Promise.reject(x.text());
});
}).then((x) => readTextOrThrow(x));
}

function postGraph(graphml) {
Expand All @@ -22,10 +26,7 @@ function postGraph(graphml) {
},
method: 'POST',
body: graphml,
}).then((x) => {
if (!x.ok) return Promise.reject(x.text());
return x.text();
});
}).then((x) => readTextOrThrow(x));
}

function updateGraph(serverID, graphml) {
Expand All @@ -35,10 +36,7 @@ function updateGraph(serverID, graphml) {
'Content-Type': 'application/xml',
},
body: graphml,
}).then((x) => {
if (!x.ok) return Promise.reject(x.text());
return x.text();
});
}).then((x) => readTextOrThrow(x));
}

function forceUpdateGraph(serverID, graphml) {
Expand All @@ -48,10 +46,7 @@ function forceUpdateGraph(serverID, graphml) {
'Content-Type': 'application/xml',
},
body: graphml,
}).then((x) => {
if (!x.ok) return Promise.reject(x.text());
return x.text();
});
}).then((x) => readTextOrThrow(x));
}

export {
Expand Down