1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-24 20:55:49 +00:00

Organize JS better (#343)

This commit is contained in:
Paulus Schoutsen
2017-07-16 23:31:19 -07:00
committed by GitHub
parent 35c4e1d5ae
commit 2453d6f397
16 changed files with 5 additions and 6 deletions

3
js/compatibility.js Normal file
View File

@@ -0,0 +1,3 @@
import objAssign from 'es6-object-assign';
objAssign.polyfill();

37
js/core.js Normal file
View File

@@ -0,0 +1,37 @@
import * as HAWS from 'home-assistant-js-websocket';
window.HAWS = HAWS;
window.HASS_DEMO = __DEMO__;
window.HASS_DEV = __DEV__;
const init = window.createHassConnection = function (password) {
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const url = `${proto}://${window.location.host}/api/websocket`;
const options = {
setupRetry: 10,
};
if (password !== undefined) {
options.authToken = password;
}
return HAWS.createConnection(url, options)
.then(function (conn) {
HAWS.subscribeEntities(conn);
HAWS.subscribeConfig(conn);
return conn;
});
};
if (window.noAuth) {
window.hassConnection = init();
} else if (window.localStorage.authToken) {
window.hassConnection = init(window.localStorage.authToken);
} else {
window.hassConnection = null;
}
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/service_worker.js');
});
}

107
js/editor/automation.js Normal file
View File

@@ -0,0 +1,107 @@
import { h, Component } from 'preact';
import Trigger from './trigger';
import Script from './script';
export default class Automation extends Component {
constructor() {
super();
this.onChange = this.onChange.bind(this);
this.triggerChanged = this.triggerChanged.bind(this);
this.actionChanged = this.actionChanged.bind(this);
}
onChange(ev) {
this.props.onChange({
...this.props.automation,
[ev.target.name]: ev.target.value,
});
}
triggerChanged(trigger) {
this.props.onChange({
...this.props.automation,
trigger,
});
}
actionChanged(action) {
this.props.onChange({
...this.props.automation,
action,
});
}
render({ automation, isWide }) {
const { alias, trigger, condition, action } = automation;
return (
<div>
<ha-config-section is-wide={isWide}>
<span slot='header'>{alias}</span>
<span slot='introduction'>
Use automations to bring your home alive.
</span>
<paper-card>
<div class='card-content'>
<paper-input
label="Name"
name="alias"
value={alias}
onChange={this.onChange}
/>
</div>
</paper-card>
</ha-config-section>
<ha-config-section is-wide={isWide}>
<span slot='header'>Triggers</span>
<span slot='introduction'>
Triggers are what starts the processing of an automation rule.
It is possible to specify multiple triggers for the same rule.
Once a trigger starts, Home Assistant will validate the conditions,
if any, and call the action.
<p><a href="https://home-assistant.io/docs/automation/trigger/" target="_blank">
Learn more about triggers.
</a></p>
</span>
<Trigger trigger={trigger} onChange={this.triggerChanged} />
</ha-config-section>
{ condition &&
<ha-config-section is-wide={isWide}>
<span slot='header'>Conditions</span>
<span slot='introduction'>
Conditions are an optional part of an automation rule and can be used to prevent
an action from happening when triggered. Conditions look very similar to triggers
but are very different. A trigger will look at events happening in the system
while a condition only looks at how the system looks right now. A trigger can
observe that a switch is being turned on. A condition can only see if a switch
is currently on or off.
<p><a href="https://home-assistant.io/docs/scripts/conditions/" target="_blank">
Learn more about conditions.
</a></p>
</span>
<paper-card>
<div class='card-content'>
Conditions are not supported yet.
<pre>{JSON.stringify(condition, null, 2)}</pre>
</div>
</paper-card>
</ha-config-section>}
<ha-config-section is-wide={isWide}>
<span slot='header'>Action</span>
<span slot='introduction'>
The actions are what Home Assistant will do when the automation is triggered.
<p><a href="https://home-assistant.io/docs/scripts/" target="_blank">
Learn more about actions.
</a></p>
</span>
<Script script={action} onChange={this.actionChanged} />
</ha-config-section>
</div>
);
}
}

