Skip to content

Commit

Permalink
Moved the views into 1 PWA
Browse files Browse the repository at this point in the history
  • Loading branch information
ieb committed May 18, 2024
1 parent e58d1e6 commit 628843d
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 103 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ ToDo
* [-] ~~Support ESP32 on https for PWA~~ Too much effort, AsyncTCP support not there. Workaround: allow insecure content on localhost and load service worker from localhost.
* [x] Implement service worker cache
* [x] Implement Admin Login without relying on Browser, as BasicAuth popups in ServciceWorkers dont work offlin. On fixing found out: Explicitly setting headers works, but service workers use preflight on GET urls and are stricter about the cors headers than a direct fetch from a window context. Username and password are now stored in sessionStorage and logout works.
* [ ] Urls not easy to use to nav (Workaround, open in browser, change url, open in app)
* [x] Urls not easy to use to nav (Workaround, open in browser, change url, open in app)
* [x] Add ability to set the server url for data
* [ ] Persist history in local storage
* [ ] Verify target VMG down wind.
* [ ] Port eink displays
Expand Down
36 changes: 19 additions & 17 deletions ui/v2/src/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const html = htm.bind(h);
*/

class AdminRequest {
constructor() {
constructor(base) {
this.base = base;
this.loadCredentials();
}

Expand All @@ -23,7 +24,7 @@ class AdminRequest {
opts.headers = opts.headers || {};
opts.headers.Authorization = `Basic ${btoa(this.authorization)}`;
opts.headers.Origin = window.location;
const ret = await fetch(url, opts);
const ret = await fetch(new URL(url, this.base), opts);
if (ret && ret.status === 401) {
this.authorization = undefined;
}
Expand All @@ -38,7 +39,7 @@ class AdminRequest {
const auth = btoa(`${username}:${password}`);
opts.headers.Authorization = `Basic ${auth}`;
opts.headers.Origin = window.location;
const ret = await fetch(url, opts);
const ret = await fetch(new URL(url, this.base), opts);
return ret;
}

Expand Down Expand Up @@ -85,7 +86,7 @@ class AdminCredentals extends Component {
super();
this.credentialsOk = props.credentialsOk;
this.checkLoginUrl = props.checkLoginUrl;
this.adminRequest = new AdminRequest();
this.adminRequest = new AdminRequest(props.apiUrl);
this.state = {
username: this.adminRequest.username,
password: this.adminRequest.password,
Expand Down Expand Up @@ -169,8 +170,7 @@ class AdminCredentals extends Component {
class AdminView extends Component {
constructor(props) {
super(props);
this.props = props;
this.apiUrl = `http://${props.host}`;
this.apiUrl = props.apiUrl;
this.state = {
pauseButton: 'Pause',
fileSystem: {},
Expand All @@ -195,8 +195,8 @@ class AdminView extends Component {


async updateFileSystem() {
const adminRequest = new AdminRequest();
const response = await adminRequest.fetch(`${this.apiUrl}/api/fs.json`);
const adminRequest = new AdminRequest(this.apiUrl);
const response = await adminRequest.fetch('/api/fs.json');
console.log('Response ', response);
const dir = await response.json();
console.log('Got Files', dir);
Expand All @@ -206,7 +206,7 @@ class AdminView extends Component {
}

async logout() {
const adminRequest = new AdminRequest();
const adminRequest = new AdminRequest(this.apiUrl);
await adminRequest.logout();
this.setState({
dir: {},
Expand All @@ -227,8 +227,8 @@ class AdminView extends Component {
formBody.push(`${encodedKey}=${encodedValue}`);
}
const body = formBody.join('&');
const adminRequest = new AdminRequest();
const response = await adminRequest.fetch(`${this.apiUrl}/api/fs.json`, {
const adminRequest = new AdminRequest(this.apiUrl);
const response = await adminRequest.fetch('/api/fs.json', {
method: 'POST',
mode: 'cors',
credentials: 'include',
Expand All @@ -241,8 +241,8 @@ class AdminView extends Component {

async rebootDevice() {
const body = '';
const adminRequest = new AdminRequest();
const response = await adminRequest.fetch(`${this.apiUrl}/api/reboot.json`, {
const adminRequest = new AdminRequest(this.apiUrl);
const response = await adminRequest.fetch('/api/reboot.json', {
method: 'POST',
mode: 'cors',
credentials: 'include',
Expand Down Expand Up @@ -315,8 +315,8 @@ class AdminView extends Component {
formData.append('op', 'upload');
formData.append('path', path);
formData.append('file', this.uploadReference.files[0]);
const adminRequest = new AdminRequest();
const response = await adminRequest.fetch(`${this.apiUrl}/api/fs.json`, {
const adminRequest = new AdminRequest(this.apiUrl);
const response = await adminRequest.fetch('/api/fs.json', {
method: 'POST',
mode: 'cors',
credentials: 'include',
Expand Down Expand Up @@ -399,8 +399,10 @@ class AdminView extends Component {
</div> `;
}

const checkLoginUrl = `${this.apiUrl}/api/login.json`;
return html`<${AdminCredentals} credentialsOk=${this.updateFileSystem} checkLoginUrl=${checkLoginUrl} />`;
return html`<${AdminCredentals}
credentialsOk=${this.updateFileSystem}
checkLoginUrl='/api/login.json'
apiUrl=${this.apiUrl} />`;
}
}

Expand Down
148 changes: 148 additions & 0 deletions ui/v2/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { h, Component } from './deps/preact/preact.module.js';
import htm from './deps/htm/index.module.js';

import { NMEALayout } from './layout.js';
import { StoreView, FrameView } from './storeview.js';
import { AdminView } from './admin.js';
import { Menu } from './menu.js';
import { StoreAPIImpl } from './n2kmodule.js';

const html = htm.bind(h);

class App extends Component {
constructor(props) {
super(props);
this.storeAPI = new StoreAPIImpl();
let apiUrl = new URL(window.location);
if (props.host) {
apiUrl = new URL(`http://${props.host}`);
}
this.state = {
apiUrl,
view: props.view,
layout: props.layout,
layoutList: [],
menuKey: Date.now(),
};
this.setView = this.setView.bind(this);
this.setApiUrl = this.setApiUrl.bind(this);
this.updateFileSystem = this.updateFileSystem.bind(this);
}

async componentDidMount() {
await this.setApiUrl(this.state.apiUrl);
}

async updateFileSystem(apiUrl) {
try {
const timeout = new AbortController();
setTimeout(() => {
timeout.abort();
}, 5000);
const response = await fetch(new URL('/api/layouts.json', apiUrl), {
credentials: 'include',
signal: timeout.signal,
});
if (response.status === 200) {
const dir = await response.json();
console.log("Layouts data", dir);
this.setState({
layoutList: dir.layouts,
apiUrl,
menuKey: Date.now(),
});
return true;
}
if (response.status === 404) {
this.setState({
layoutList: [],
apiUrl,
menuKey: Date.now(),
});
return true;
}
} catch (e) {
// Some other failure, leave as is
this.setState({
layoutList: [],
menuKey: Date.now(),
});
}
return false;
}

setView(view, layout) {
if (layout) {
this.setState({
view,
layout,
menuKey: Date.now(),
});
} else {
this.setState({
view,
menuKey: Date.now(),
});
}
}

async setApiUrl(apiUrl) {
if (await this.updateFileSystem(apiUrl)) {
// host is responding switch over the feed.
this.storeAPI.stop();
this.storeAPI.start(apiUrl);
this.setState({
apiChangeMessage: 'connected',
menuKey: Date.now(),
});
} else {
this.setState({
apiChangeMessage: 'failed, switched back',
menuKey: Date.now(),
});
}
}

renderView() {
if (this.state.view === 'admin') {
return html`<${AdminView}
key=${this.state.menuKey}
apiUrl=${this.state.apiUrl} />`;
}
if (this.state.view === 'store') {
return html`<${StoreView}
key=${this.state.menuKey}
storeAPI=${this.storeAPI} />`;
}
if (this.state.view === 'frames') {
return html`<${FrameView}
key=${this.state.menuKey}
storeAPI=${this.storeAPI} />`;
}
return html`<${NMEALayout}
key=${this.state.menuKey}
storeAPI=${this.storeAPI}
apiUrl=${this.state.apiUrl}
layout=${this.state.layout} />`;
}

render() {
const view = this.renderView();
console.log("Connection state ",this.state.apiChangeMessage);
return html`<div>
<${Menu}
key=${this.state.menuKey}
layout=${this.state.layout}
layoutList=${this.state.layoutList}
setView=${this.setView}
apiUrl=${this.state.apiUrl}
setApiUrl=${this.setApiUrl}
apiChangeMessage=${this.state.apiChangeMessage}
/>
${view}
</div>`;
}
}


export { App };
Binary file added ui/v2/src/icon512.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/v2/src/icon512_maskable.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 6 additions & 24 deletions ui/v2/src/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { h, render } from './deps/preact/preact.module.js';
import htm from './deps/htm/index.module.js';

import { NMEALayout } from './layout.js';
import { StoreView, FrameView } from './storeview.js';
import { AdminView } from './admin.js';
import { Menu } from './menu.js';
import { StoreAPIImpl } from './n2kmodule.js';

import { App } from './app.js';
const html = htm.bind(h);

if ('serviceWorker' in navigator) {
Expand Down Expand Up @@ -34,20 +28,8 @@ const getLocationProperties = () => {

const rootElement = document.getElementById('root');
const properties = getLocationProperties();
if (properties.view === 'admin') {
render(html`<${Menu} locationProperties=${properties} />
<${AdminView} title="Admin" host=${properties.host} />`, rootElement);
} else {
const storeAPI = new StoreAPIImpl();
storeAPI.start(properties.host);
if (properties.view === 'dump-store') {
render(html`<${Menu} locationProperties=${properties} />
<${StoreView} title="Store" storeAPI=${storeAPI} />`, rootElement);
} else if (properties.view === 'can-frames') {
render(html`<${Menu} locationProperties=${properties} />
<${FrameView} title="CAN Frames" storeAPI=${storeAPI} />`, rootElement);
} else {
render(html`<${Menu} locationProperties=${properties} />
<${NMEALayout} locationProperties=${properties} storeAPI=${storeAPI} />`, rootElement);
}
}

render(html`<${App} host=${properties.host} view=${properties.view} layout=${properties.layout} />`, rootElement);



18 changes: 7 additions & 11 deletions ui/v2/src/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,9 @@ class NMEALayout extends Component {

constructor(props) {
super(props);
this.props = props;
this.storeAPI = props.storeAPI;
if (props.locationProperties.host) {
this.apiUrl = `http://${props.locationProperties.host}`;
} else {
this.apiUrl = '';
}
this.layoutName = props.locationProperties.layout || 'main';
this.apiUrl = props.apiUrl;
this.layoutName = props.layout || 'main';
this.onEditLayout = this.onEditLayout.bind(this);
this.onAddItem = this.onAddItem.bind(this);
this.onChangeItem = this.onChangeItem.bind(this);
Expand Down Expand Up @@ -76,9 +71,9 @@ class NMEALayout extends Component {

async componentDidMount() {
this.updateInterval = setInterval((async () => {
const packetsRecieved = this.props.storeAPI.getPacketsRecieved();
const nmea0183Address = this.props.storeAPI.getNmea0183Address();
const connectedClients = this.props.storeAPI.getConnectedClients();
const packetsRecieved = this.storeAPI.getPacketsRecieved();
const nmea0183Address = this.storeAPI.getNmea0183Address();
const connectedClients = this.storeAPI.getConnectedClients();
if (this.state.packetsRecieved !== packetsRecieved
|| this.state.nmea0183Address !== nmea0183Address
|| this.state.connectedClients !== connectedClients
Expand Down Expand Up @@ -110,7 +105,8 @@ class NMEALayout extends Component {

async loadLayout() {
let layout;
const layoutUrl = `${this.apiUrl}/api/layout.json?layout=${encodeURIComponent(this.layoutName)}`;
const layoutUrl = new URL('/api/layout.json', this.apiUrl);
layoutUrl.searchParams.set('layout', this.layoutName);
try {
const response = await fetch(layoutUrl, {
credentials: 'include',
Expand Down
6 changes: 6 additions & 0 deletions ui/v2/src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
"src": "/icon512.png",
"sizes": "512x512",
"purpose": "any"
},
{
"src": "/icon512_maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable" // <-- New property value `"maskable"`
}
]
}
Loading

0 comments on commit 628843d

Please sign in to comment.