Skip to content

Commit

Permalink
add plugin config validation functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
bwp91 committed Jan 24, 2025
1 parent 5f20fb8 commit 0e6b740
Show file tree
Hide file tree
Showing 34 changed files with 195 additions and 15 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,32 @@ All notable changes to `homebridge-config-ui-x` will be documented in this file.

## BETA

### ⚠️ Plugin Config Validation

This version of Homebridge UI adds validation to plugin config screens. This does not apply to manual plugin configuration (with raw `JSON`).

- If your entered configuration is valid, then you will see a green tick by the save button.
- If your configuration is not valid, then you will see an orange warning triangle by the save button. You will still be allowed to save the configuration.
- A plugin can enforce strict validation:
- For plugin developers: if you want to enforce a valid configuration, you can add `"strictValidation": true` as a root property to your `config.schema.json` file.
- For users: if the plugin developer has enabled this setting and your configuration is invalid, then you will see a red warning triangle by the save button. You will not be allowed to save the configuration until it is valid.

If you have a plugin which can be configured multiple times, then an icon will be shown on each configuration block.

Plugin developers:

- Please do not rely on this validation and assume that a user's configuration will be valid: you should still validate the user's config on plugin startup
- Remember that some users do not use the UI at all, and other users may prefer to configure using raw `JSON` rather than config screens
- For custom UI screens, the validation icon will be hidden when the save button is disabled (using `homebridge.disableSaveButton()` from `@homebridge/plugin-ui-utils`)
- Please report any cases where the validation is not working as expected

### UI Changes

