mirror of
https://github.com/esphome/esphome.git
synced 2024-11-24 16:08:10 +01:00
Dashboard editor live validation (#540)
* Dashboard editor validation * Improve range detection * Lint
This commit is contained in:
parent
e373620393
commit
a1a7448868
7 changed files with 134 additions and 19 deletions
|
@ -245,7 +245,7 @@ def command_vscode(args):
|
|||
from esphome import vscode
|
||||
|
||||
CORE.config_path = args.configuration
|
||||
vscode.read_config()
|
||||
vscode.read_config(args)
|
||||
|
||||
|
||||
def command_compile(args, config):
|
||||
|
@ -423,7 +423,8 @@ def parse_args(argv):
|
|||
dashboard.add_argument("--socket",
|
||||
help="Make the dashboard serve under a unix socket", type=str)
|
||||
|
||||
subparsers.add_parser('vscode', help=argparse.SUPPRESS)
|
||||
vscode = subparsers.add_parser('vscode', help=argparse.SUPPRESS)
|
||||
vscode.add_argument('--ace', action='store_true')
|
||||
|
||||
return parser.parse_args(argv[1:])
|
||||
|
||||
|
|
|
@ -233,15 +233,19 @@ class Config(OrderedDict):
|
|||
return err
|
||||
return None
|
||||
|
||||
def get_deepest_value_for_path(self, path):
|
||||
# type: (ConfigPath) -> ConfigType
|
||||
def get_deepest_document_range_for_path(self, path):
|
||||
# type: (ConfigPath) -> Optional[ESPHomeDataBase]
|
||||
data = self
|
||||
doc_range = None
|
||||
for item_index in path:
|
||||
try:
|
||||
data = data[item_index]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
return data
|
||||
return data
|
||||
return doc_range
|
||||
if isinstance(data, ESPHomeDataBase) and data.esp_range is not None:
|
||||
doc_range = data.esp_range
|
||||
|
||||
return doc_range
|
||||
|
||||
def get_nested_item(self, path):
|
||||
# type: (ConfigPath) -> ConfigType
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import print_function
|
|||
|
||||
import codecs
|
||||
import json
|
||||
import os
|
||||
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.py_compat import safe_input
|
||||
|
@ -9,7 +10,8 @@ from esphome.py_compat import safe_input
|
|||
|
||||
def read_config_file(path):
|
||||
# type: (basestring) -> unicode
|
||||
if CORE.vscode:
|
||||
if CORE.vscode and (not CORE.ace or
|
||||
os.path.abspath(path) == os.path.abspath(CORE.config_path)):
|
||||
print(json.dumps({
|
||||
'type': 'read_file',
|
||||
'path': path,
|
||||
|
|
|
@ -467,6 +467,7 @@ class EsphomeCore(object):
|
|||
self.dashboard = False
|
||||
# True if command is run from vscode api
|
||||
self.vscode = False
|
||||
self.ace = False
|
||||
# The name of the node
|
||||
self.name = None # type: str
|
||||
# The relative path to the configuration YAML
|
||||
|
|
|
@ -256,12 +256,12 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
|
|||
self.write_message({'event': 'exit', 'code': returncode})
|
||||
|
||||
def on_close(self):
|
||||
# Shutdown proc on WS close
|
||||
self._is_closed = True
|
||||
# Check if proc exists (if 'start' has been run)
|
||||
if self.is_process_active:
|
||||
_LOGGER.debug("Terminating process")
|
||||
self._proc.proc.terminate()
|
||||
# Shutdown proc on WS close
|
||||
self._is_closed = True
|
||||
|
||||
def build_command(self, json_message):
|
||||
raise NotImplementedError
|
||||
|
@ -310,6 +310,11 @@ class EsphomeVscodeHandler(EsphomeCommandWebSocket):
|
|||
return ["esphome", "--dashboard", "-q", 'dummy', "vscode"]
|
||||
|
||||
|
||||
class EsphomeAceEditorHandler(EsphomeCommandWebSocket):
|
||||
def build_command(self, json_message):
|
||||
return ["esphome", "--dashboard", "-q", settings.config_dir, "vscode", "--ace"]
|
||||
|
||||
|
||||
class SerialPortRequestHandler(BaseHandler):
|
||||
@authenticated
|
||||
def get(self):
|
||||
|
@ -678,6 +683,7 @@ def make_app(debug=False):
|
|||
(rel + "clean-mqtt", EsphomeCleanMqttHandler),
|
||||
(rel + "clean", EsphomeCleanHandler),
|
||||
(rel + "vscode", EsphomeVscodeHandler),
|
||||
(rel + "ace", EsphomeAceEditorHandler),
|
||||
(rel + "edit", EditRequestHandler),
|
||||
(rel + "download.bin", DownloadBinaryRequestHandler),
|
||||
(rel + "serial-ports", SerialPortRequestHandler),
|
||||
|
@ -723,7 +729,7 @@ def start_web_server(args):
|
|||
|
||||
webbrowser.open('localhost:{}'.format(args.port))
|
||||
|
||||
if settings.status_use_ping:
|
||||
if not settings.status_use_ping:
|
||||
status_thread = PingStatusThread()
|
||||
else:
|
||||
status_thread = MDNSStatusThread()
|
||||
|
|
|
@ -550,10 +550,75 @@ const editModalElem = document.getElementById("modal-editor");
|
|||
const editorElem = editModalElem.querySelector("#editor");
|
||||
const editor = ace.edit(editorElem);
|
||||
let activeEditorConfig = null;
|
||||
let aceWs = null;
|
||||
let aceValidationScheduled = false;
|
||||
let aceValidationRunning = false;
|
||||
const startAceWebsocket = () => {
|
||||
aceWs = new WebSocket(`${wsUrl}ace`);
|
||||
aceWs.addEventListener('message', (event) => {
|
||||
const raw = JSON.parse(event.data);
|
||||
if (raw.event === "line") {
|
||||
const msg = JSON.parse(raw.data);
|
||||
if (msg.type === "result") {
|
||||
console.log(msg);
|
||||
const arr = [];
|
||||
|
||||
for (const v of msg.validation_errors) {
|
||||
let o = {
|
||||
text: v.message,
|
||||
type: 'error',
|
||||
row: 0,
|
||||
column: 0
|
||||
};
|
||||
if (v.range != null) {
|
||||
o.row = v.range.start_line;
|
||||
o.column = v.range.start_col;
|
||||
}
|
||||
arr.push(o);
|
||||
}
|
||||
for (const v of msg.yaml_errors) {
|
||||
arr.push({
|
||||
text: v.message,
|
||||
type: 'error',
|
||||
row: 0,
|
||||
column: 0
|
||||
});
|
||||
}
|
||||
|
||||
editor.session.setAnnotations(arr);
|
||||
|
||||
aceValidationRunning = false;
|
||||
} else if (msg.type === "read_file") {
|
||||
sendAceStdin({
|
||||
type: 'file_response',
|
||||
content: editor.getValue()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
aceWs.addEventListener('open', () => {
|
||||
const msg = JSON.stringify({type: 'spawn'});
|
||||
aceWs.send(msg);
|
||||
});
|
||||
aceWs.addEventListener('close', () => {
|
||||
aceWs = null;
|
||||
setTimeout(startAceWebsocket, 5000)
|
||||
});
|
||||
};
|
||||
const sendAceStdin = (data) => {
|
||||
let send = JSON.stringify({
|
||||
type: 'stdin',
|
||||
data: JSON.stringify(data)+'\n',
|
||||
});
|
||||
aceWs.send(send);
|
||||
};
|
||||
startAceWebsocket();
|
||||
|
||||
editor.setTheme("ace/theme/dreamweaver");
|
||||
editor.session.setMode("ace/mode/yaml");
|
||||
editor.session.setOption('useSoftTabs', true);
|
||||
editor.session.setOption('tabSize', 2);
|
||||
editor.session.setOption('useWorker', false);
|
||||
|
||||
const saveButton = editModalElem.querySelector(".save-button");
|
||||
const saveValidateButton = editModalElem.querySelector(".save-validate-button");
|
||||
|
@ -569,6 +634,19 @@ const saveEditor = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const debounce = (func, wait) => {
|
||||
let timeout;
|
||||
return function() {
|
||||
let context = this, args = arguments;
|
||||
let later = function() {
|
||||
timeout = null;
|
||||
func.apply(context, args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
};
|
||||
|
||||
editor.commands.addCommand({
|
||||
name: 'saveCommand',
|
||||
bindKey: {win: 'Ctrl-S', mac: 'Command-S'},
|
||||
|
@ -576,6 +654,24 @@ editor.commands.addCommand({
|
|||
readOnly: false
|
||||
});
|
||||
|
||||
editor.session.on('change', debounce(() => {
|
||||
aceValidationScheduled = true;
|
||||
}, 250));
|
||||
|
||||
setInterval(() => {
|
||||
if (!aceValidationScheduled || aceValidationRunning)
|
||||
return;
|
||||
if (aceWs == null)
|
||||
return;
|
||||
|
||||
sendAceStdin({
|
||||
type: 'validate',
|
||||
file: activeEditorConfig
|
||||
});
|
||||
aceValidationRunning = true;
|
||||
aceValidationScheduled = false;
|
||||
}, 100);
|
||||
|
||||
saveButton.addEventListener('click', saveEditor);
|
||||
saveValidateButton.addEventListener('click', saveEditor);
|
||||
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
from __future__ import print_function
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from esphome.config import load_config, _format_vol_invalid
|
||||
from esphome.core import CORE
|
||||
from esphome.py_compat import text_type, safe_input
|
||||
from esphome.yaml_util import ESPHomeDataBase
|
||||
|
||||
|
||||
def _get_invalid_range(res, invalid):
|
||||
# type: (Config, vol.Invalid) -> Optional[DocumentRange]
|
||||
obj = res.get_deepest_value_for_path(invalid.path)
|
||||
if isinstance(obj, ESPHomeDataBase) and obj.esp_range is not None:
|
||||
return obj.esp_range
|
||||
return None
|
||||
return res.get_deepest_document_range_for_path(invalid.path)
|
||||
|
||||
|
||||
def _dump_range(range):
|
||||
|
@ -53,12 +50,17 @@ class VSCodeResult(object):
|
|||
})
|
||||
|
||||
|
||||
def read_config():
|
||||
def read_config(args):
|
||||
while True:
|
||||
CORE.reset()
|
||||
data = json.loads(safe_input())
|
||||
assert data['type'] == 'validate'
|
||||
CORE.vscode = True
|
||||
CORE.ace = args.ace
|
||||
f = data['file']
|
||||
if CORE.ace:
|
||||
CORE.config_path = os.path.join(args.configuration, f)
|
||||
else:
|
||||
CORE.config_path = data['file']
|
||||
vs = VSCodeResult()
|
||||
try:
|
||||
|
@ -67,6 +69,9 @@ def read_config():
|
|||
vs.add_yaml_error(text_type(err))
|
||||
else:
|
||||
for err in res.errors:
|
||||
try:
|
||||
range_ = _get_invalid_range(res, err)
|
||||
vs.add_validation_error(range_, _format_vol_invalid(err, res))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
continue
|
||||
print(vs.dump())
|
||||
|
|
Loading…
Reference in a new issue