10
js/editor/editor.js Normal file
View File

@@ -0,0 +1,10 @@
import { h, render } from 'preact';
import Automation from './automation';
window.AutomationEditor = function (mountEl, props, mergeEl) {
return render(h(Automation, props), mountEl, mergeEl);
};
window.unmountPreact = function (mountEl, mergeEl) {
render(() => null, mountEl, mergeEl);
};

View File

@@ -0,0 +1,57 @@
import { h, Component } from 'preact';
export default class JSONTextArea extends Component {
constructor(props) {
super(props);
this.state.isValid = true;
this.state.value = JSON.stringify(props.value || {}, null, 2);
this.onChange = this.onChange.bind(this);
}
onChange(ev) {
const value = ev.target.value;
let parsed;
let isValid;
try {
parsed = JSON.parse(value);
isValid = true;
} catch (err) {
// Invalid JSON
isValid = false;
}
this.setState({
value,
isValid,
});
if (isValid) {
this.props.onChange(parsed);
}
}
componentWillReceiveProps({ value }) {
this.setState({
value: JSON.stringify(value, null, 2),
isValid: true,
});
}
render(props, { value, isValid }) {
const style = {
minWidth: 300,
width: '100%',
};
if (!isValid) {
style.border = '1px solid red';
}
return (
<iron-autogrow-textarea
value={value}
style={style}
onValue-Changed={this.onChange}
/>
);
}
}

View File

@@ -0,0 +1,52 @@
import { h, Component } from 'preact';
import JSONTextArea from '../json_textarea';
export default class CallServiceAction extends Component {
constructor() {
super();
this.onChange = this.onChange.bind(this);
this.serviceDataChanged = this.serviceDataChanged.bind(this);
}
onChange(ev) {
this.props.onChange(this.props.index, {
...this.props.action,
[ev.target.name]: ev.target.value
});
}
/* eslint-disable camelcase */
serviceDataChanged(data) {
this.props.onChange(this.props.index, {
...this.props.action,
data,
});
}
render({ action }) {
const { alias, service, data } = action;
return (
<div>
<paper-input
label="Alias"
name="alias"
value={alias}
onChange={this.onChange}
/>
<paper-input
label="Service"
name="service"
value={service}
onChange={this.onChange}
/>
Service Data<br />
<JSONTextArea
value={data}
onChange={this.serviceDataChanged}
/>
</div>
);
}
}

50
js/editor/script/index.js Normal file
View File

@@ -0,0 +1,50 @@
import { h, Component } from 'preact';
import ScriptAction from './script_action';
export default class Script extends Component {
constructor() {
super();
this.addAction = this.addAction.bind(this);
this.actionChanged = this.actionChanged.bind(this);
}
addAction() {
const script = this.props.script.concat({
service: '',
});
this.props.onChange(script);
}
actionChanged(index, newValue) {
const script = this.props.script.concat();
if (newValue === null) {
script.splice(index, 1);
} else {
script[index] = newValue;
}
this.props.onChange(script);
}
render({ script }) {
return (
<div class="script">
{script.map((act, idx) => (
<ScriptAction
index={idx}
action={act}
onChange={this.actionChanged}
/>))}
<paper-card>
<div class='card-actions add-card'>
<paper-button onTap={this.addAction}>Add action</paper-button>
</div>
</paper-card>
</div>
);
}
}

View File

