Skip to content

Commit

Permalink
improvement: removed backends.json, added configuration management in…
Browse files Browse the repository at this point in the history
… the settings UI.
  • Loading branch information
JoshuaDodds committed Jan 1, 2025
1 parent 26fb82a commit 18adb73
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 29 deletions.
12 changes: 0 additions & 12 deletions src/backends.json

This file was deleted.

178 changes: 175 additions & 3 deletions src/components/settings-page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@ import { WithTranslation, withTranslation } from 'react-i18next';
import { DescriptionField, TitleField } from '../../i18n/rjsf-translation-fields';
import { Stats } from './stats';
import frontendPackageJson from './../../../package.json';
import { computeSettingsDiff, formatDate } from '../../utils';
import {
computeSettingsDiff,
formatDate,
getBackendURLs,
setBackendURLs,
getCurrentBackendURL,
setCurrentBackendURL,
formatDisplayURL,
getWebSocketURL,
} from '../../utils';
import { saveAs } from 'file-saver';
import Spinner from '../spinner';
import { TranslatedImageLocaliser } from './image-localiser';
Expand All @@ -24,7 +33,7 @@ import { tabs } from './tabs';

const Form = withTheme(Bootstrap5Theme);

type SettingsTab = 'settings' | 'bridge' | 'about' | 'tools' | 'donate' | 'translate';
type SettingsTab = 'settings' | 'bridge' | 'about' | 'tools' | 'donate' | 'translate' | 'ui-options';

