Add config editor to webUI (#4608)

* Add raw config endpoint

* Add config editor

* Add code editor

* Add error

* Add ability to copy config

* Only show the save button when code has been edited

* Update errors

* Remove debug config from system page

* Break out config saving steps to pinpoint where error occurred.

* Show correct config errors

* Switch to monaco editor

* Adjust UI colors and behavior

* Get yaml validation working

* Set success color
This commit is contained in:
Nicolas Mowen 2022-12-07 06:36:56 -07:00 committed by GitHub
parent 97161310a5
commit 7888059c9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 370 additions and 29 deletions

View File

@ -1028,3 +1028,8 @@ class FrigateConfig(FrigateBaseModel):
config = json.loads(raw_config) config = json.loads(raw_config)
return cls.parse_obj(config) return cls.parse_obj(config)
@classmethod
def parse_raw(cls, raw_config):
config = load_config_with_no_duplicates(raw_config)
return cls.parse_obj(config)

View File

@ -7,6 +7,7 @@ import json
import os import os
import subprocess as sp import subprocess as sp
import time import time
import traceback
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
from urllib.parse import unquote from urllib.parse import unquote
@ -27,11 +28,17 @@ from flask import (
from peewee import SqliteDatabase, operator, fn, DoesNotExist from peewee import SqliteDatabase, operator, fn, DoesNotExist
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from frigate.config import FrigateConfig
from frigate.const import CLIPS_DIR, RECORD_DIR from frigate.const import CLIPS_DIR, RECORD_DIR
from frigate.models import Event, Recordings from frigate.models import Event, Recordings
from frigate.object_processing import TrackedObject from frigate.object_processing import TrackedObject
from frigate.stats import stats_snapshot from frigate.stats import stats_snapshot
from frigate.util import clean_camera_user_pass, ffprobe_stream, vainfo_hwaccel from frigate.util import (
clean_camera_user_pass,
ffprobe_stream,
restart_frigate,
vainfo_hwaccel,
)
from frigate.storage import StorageMaintainer from frigate.storage import StorageMaintainer
from frigate.version import VERSION from frigate.version import VERSION
@ -599,7 +606,81 @@ def config():
return jsonify(config) return jsonify(config)
@bp.route("/config/schema") @bp.route("/config/raw")
def config_raw():
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
# Check if we can use .yaml instead of .yml
config_file_yaml = config_file.replace(".yml", ".yaml")
if os.path.isfile(config_file_yaml):
config_file = config_file_yaml
if not os.path.isfile(config_file):
return "Could not find file", 410
with open(config_file, "r") as f:
raw_config = f.read()
f.close()
return raw_config, 200
@bp.route("/config/save", methods=["POST"])
def config_save():
new_config = request.get_data().decode()
if not new_config:
return "Config with body param is required", 400
# Validate the config schema
try:
new_yaml = FrigateConfig.parse_raw(new_config)
except Exception as e:
return make_response(
jsonify(
{
"success": False,
"message": f"\nConfig Error:\n\n{str(traceback.format_exc())}",
}
),
400,
)
# Save the config to file
try:
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
# Check if we can use .yaml instead of .yml
config_file_yaml = config_file.replace(".yml", ".yaml")
if os.path.isfile(config_file_yaml):
config_file = config_file_yaml
with open(config_file, "w") as f:
f.write(new_config)
f.close()
except Exception as e:
return make_response(
jsonify(
{
"success": False,
"message": f"Could not write config file, be sure that frigate has write permission on the config file.",
}
),
400,
)
try:
restart_frigate()
except Exception as e:
logging.error(f"Error restarting frigate: {e}")
return "Config successfully saved, unable to restart frigate", 200
return "Config successfully saved, restarting...", 200
@bp.route("/config/schema.json")
def config_schema(): def config_schema():
return current_app.response_class( return current_app.response_class(
current_app.frigate_config.schema_json(), mimetype="application/json" current_app.frigate_config.schema_json(), mimetype="application/json"

176
web/package-lock.json generated
View File

@ -13,6 +13,7 @@
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"idb-keyval": "^6.2.0", "idb-keyval": "^6.2.0",
"immer": "^9.0.16", "immer": "^9.0.16",
"monaco-yaml": "^4.0.2",
"preact": "^10.11.3", "preact": "^10.11.3",
"preact-async-route": "^2.2.1", "preact-async-route": "^2.2.1",
"preact-router": "^4.1.0", "preact-router": "^4.1.0",
@ -21,7 +22,8 @@
"swr": "^1.3.0", "swr": "^1.3.0",
"video.js": "^7.20.3", "video.js": "^7.20.3",
"videojs-playlist": "^5.0.0", "videojs-playlist": "^5.0.0",
"videojs-seek-buttons": "^3.0.1" "videojs-seek-buttons": "^3.0.1",
"vite-plugin-monaco-editor": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.4.0", "@preact/preset-vite": "^2.4.0",
@ -1361,8 +1363,7 @@
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.11", "version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
"dev": true
}, },
"node_modules/@types/ms": { "node_modules/@types/ms": {
"version": "0.7.31", "version": "0.7.31",
@ -6257,6 +6258,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
},
"node_modules/jsx-ast-utils": { "node_modules/jsx-ast-utils": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
@ -6598,6 +6604,62 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/monaco-editor": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.1.tgz",
"integrity": "sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==",
"peer": true
},
"node_modules/monaco-marker-data-provider": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.1.1.tgz",
"integrity": "sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==",
"funding": {
"url": "https://github.com/sponsors/remcohaszing"
},
"peerDependencies": {
"monaco-editor": ">=0.30.0"
}
},
"node_modules/monaco-worker-manager": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz",
"integrity": "sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==",
"peerDependencies": {
"monaco-editor": ">=0.30.0"
}
},
"node_modules/monaco-yaml": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-4.0.2.tgz",
"integrity": "sha512-Wxn6CblkQDLOUusfi0eZ3qZhkuKYIrK7fXlkJOOG+W18zgKePbuZW0XNWpczlxDC27D753dB18pMnx4U7MZ3yg==",
"dependencies": {
"@types/json-schema": "^7.0.0",
"jsonc-parser": "^3.0.0",
"monaco-marker-data-provider": "^1.0.0",
"monaco-worker-manager": "^2.0.0",
"path-browserify": "^1.0.0",
"prettier": "^2.0.0",
"vscode-languageserver-textdocument": "^1.0.0",
"vscode-languageserver-types": "^3.0.0",
"vscode-uri": "^3.0.0",
"yaml": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/remcohaszing"
},
"peerDependencies": {
"monaco-editor": ">=0.30"
}
},
"node_modules/monaco-yaml/node_modules/yaml": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
"engines": {
"node": ">= 14"
}
},
"node_modules/mpd-parser": { "node_modules/mpd-parser": {
"version": "0.21.1", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.1.tgz", "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.1.tgz",
@ -7181,6 +7243,11 @@
"url": "https://github.com/inikulin/parse5?sponsor=1" "url": "https://github.com/inikulin/parse5?sponsor=1"
} }
}, },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -7438,7 +7505,6 @@
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz",
"integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==", "integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==",
"dev": true,
"bin": { "bin": {
"prettier": "bin-prettier.js" "prettier": "bin-prettier.js"
}, },
@ -8780,6 +8846,14 @@
} }
} }
}, },
"node_modules/vite-plugin-monaco-editor": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz",
"integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==",
"peerDependencies": {
"monaco-editor": ">=0.33.0"
}
},
"node_modules/vitest": { "node_modules/vitest": {
"version": "0.25.3", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-0.25.3.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.25.3.tgz",
@ -8835,6 +8909,21 @@
} }
} }
}, },
"node_modules/vscode-languageserver-textdocument": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.7.tgz",
"integrity": "sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg=="
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
},
"node_modules/vscode-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz",
"integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ=="
},
"node_modules/w3c-xmlserializer": { "node_modules/w3c-xmlserializer": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
@ -10160,8 +10249,7 @@
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.11", "version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
"dev": true
}, },
"@types/ms": { "@types/ms": {
"version": "0.7.31", "version": "0.7.31",
@ -13622,6 +13710,11 @@
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true "dev": true
}, },
"jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
},
"jsx-ast-utils": { "jsx-ast-utils": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
@ -13875,6 +13968,48 @@
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
"dev": true "dev": true
}, },
"monaco-editor": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.1.tgz",
"integrity": "sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==",
"peer": true
},
"monaco-marker-data-provider": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.1.1.tgz",
"integrity": "sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==",
"requires": {}
},
"monaco-worker-manager": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz",
"integrity": "sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==",
"requires": {}
},
"monaco-yaml": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-4.0.2.tgz",
"integrity": "sha512-Wxn6CblkQDLOUusfi0eZ3qZhkuKYIrK7fXlkJOOG+W18zgKePbuZW0XNWpczlxDC27D753dB18pMnx4U7MZ3yg==",
"requires": {
"@types/json-schema": "^7.0.0",
"jsonc-parser": "^3.0.0",
"monaco-marker-data-provider": "^1.0.0",
"monaco-worker-manager": "^2.0.0",
"path-browserify": "^1.0.0",
"prettier": "^2.0.0",
"vscode-languageserver-textdocument": "^1.0.0",
"vscode-languageserver-types": "^3.0.0",
"vscode-uri": "^3.0.0",
"yaml": "^2.0.0"
},
"dependencies": {
"yaml": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg=="
}
}
},
"mpd-parser": { "mpd-parser": {
"version": "0.21.1", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.1.tgz", "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.1.tgz",
@ -14287,6 +14422,11 @@
"entities": "^4.4.0" "entities": "^4.4.0"
} }
}, },
"path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
},
"path-exists": { "path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -14446,8 +14586,7 @@
"prettier": { "prettier": {
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz",
"integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==", "integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA=="
"dev": true
}, },
"pretty-format": { "pretty-format": {
"version": "27.5.1", "version": "27.5.1",
@ -15456,6 +15595,12 @@
"rollup": "^2.79.1" "rollup": "^2.79.1"
} }
}, },
"vite-plugin-monaco-editor": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz",
"integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==",
"requires": {}
},
"vitest": { "vitest": {
"version": "0.25.3", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-0.25.3.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.25.3.tgz",
@ -15478,6 +15623,21 @@
"vite": "^3.0.0" "vite": "^3.0.0"
} }
}, },
"vscode-languageserver-textdocument": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.7.tgz",
"integrity": "sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg=="
},
"vscode-languageserver-types": {
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
},
"vscode-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz",
"integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ=="
},
"w3c-xmlserializer": { "w3c-xmlserializer": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",

View File

@ -22,6 +22,8 @@
"preact-router": "^4.1.0", "preact-router": "^4.1.0",
"react": "npm:@preact/compat@^17.1.2", "react": "npm:@preact/compat@^17.1.2",
"react-dom": "npm:@preact/compat@^17.1.2", "react-dom": "npm:@preact/compat@^17.1.2",
"vite-plugin-monaco-editor": "^1.1.0",
"monaco-yaml": "^4.0.2",
"swr": "^1.3.0", "swr": "^1.3.0",
"video.js": "^7.20.3", "video.js": "^7.20.3",
"videojs-playlist": "^5.0.0", "videojs-playlist": "^5.0.0",

View File

@ -44,8 +44,10 @@ export default function Sidebar() {
</Match> </Match>
{birdseye?.enabled ? <Destination href="/birdseye" text="Birdseye" /> : null} {birdseye?.enabled ? <Destination href="/birdseye" text="Birdseye" /> : null}
<Destination href="/events" text="Events" /> <Destination href="/events" text="Events" />
<Separator />
<Destination href="/storage" text="Storage" /> <Destination href="/storage" text="Storage" />
<Destination href="/system" text="System" /> <Destination href="/system" text="System" />
<Destination href="/config" text="Config" />
<Separator /> <Separator />
<div className="flex flex-grow" /> <div className="flex flex-grow" />
{ENV !== 'production' ? ( {ENV !== 'production' ? (

View File

@ -37,6 +37,7 @@ export default function App() {
/> />
<AsyncRoute path="/storage" getComponent={Routes.getStorage} /> <AsyncRoute path="/storage" getComponent={Routes.getStorage} />
<AsyncRoute path="/system" getComponent={Routes.getSystem} /> <AsyncRoute path="/system" getComponent={Routes.getSystem} />
<AsyncRoute path="/config" getComponent={Routes.getConfig} />
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} /> <AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
<Cameras default path="/" /> <Cameras default path="/" />
</Router> </Router>

99
web/src/routes/Config.jsx Normal file
View File

@ -0,0 +1,99 @@
import { h } from 'preact';
import useSWR from 'swr';
import axios from 'axios';
import { useApiHost } from '../api';
import ActivityIndicator from '../components/ActivityIndicator';
import Heading from '../components/Heading';
import { useEffect, useState } from 'preact/hooks';
import Button from '../components/Button';
import { editor, Uri } from 'monaco-editor';
import { setDiagnosticsOptions } from 'monaco-yaml';
export default function Config() {
const apiHost = useApiHost();
const { data: config } = useSWR('config/raw');
const [success, setSuccess] = useState();
const [error, setError] = useState();
const onHandleSaveConfig = async (e) => {
if (e) {
e.stopPropagation();
}
axios
.post('config/save', window.editor.getValue(), {
headers: { 'Content-Type': 'text/plain' },
})
.then((response) => {
if (response.status === 200) {
setSuccess(response.data);
}
})
.catch((error) => {
if (error.response) {
setError(error.response.data.message);
} else {
setError(error.message);
}
});
};
const handleCopyConfig = async () => {
await window.navigator.clipboard.writeText(window.editor.getValue());
};
useEffect(() => {
if (!config) {
return;
}
const modelUri = Uri.parse('a://b/api/config/schema.json');
setDiagnosticsOptions({
enableSchemaRequest: true,
hover: true,
completion: true,
validate: true,
format: true,
schemas: [
{
uri: `${apiHost}/api/config/schema.json`,
fileMatch: [String(modelUri)],
},
],
});
window.editor = editor.create(document.getElementById('container'), {
language: 'yaml',
model: editor.createModel(config, 'yaml', modelUri),
scrollBeyondLastLine: false,
theme: 'vs-dark',
});
});
if (!config) {
return <ActivityIndicator />;
}
return (
<div className="space-y-4 p-2 px-4 h-full">
<div className="flex justify-between">
<Heading>Config</Heading>
<div>
<Button className="mx-2" onClick={(e) => handleCopyConfig(e)}>
Copy Config
</Button>
<Button className="mx-2" onClick={(e) => onHandleSaveConfig(e)}>
Save & Restart
</Button>
</div>
</div>
{success && <div className="max-h-20 text-green-500">{success}</div>}
{error && <div className="p-4 overflow-scroll text-red-500 whitespace-pre-wrap">{error}</div>}
<div id="container" className="h-full" />
</div>
);
}

View File

@ -7,7 +7,7 @@ import { useWs } from '../api/ws';
import useSWR from 'swr'; import useSWR from 'swr';
import axios from 'axios'; import axios from 'axios';
import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table'; import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table';
import { useCallback, useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import Dialog from '../components/Dialog'; import Dialog from '../components/Dialog';
const emptyObject = Object.freeze({}); const emptyObject = Object.freeze({});
@ -34,13 +34,6 @@ export default function System() {
const gpuNames = Object.keys(gpu_usages || emptyObject); const gpuNames = Object.keys(gpu_usages || emptyObject);
const cameraNames = Object.keys(cameras || emptyObject); const cameraNames = Object.keys(cameras || emptyObject);
const handleCopyConfig = useCallback(() => {
async function copy() {
await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2));
}
copy();
}, [config]);
const onHandleFfprobe = async (camera, e) => { const onHandleFfprobe = async (camera, e) => {
if (e) { if (e) {
e.stopPropagation(); e.stopPropagation();
@ -267,16 +260,6 @@ export default function System() {
<p>System stats update automatically every {config.mqtt.stats_interval} seconds.</p> <p>System stats update automatically every {config.mqtt.stats_interval} seconds.</p>
</Fragment> </Fragment>
)} )}
<div className="relative">
<Heading size="sm">Config</Heading>
<Button className="absolute top-8 right-4" onClick={handleCopyConfig}>
Copy to Clipboard
</Button>
<pre className="overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2 max-h-96">
{JSON.stringify(config, null, 2)}
</pre>
</div>
</div> </div>
); );
} }

View File

@ -38,6 +38,11 @@ export async function getStorage(_url, _cb, _props) {
return module.default; return module.default;
} }
export async function getConfig(_url, _cb, _props) {
const module = await import('./Config.jsx');
return module.default;
}
export async function getStyleGuide(_url, _cb, _props) { export async function getStyleGuide(_url, _cb, _props) {
const module = await import('./StyleGuide.jsx'); const module = await import('./StyleGuide.jsx');
return module.default; return module.default;

View File

@ -2,13 +2,16 @@
import path from "path"; import path from "path";
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import preact from '@preact/preset-vite' import preact from '@preact/preset-vite'
import monacoEditorPlugin from 'vite-plugin-monaco-editor';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
define: { define: {
'import.meta.vitest': 'undefined', 'import.meta.vitest': 'undefined',
}, },
plugins: [preact()], plugins: [preact(), monacoEditorPlugin.default({
customWorkers: [{label: 'yaml', entry: 'monaco-yaml/yaml.worker'}]
})],
test: { test: {
environment: 'jsdom', environment: 'jsdom',
alias: { alias: {