@@ -0,0 +1,106 @@
import { h, Component } from 'preact';
import CallService from './call_service';
function getType(action) {
if ('service' in action) {
return 'Call Service';
}
return null;
}
const TYPES = {
'Call Service': CallService,
Delay: null,
'Templated Delay': null,
Condition: null,
'Fire Event': null,
};
const OPTIONS = Object.keys(TYPES).sort();
export default class Action extends Component {
constructor() {
super();
this.typeChanged = this.typeChanged.bind(this);
this.onDelete = this.onDelete.bind(this);
}
typeChanged(ev) {
const newType = ev.target.selectedItem.innerHTML;
const oldType = getType(this.props.action);
if (oldType !== newType) {
this.props.onChange(this.props.index, {
platform: newType,
});
}
}
onDelete() {
// eslint-disable-next-line
if (confirm('Sure you want to delete?')) {
this.props.onChange(this.props.index, null);
}
}
render({ index, action, onChange }) {
const type = getType(action);
const Comp = TYPES[type];
const selected = OPTIONS.indexOf(type);
let content;
if (Comp) {
content = (
<div>
<paper-dropdown-menu-light label="Action Type" no-animations>
<paper-listbox
slot="dropdown-content"
selected={selected}
oniron-select={this.typeChanged}
>
{OPTIONS.map(opt => <paper-item>{opt}</paper-item>)}
</paper-listbox>
</paper-dropdown-menu-light>
<Comp
index={index}
action={action}
onChange={onChange}
/>
</div>
);
} else {
content = (
<div>
Unsupported action
<pre>{JSON.stringify(action, null, 2)}</pre>
</div>
);
}
return (
<paper-card>
<div class='card-menu'>
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
>
<paper-icon-button
icon="mdi:dots-vertical"
slot="dropdown-trigger"
/>
<paper-listbox slot="dropdown-content">
<paper-item disabled>Duplicate</paper-item>
<paper-item onTap={this.onDelete}>Delete</paper-item>
</paper-listbox>
</paper-menu-button>
</div>
<div class='card-content'>{content}</div>
</paper-card>
);
}
}

View File

@@ -0,0 +1,41 @@
import { h, Component } from 'preact';
import JSONTextArea from '../json_textarea';
import { onChange } from './util';
export default class EventTrigger extends Component {
constructor() {
super();
this.onChange = onChange.bind(this);
this.eventDataChanged = this.eventDataChanged.bind(this);
}
/* eslint-disable camelcase */
eventDataChanged(event_data) {
this.props.onChange(this.props.index, {
...this.props.trigger,
event_data,
});
}
render({ trigger }) {
const { event_type, event_data } = trigger;
return (
<div>
<paper-input
label="Event Type"
name="event_type"
value={event_type}
onChange={this.onChange}
/>
Event Data
<JSONTextArea
value={event_data}
onChange={this.eventDataChanged}
/>
</div>
);
}
}

View File

@@ -0,0 +1,50 @@
import { h, Component } from 'preact';
import TriggerRow from './trigger_row';
export default class Trigger extends Component {
constructor() {
super();
this.addTrigger = this.addTrigger.bind(this);
this.triggerChanged = this.triggerChanged.bind(this);
}
addTrigger() {
const trigger = this.props.trigger.concat({
platform: 'event',
});
this.props.onChange(trigger);
}
triggerChanged(index, newValue) {
const trigger = this.props.trigger.concat();
if (newValue === null) {
trigger.splice(index, 1);
} else {
trigger[index] = newValue;
}
this.props.onChange(trigger);
}
render({ trigger }) {
return (
<div class="triggers">
{trigger.map((trg, idx) => (
<TriggerRow
index={idx}
trigger={trg}
onChange={this.triggerChanged}
/>))}
<paper-card>
<div class='card-actions add-card'>
<paper-button onTap={this.addTrigger}>Add trigger</paper-button>
</div>
</paper-card>
</div>
);
}
}

View File

@@ -0,0 +1,45 @@
import { h, Component } from 'preact';
import { onChange } from './util';
export default class NumericStateTrigger extends Component {
constructor() {
super();
this.onChange = onChange.bind(this);
}
/* eslint-disable camelcase */
render({ trigger }) {
const { value_template, entity_id, below, above } = trigger;
return (
<div>
<paper-input
label="Entity Id"
name="entity_id"
value={entity_id}
onChange={this.onChange}
/>
<paper-input
label="Above"
name="above"
value={above}
onChange={this.onChange}
/>
<paper-input
label="Below"
name="below"
value={below}
onChange={this.onChange}
/>
Value template (optional)<br />
<textarea
name="value_template"
value={value_template}
style={{ width: '100%', height: 100 }}
onChange={this.onChange}
/>
</div>
);
}
}