type SettingsKeys = string;
type UrlParams = {
Expand Down Expand Up @@ -92,10 +101,17 @@ type PropsFromStore = Pick<
>;
class SettingsPage extends Component<
PropsFromStore & SettingsPageProps & DeviceApi & BridgeApi & UtilsApi & WithTranslation<'setting'>,
SettingsPageState
SettingsPageState & {
backends: string[];
newBackend: string;
newSecure: boolean;
}
> {
state = {
keyName: ROOT_KEY_NAME,
backends: getBackendURLs(),
newBackend: '',
newSecure: false,
};
settingsFormData: { [s: string]: Record<string, unknown> } = {};
renderCategoriesTabs(): JSX.Element {
Expand Down Expand Up @@ -138,10 +154,166 @@ class SettingsPage extends Component<
return this.renderDonate();
case 'translate':
return this.renderTranslate();
case 'ui-options':
return this.renderUiOptions();
default:
return <Redirect to={`/settings/settings`} />;
}
}
handleAddBackend = () => {
const { newBackend, newSecure, backends } = this.state;
if (!newBackend) return;
const updatedBackends = [...backends, getWebSocketURL(newBackend, newSecure)];
setBackendURLs(updatedBackends);
this.setState({ backends: updatedBackends, newBackend: '', newSecure: false });
window.location.reload();
};

handleRemoveBackend = (index: number) => {
const { backends } = this.state;
const updatedBackends = backends.filter((_, i) => i !== index);
setBackendURLs(updatedBackends);
this.setState({ backends: updatedBackends });
window.location.reload();
};

handleUpdateBackend = (index: number, url: string, secure: boolean) => {
const { backends } = this.state;
const updatedBackend = getWebSocketURL(url, secure);
const updatedBackends = backends.map((backend, i) => (i === index ? updatedBackend : backend));
setBackendURLs(updatedBackends);
this.setState({ backends: updatedBackends });
window.location.reload();
};

renderUiOptions(): JSX.Element {
const { t } = this.props;
const { backends, newBackend, newSecure } = this.state;

return (
<div className="p-3">
<p>
<h4>{t('settings:additional_backend_header')} </h4>
<hr />
<p>
{t('settings:additional_backend_title')} <br />
{t('settings:additional_backend_description')}{' '}
</p>
<p>{t('settings:additional_backend_secure_description')} </p>
</p>
<div className="mb-3">
{backends.length === 0 ? (
<div className="d-flex align-items-center mb-2">
<input
type="text"
className="form-control me-2"
placeholder={t('settings:additional_backend_host_tip')}
value={newBackend}
onChange={(e) => this.setState({ newBackend: e.target.value })}
/>
<div className="form-check me-2">
<input
className="form-check-input"
type="checkbox"
id="secureCheckbox"
checked={newSecure}
onChange={() => this.setState({ newSecure: !newSecure })}
/>
<label className="form-check-label" htmlFor="secureCheckbox">
{t('settings:additional_backend_secure_label')}
</label>
</div>
<button className="btn btn-primary" onClick={this.handleAddBackend}>
+
</button>
</div>
) : (
<>
{backends.map((backend, index) => {
const parsedBackend = new URL(backend);
const isSecure = parsedBackend.protocol === 'wss:';
const hostname = parsedBackend.hostname;

if (index === 0) {
return (
<div key={index} className="d-flex align-items-center mb-2">
<input
type="text"
className="form-control me-5"
value={hostname}
disabled
/>
<span className="badge bg-secondary me-2">
This host is derived and cannot be edited
</span>
</div>
);
}

return (
<div key={index} className="d-flex align-items-center mb-2">
<input
type="text"
className="form-control me-2"
defaultValue={hostname}
onBlur={(e) => this.handleUpdateBackend(index, e.target.value, isSecure)}
/>
<div className="form-check me-2">
<input
className="form-check-input"
type="checkbox"
id={`secureCheckbox-${index}`}
checked={isSecure}
onChange={(e) =>
this.handleUpdateBackend(index, hostname, e.target.checked)
}
/>
<label
className="form-check-label me-7"
htmlFor={`secureCheckbox-${index}`}
>
{t('settings:additional_backend_secure_label')}
</label>
</div>
<button
className="btn btn-danger w-25"
onClick={() => this.handleRemoveBackend(index)}
>
-
</button>
</div>
);
})}
<div className="d-flex align-items-center mt-3">
<input
type="text"
className="form-control me-2"
placeholder={t('settings:additional_backend_host_tip')}
value={newBackend}
onChange={(e) => this.setState({ newBackend: e.target.value })}
/>
<div className="form-check me-2">
<input
className="form-check-input"
type="checkbox"
id="newSecureCheckbox"
checked={newSecure}
onChange={() => this.setState({ newSecure: !newSecure })}
/>
<label className="form-check-label me-7" htmlFor="newSecureCheckbox">
{t('settings:additional_backend_secure_label')}
</label>
</div>
<button className="btn btn-primary w-25" onClick={this.handleAddBackend}>
+
</button>
</div>
</>
)}
</div>
</div>
);
}
renderTranslate(): JSX.Element {
const { t } = this.props;
return (
Expand Down
5 changes: 4 additions & 1 deletion src/components/settings-page/tabs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

export const tabs = [
{
translationKey: 'settings',
Expand All @@ -8,6 +7,10 @@ export const tabs = [
translationKey: 'tools',
url: `/settings/tools`,
},
{
translationKey: 'ui-options',
url: `/settings/ui-options`,
},
{
translationKey: 'about',
url: `/settings/about`,
Expand Down
9 changes: 8 additions & 1 deletion src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2620,7 +2620,14 @@
"translation_prompt": "You can help with the translation at",
"zigbee_herdsman_converters_version": "zigbee-herdsman-converters version",
"zigbee_herdsman_version": "zigbee-herdsman version",
"localise_images": "Localise device images"
"localise_images": "Localise device images",
"ui-options": "UI Options",
"additional_backend_header": "Additional Backend Configuration",
"additional_backend_title": "Here you can add an additional Z2M backend to allow this frontend to manage multiple Z2M instances from a single UI.",
"additional_backend_description": "When one or more additional backends have been added, they will show here and a new backend selector widget will appear in the top right.",
"additional_backend_secure_description": "(Select secure if the host uses https/wss protocol. Otherwise, leave unselected.)",
"additional_backend_host_tip": "Enter URL or hostname without protocol. (ie. z2m.mydomain.net, z2m.mydomain.net:8080, 192.168.1.150:8080 are all valid)",
"additional_backend_secure_label": "Secure?"
},
"touchlink": {
"detected_devices_message": "Detected {{count}} touchlink devices.",
Expand Down
18 changes: 6 additions & 12 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,17 @@ import { createRoot } from 'react-dom/client';

import api from './ws-client';
import { Main } from './Main';
import { setBackendURLs, getWebSocketURL, getCurrentBackendURL, setCurrentBackendURL } from './utils';
import { setBackendURLs, getWebSocketURL, getCurrentBackendURL, setCurrentBackendURL, getBackendURLs } from './utils';

async function initApp() {
const defaultUrl = getWebSocketURL(window.location.host);
try {
// obviously we need a better solution for configuration management but it's functional as a POC ;)
const headResponse = await fetch('backends.json', { method: 'HEAD' });
if (headResponse.ok) {
const response = await fetch('backends.json');
const data = await response.json();
const backendUrls = data.backends.map((backend) => getWebSocketURL(backend.url, backend.secure));
setBackendURLs([defaultUrl, ...backendUrls]);
setCurrentBackendURL(getCurrentBackendURL() || backendUrls[0]);
}
const storedBackends = getBackendURLs();
const backendUrls = storedBackends.length > 0 ? storedBackends : [defaultUrl];
setBackendURLs(backendUrls);
setCurrentBackendURL(getCurrentBackendURL() || backendUrls[0]);
} catch (error) {
// In the event there are no additional backends defined, this falls back to the
// old logic of (essentially):
// falls back to old logic of (essentially):
// const apiUrl = `${window.location.host}${document.location.pathname}api`;
// const api = new Api(`${isSecurePage() ? 'wss' : 'ws'}://${apiUrl}`);
setBackendURLs([defaultUrl]);
Expand Down

0 comments on commit 18adb73

Please sign in to comment.