- updates to the `uk.json` language file (#2312) (@xrust83)
- updates to the `cs.json` language file (#2332) (@DavidHuljak)
- fix typos in hb/ui settings schemas (#2317) (@dnicolson)
- fix margins in update plugin modal
- add plugin config validation functionality

### Other Changes

Expand Down
1 change: 1 addition & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"pluginAlias": "config",
"pluginType": "platform",
"singular": true,
"strictValidation": true,
"schema": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ <h5 class="modal-title">{{ plugin.displayName || plugin.name }}</h5>
[configSchema]="schema"
[(data)]="pluginConfig[0]"
(dataChanged)="schemaFormUpdatedSubject.next($event)"
(isValid)="onIsValid($event)"
>
</app-schema-form>
</div>
Expand Down Expand Up @@ -75,7 +76,18 @@ <h5 class="modal-title">{{ plugin.displayName || plugin.name }}</h5>
</button>
</div>
<div class="text-center"></div>
<div class="text-right">
<div class="text-right d-flex align-items-center justify-content-end">
@if (!saveButtonDisabled) {
<i
class="fa fa-fw fa-2xl mr-1"
[ngClass]="formIsValid ? 'fa-circle-check green-text' : (strictValidation ? 'fa-circle-exclamation red-text' : 'fa-circle-exclamation orange-text')"
[ngbTooltip]="(formIsValid ? 'form.label_valid' : (strictValidation ? 'form.label_invalid_strict' : 'form.label_invalid')) | translate"
container="modal"
placement="top"
openDelay="150"
triggers="hover"
></i>
}
<button
type="button"
class="btn btn-primary"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NgClass } from '@angular/common'
import { Component, ElementRef, inject, Input, OnDestroy, OnInit, viewChild } from '@angular/core'
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbActiveModal, NgbModal, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { TranslatePipe, TranslateService } from '@ngx-translate/core'
import { ToastrService } from 'ngx-toastr'
import { firstValueFrom, Subject } from 'rxjs'
Expand All @@ -21,6 +22,8 @@ import { environment } from '@/environments/environment'
standalone: true,
imports: [
SchemaFormComponent,
NgbTooltip,
NgClass,
TranslatePipe,
],
})
Expand Down Expand Up @@ -58,6 +61,8 @@ export class CustomPluginsComponent implements OnInit, OnDestroy {
public formActionSubject = new Subject()
public childBridges: any[] = []
public isFirstSave = false
public formIsValid = true
public strictValidation = false

private io: IoNamespace
private basePath: string
Expand All @@ -72,6 +77,7 @@ export class CustomPluginsComponent implements OnInit, OnDestroy {
this.io = this.$ws.connectToNamespace('plugins/settings-ui')
this.pluginAlias = this.schema.pluginAlias
this.pluginType = this.schema.pluginType
this.strictValidation = this.schema.strictValidation

if (this.pluginConfig.length === 0) {
this.isFirstSave = true
Expand Down Expand Up @@ -496,6 +502,10 @@ export class CustomPluginsComponent implements OnInit, OnDestroy {
}
}

onIsValid($event: boolean) {
this.formIsValid = $event
}

ngOnDestroy() {
window.removeEventListener('message', this.handleMessage)
this.io.end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,42 @@ <h5 class="modal-title">{{ plugin.displayName || plugin.name }}</h5>
<!-- MULTIPLE CONFIG BLOCKS-->
@if (pluginConfig.length && !schema.singular) {
<div ngbAccordion [closeOthers]="true" (show)="blockShown($event)" (hide)="blockHidden($event)">
@for (block of pluginConfig; track block) {
@for (block of pluginConfig; track block; let i = $index) {
<div [ngbAccordionItem]="block.__uuid__" class="card" [collapsed]="show !== block.__uuid__" [id]="block.__uuid__">
<div ngbAccordionHeader class="card-header">
<div class="d-flex align-items-center justify-content-between">
<h5 class="m-0">{{ block.name }}</h5>
<h5 class="m-0">
<i
class="fa fa-fw"
[ngClass]="formBlocksValid[i] ? 'fa-circle-check green-text' : (strictValidation ? 'fa-circle-exclamation red-text' : 'fa-circle-exclamation orange-text')"
[ngbTooltip]="(formBlocksValid[i] ? 'form.label_valid' : (strictValidation ? 'form.label_invalid_strict' : 'form.label_invalid')) | translate"
container="modal"
placement="right"
openDelay="150"
triggers="hover"
></i>
{{ block.name }}
</h5>
<div>
@if (plugin.name !== 'homebridge-config-ui-x' && show === block.__uuid__) {
<button
class="btn btn-danger ml-2"
(click)="removeBlock(block.__uuid__)"
[ngbTooltip]="'form.button_delete' | translate"
container="modal"
placement="left"
openDelay="150"
triggers="hover"
>
<i class="fa fa-fw fa-trash"></i>
</button>
}
<button
class="btn btn-primary ml-2"
class="btn btn-primary ml-2 mr-0"
ngbAccordionButton
[ngbTooltip]="'form.button_edit' | translate"
container="modal"
placement="left"
openDelay="150"
triggers="hover"
>
Expand All @@ -53,7 +66,11 @@ <h5 class="m-0">{{ block.name }}</h5>
</div>
<div ngbAccordionCollapse>
<div ngbAccordionBody class="card-body">
<app-schema-form [configSchema]="schema" [(data)]="block.config"></app-schema-form>
<app-schema-form
[configSchema]="schema"
[(data)]="block.config"
(isValid)="onIsValid($event, i)"
></app-schema-form>
</div>
</div>
</div>
Expand All @@ -64,7 +81,11 @@ <h5 class="m-0">{{ block.name }}</h5>
<!-- SINGLE CONFIG BLOCK ONLY -->
@if (pluginConfig.length && schema.singular) {
<div class="card card-body">
<app-schema-form [configSchema]="schema" [(data)]="pluginConfig[0].config"></app-schema-form>
<app-schema-form
[configSchema]="schema"
[(data)]="pluginConfig[0].config"
(isValid)="onIsValid($event, 0)"
></app-schema-form>
@if (plugin.name==='homebridge-deconz') {
<app-homebridge-deconz></app-homebridge-deconz>
} @if (plugin.name==='homebridge-hue') {
Expand Down Expand Up @@ -92,14 +113,28 @@ <h5 class="m-0">{{ block.name }}</h5>
</button>
</div>
<div class="text-center"></div>
<div class="text-right">
@if (!schema.singular) {
<button type="button" class="btn btn-elegant" data-dismiss="modal" (click)="addBlock()">
<i class="fa fa-fw fa-plus"></i>
</button>
<div class="text-right d-flex align-items-center justify-content-end">
@if (schema.singular) {
<i
class="fa fa-fw fa-2xl mr-1"
[ngClass]="formIsValid ? 'fa-circle-check green-text' : (strictValidation ? 'fa-circle-exclamation red-text' : 'fa-circle-exclamation orange-text')"
[ngbTooltip]="(formIsValid ? 'form.label_valid' : (strictValidation ? 'form.label_invalid_strict' : 'form.label_invalid')) | translate"
container="modal"
placement="top"
openDelay="150"
triggers="hover"
></i>
}
<button type="button" class="btn btn-primary" data-dismiss="modal" (click)="save()" [disabled]="saveInProgress">
@if (!saveInProgress) { {{ 'form.button_save' | translate }} } @if (saveInProgress) {
<button
type="button"
class="btn btn-primary"
data-dismiss="modal"
(click)="save()"
[disabled]="saveInProgress || (!formIsValid && strictValidation)"
>
@if (!saveInProgress) {
<span>{{ 'form.button_save' | translate }}</span>
} @if (saveInProgress) {
<i class="fas fa-fw fa-spinner fa-pulse"></i>
}
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NgClass } from '@angular/common'
import { Component, inject, Input, OnInit } from '@angular/core'
import { NgbAccordionBody, NgbAccordionButton, NgbAccordionCollapse, NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem, NgbActiveModal, NgbModal, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { TranslatePipe, TranslateService } from '@ngx-translate/core'
Expand Down Expand Up @@ -26,6 +27,7 @@ export interface PluginConfigBlock {
export interface PluginSchema {
pluginAlias: string
pluginType: 'platform' | 'accessory'
strictValidation?: boolean
singular?: boolean
headerDisplay?: string
footerDisplay?: string
Expand All @@ -48,6 +50,7 @@ export interface PluginSchema {
NgbAccordionButton,
NgbAccordionCollapse,
NgbAccordionBody,
NgClass,
SchemaFormComponent,
HomebridgeDeconzComponent,
HomebridgeHueComponent,
Expand Down Expand Up @@ -75,12 +78,16 @@ export class PluginConfigComponent implements OnInit {
public saveInProgress: boolean
public childBridges: any[] = []
public isFirstSave = false
public formBlocksValid: { [key: number]: boolean } = {}
public formIsValid = true
public strictValidation = false

constructor() {}

ngOnInit() {
this.pluginAlias = this.schema.pluginAlias
this.pluginType = this.schema.pluginType
this.strictValidation = this.schema.strictValidation
this.loadPluginConfig()
}

Expand Down Expand Up @@ -187,12 +194,18 @@ export class PluginConfigComponent implements OnInit {
},
})

this.formBlocksValid[this.pluginConfig.length - 1] = false
this.blockShown(__uuid__)
}

removeBlock(__uuid__: string) {
const pluginConfigIndex = this.pluginConfig.findIndex(x => x.__uuid__ === __uuid__)
this.pluginConfig.splice(pluginConfigIndex, 1)

delete this.formBlocksValid[pluginConfigIndex]
if (!Object.keys(this.formBlocksValid).length) {
this.formIsValid = true
}
}

async getChildBridges(): Promise<void> {
Expand Down Expand Up @@ -229,4 +242,9 @@ export class PluginConfigComponent implements OnInit {
}
}
}

onIsValid($event: boolean, index: number) {
this.formBlocksValid[index] = $event
this.formIsValid = Object.values(this.formBlocksValid).every(x => x)
}
}
3 changes: 3 additions & 0 deletions ui/src/i18n/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Unlock",
"form.button_unpair": "Unpair",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automatic (Use Browser Settings)",
"login.button_login": "Вписване",
"login.invalid_credentials": "Грешно потребителско име или парола",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Desbloquejar",
"form.button_unpair": "Unpair",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automatic (Use Browser Settings)",
"login.button_login": "Iniciar Sesión",
"login.invalid_credentials": "Usuari o contrasenya incorrecte",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Odemknout",
"form.button_unpair": "Odpárovat",
"form.label.changes_kept": "Upozorňujeme, že stávající výběry/změny zůstávají zachovány při přepínání mezi položkami.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automaticky (Použijí se nastavení prohlížeče)",
"login.button_login": "Přihlásit se",
"login.invalid_credentials": "Neplatné uživatelské jméno a heslo",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Freischalten",
"form.button_unpair": "Entkoppeln",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automatisch (Browsereinstellungen verwenden)",
"login.button_login": "Anmeldung",
"login.invalid_credentials": "Ungültiger Benutzername und / oder Passwort",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Unlock",
"form.button_unpair": "Unpair",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automatic (Use Browser Settings)",
"login.button_login": "Log in",
"login.invalid_credentials": "Invalid Username or Password",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Desbloquear",
"form.button_unpair": "Desvincular",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automático (Usar ajustes del navegador)",
"login.button_login": "Iniciar sesión",
"login.invalid_credentials": "Usuario o contraseña incorrectos",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Poista lukitus",
"form.button_unpair": "Pura pariliitos",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automattinen (Käytä selaimen asetuksia)",
"login.button_login": "Kirjaudu",
"login.invalid_credentials": "Väärä käyttäjätunnus tai salasana",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Déverrouiller",
"form.button_unpair": "Unpair",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automatic (Use Browser Settings)",
"login.button_login": "Connexion",
"login.invalid_credentials": "Nom d'utilisateur ou mot de passe incorrect",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Unlock",
"form.button_unpair": "Unpair",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automatic (Use Browser Settings)",
"login.button_login": "התחבר",
"login.invalid_credentials": "שם משתמש וסיסמה לא תקינים",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Unlock",
"form.button_unpair": "Unpair",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automatic (Use Browser Settings)",
"login.button_login": "Bejelentkezés",
"login.invalid_credentials": "Hibás felhasználónév vagy jelszó",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/i18n/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"form.button_unlock": "Unlock",
"form.button_unpair": "Unpair",
"form.label.changes_kept": "Note that existing selections/changes are preserved when switching between items.",
"form.label_invalid": "Config validation failed - you can still save your changes.",
"form.label_invalid_strict": "Config validation failed - please review the form before saving.",
"form.label_valid": "Config validation passed.",
"form.select.auto": "Automatic (Use Browser Settings)",
"login.button_login": "Masuk",
"login.invalid_credentials": "Nama Pengguna dan Kata Sandi Salah",
Expand Down
Loading

0 comments on commit 0e6b740

Please sign in to comment.