View File

@@ -0,0 +1,41 @@
import { h, Component } from 'preact';
import { onChange } from './util';
export default class StateTrigger extends Component {
constructor() {
super();
this.onChange = onChange.bind(this);
}
/* eslint-disable camelcase */
render({ trigger }) {
const { entity_id, to } = trigger;
const trgFrom = trigger.from;
const trgFor = trigger.for;
return (
<div>
<paper-input
label="Entity Id"
name="entity_id"
value={entity_id}
onChange={this.onChange}
/>
<paper-input
label="From"
name="from"
value={trgFrom}
onChange={this.onChange}
/>
<paper-input
label="To"
name="to"
value={to}
onChange={this.onChange}
/>
{trgFor && <pre>For: {JSON.stringify(trgFor, null, 2)}</pre>}
</div>
);
}
}

View File

@@ -0,0 +1,103 @@
import { h, Component } from 'preact';
import EventTrigger from './event';
import StateTrigger from './state';
import NumericStateTrigger from './numeric_state';
const TYPES = {
event: EventTrigger,
state: StateTrigger,
homeassistant: null,
mqtt: null,
numeric_state: NumericStateTrigger,
sun: null,
template: null,
time: null,
zone: null,
};
const OPTIONS = Object.keys(TYPES).sort();
export default class TriggerRow extends Component {
constructor() {
super();
this.typeChanged = this.typeChanged.bind(this);
this.onDelete = this.onDelete.bind(this);
}
typeChanged(ev) {
const type = ev.target.selectedItem.innerHTML;
if (type !== this.props.trigger.platform) {
this.props.onChange(this.props.index, {
platform: type,
});
}
}
onDelete() {
// eslint-disable-next-line
if (confirm('Sure you want to delete?')) {
this.props.onChange(this.props.index, null);
}
}
render({ index, trigger, onChange }) {
const Comp = TYPES[trigger.platform];
const selected = OPTIONS.indexOf(trigger.platform);
let content;
if (Comp) {
content = (
<div>
<paper-dropdown-menu-light label="Trigger Type" no-animations>
<paper-listbox
slot="dropdown-content"
selected={selected}
oniron-select={this.typeChanged}
>
{OPTIONS.map(opt => <paper-item>{opt}</paper-item>)}
</paper-listbox>
</paper-dropdown-menu-light>
<Comp
index={index}
trigger={trigger}
onChange={onChange}
/>
</div>
);
} else {
content = (
<div>
Unsupported platform: {trigger.platform}
<pre>{JSON.stringify(trigger, null, 2)}</pre>
</div>
);
}
return (
<paper-card>
<div class='card-menu'>
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
>
<paper-icon-button
icon="mdi:dots-vertical"
slot="dropdown-trigger"
/>
<paper-listbox slot="dropdown-content">
<paper-item disabled>Duplicate</paper-item>
<paper-item onTap={this.onDelete}>Delete</paper-item>
</paper-listbox>
</paper-menu-button>
</div>
<div class='card-content'>{content}</div>
</paper-card>
);
}
}

11
js/editor/trigger/util.js Normal file
View File

@@ -0,0 +1,11 @@
export function onChange(ev) {
const trigger = { ...this.props.trigger };
if (ev.target.value) {
trigger[ev.target.name] = ev.target.value;
} else {
delete trigger[ev.target.name];
}
this.props.onChange(this.props.index, trigger);
}

3
js/editor/util.js Normal file
View File

@@ -0,0 +1,3 @@
export function validEntityId(entityId) {
return /^(\w+)\.(\w+)$/.test(entityId);
}