From 4a11c25f62a9d08bef5a2f7d2f3a352cfcb5b691 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Thu, 21 Aug 2025 10:45:50 +0000 Subject: [PATCH 01/11] widget improvements --- .../db-table-widgets.component.ts | 21 +++++- .../boolean/boolean.component.html | 10 ++- .../boolean/boolean.component.ts | 3 + .../number/number.component.css | 4 +- .../number/number.component.html | 2 +- .../number/number.component.ts | 20 +++++ .../text/text.component.css | 6 ++ .../text/text.component.html | 2 +- .../record-view-fields/text/text.component.ts | 75 ++++++++++++++++++- .../boolean/boolean.component.html | 10 ++- .../boolean/boolean.component.ts | 3 + .../number/number.component.css | 4 +- .../number/number.component.html | 2 +- .../number/number.component.ts | 20 +++++ .../text/text.component.css | 6 ++ .../text/text.component.html | 2 +- .../text/text.component.ts | 75 ++++++++++++++++++- 17 files changed, 250 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts index c1e7671cd..8bde8bc08 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts @@ -169,10 +169,13 @@ export class DbTableWidgetsComponent implements OnInit { "allow_negative": true } `, - Number: `// Configure number display with unit conversion + Number: `// Configure number display with unit conversion and threshold validation // Example units: "bytes", "meters", "seconds", "grams" +// threshold_min/threshold_max: Values outside these limits will be highlighted in red { - "unit": null + "unit": null, + "threshold_min": null, + "threshold_max": null }`, Password: `// provide algorithm to encrypt your password, one of: @@ -226,7 +229,19 @@ export class DbTableWidgetsComponent implements OnInit { } ] }`, - String: `// No settings required`, + String: `// Optional validation for string values +// validate: Any validator.js method (e.g., "isEmail", "isURL", "isUUID", "isJSON", "isAlpha", "isNumeric") +// Full list: isEmail, isURL, isMACAddress, isIP, isIPRange, isFQDN, isBoolean, isIBAN, isBIC, +// isAlpha, isAlphanumeric, isNumeric, isPort, isLowercase, isUppercase, isAscii, isBase64, +// isHexadecimal, isHexColor, isRgbColor, isHSL, isMD5, isHash, isJWT, isJSON, isUUID, +// isMongoId, isCreditCard, isISBN, isISSN, isMobilePhone, isPostalCode, isEthereumAddress, +// isCurrency, isBtcAddress, isISO8601, isISO31661Alpha2, isISO31661Alpha3, isISO4217, +// isDataURI, isMagnetURI, isMimeType, isLatLong, isSlug, isStrongPassword, isTaxID, isVAT +// OR use "regex" with a regex parameter for custom pattern matching +{ + "validate": null, + "regex": null +}`, Textarea: `// provide number of strings to show. { "rows": 5 diff --git a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.html b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.html index 6afe2967d..81fbe48e8 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.html @@ -1,3 +1,9 @@ -check_small -close_small + + check_small + + + close_small + diff --git a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.ts b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.ts index 0947ac496..ef3ce9550 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.ts @@ -13,4 +13,7 @@ import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-rec imports: [MatIconModule, CommonModule] }) export class BooleanRecordViewComponent extends BaseRecordViewFieldComponent { + get invertColors(): boolean { + return this.widgetStructure?.widget_params?.invertColors === true; + } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css index 8b1378917..82dac759f 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css @@ -1 +1,3 @@ - +.out-of-threshold { + color: #f44336; +} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html index 8f3bd4c32..c98316b79 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html @@ -1 +1 @@ -{{displayValue}} +{{displayValue}} diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts index bb3773ddf..011036e74 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts @@ -32,4 +32,24 @@ export class NumberRecordViewComponent extends BaseRecordViewFieldComponent { return this.value.toString(); } } + + get isOutOfThreshold(): boolean { + if (this.value == null || this.value === '') { + return false; + } + + const thresholdMin = this.widgetStructure?.widget_params?.threshold_min; + const thresholdMax = this.widgetStructure?.widget_params?.threshold_max; + const numValue = parseFloat(this.value); + + if (thresholdMin !== undefined && numValue < thresholdMin) { + return true; + } + + if (thresholdMax !== undefined && numValue > thresholdMax) { + return true; + } + + return false; + } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.css b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.css index e69de29bb..944378e53 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.css +++ b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.css @@ -0,0 +1,6 @@ +.validation-error { + color: #f44336; + text-decoration: underline; + text-decoration-style: wavy; + text-underline-offset: 2px; +} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.html b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.html index 7dfcb3f4b..d6b7e12f0 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.html @@ -1 +1 @@ -{{value || '—'}} +{{value || '—'}} diff --git a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.ts b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.ts index 788c2c8fb..68cb8dc14 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.ts @@ -1,5 +1,5 @@ import { Component, Injectable } from '@angular/core'; - +import * as validator from 'validator'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; @Injectable() @@ -10,4 +10,77 @@ import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-rec imports: [] }) export class TextRecordViewComponent extends BaseRecordViewFieldComponent { + get isInvalid(): boolean { + if (!this.value || this.value === '') { + return false; + } + + const validateType = this.widgetStructure?.widget_params?.validate; + if (!validateType) { + return false; + } + + const stringValue = String(this.value); + + // Special case for regex validation + if (validateType === 'regex') { + const regexPattern = this.widgetStructure?.widget_params?.regex; + if (!regexPattern) { + return false; + } + try { + const regex = new RegExp(regexPattern); + return !regex.test(stringValue); + } catch (error) { + console.warn('Invalid regex pattern:', error); + return false; + } + } + + // Check if validator has this method + const validatorMethod = validator[validateType]; + if (typeof validatorMethod !== 'function') { + console.warn(`Unknown validator method: ${validateType}`); + return false; + } + + try { + // Call the validator method and invert result (true if invalid) + return !validatorMethod(stringValue); + } catch (error) { + console.warn(`Validation error for ${validateType}:`, error); + return false; + } + } + + get validationErrorMessage(): string { + const validateType = this.widgetStructure?.widget_params?.validate; + if (!validateType) { + return ''; + } + + if (validateType === 'regex') { + return 'Does not match the required pattern'; + } + + // Create user-friendly messages for common validators + const messages = { + isEmail: 'Invalid email address', + isURL: 'Invalid URL', + isIP: 'Invalid IP address', + isUUID: 'Invalid UUID', + isJSON: 'Invalid JSON', + isCreditCard: 'Invalid credit card number', + isISBN: 'Invalid ISBN', + isAlpha: 'Should contain only letters', + isNumeric: 'Should contain only numbers', + isAlphanumeric: 'Should contain only letters and numbers', + isHexColor: 'Invalid hex color', + isBase64: 'Invalid Base64 string', + isMobilePhone: 'Invalid mobile phone number', + isPostalCode: 'Invalid postal code' + }; + + return messages[validateType] || `Invalid ${validateType.replace(/^is/, '').toLowerCase()}`; + } } diff --git a/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.html b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.html index a83870454..4012df024 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.html @@ -1,7 +1,13 @@
- check_small - close_small + + check_small + + + close_small +
diff --git a/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.ts b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.ts index dc78799f4..b56ddec31 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.ts @@ -15,4 +15,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule, CommonModule] }) export class BooleanDisplayComponent extends BaseTableDisplayFieldComponent { + get invertColors(): boolean { + return this.widgetStructure?.widget_params?.invertColors === true; + } } diff --git a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css index 8b1378917..82dac759f 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css +++ b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css @@ -1 +1,3 @@ - +.out-of-threshold { + color: #f44336; +} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html index 356e592cb..8e72705c6 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html @@ -1,5 +1,5 @@
- {{displayValue}} + {{displayValue}} } - + @if (value && validateUuid(value)) { UUID {{ getUuidVersion(value) ? 'v' + getUuidVersion(value) : 'version unknown' }} } - + @if (value && !validateUuid(value)) { Invalid UUID format } diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css index 82dac759f..893313654 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css @@ -1,3 +1,11 @@ .out-of-threshold { - color: #f44336; + display: flex; + align-items: center; + gap: 2px; +} + +.out-of-threshold__icon { + font-size: 16px; + width: 16px; + height: 16px; } \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html index c98316b79..55d954de1 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html @@ -1 +1,5 @@ -{{displayValue}} + + {{displayValue}} + north + south + diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts index 011036e74..1cd47b51a 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts @@ -1,5 +1,8 @@ import { Component, Injectable } from '@angular/core'; + import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; +import { CommonModule } from '@angular/common'; +import { MatIconModule } from '@angular/material/icon'; import convert from 'convert'; @Injectable() @@ -7,7 +10,7 @@ import convert from 'convert'; selector: 'app-number-record-view', templateUrl: './number.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './number.component.css'], - imports: [] + imports: [CommonModule, MatIconModule] }) export class NumberRecordViewComponent extends BaseRecordViewFieldComponent { @@ -33,7 +36,7 @@ export class NumberRecordViewComponent extends BaseRecordViewFieldComponent { } } - get isOutOfThreshold(): boolean { + get isOutOfThreshold(): 'up' | 'down' | false { if (this.value == null || this.value === '') { return false; } @@ -43,11 +46,11 @@ export class NumberRecordViewComponent extends BaseRecordViewFieldComponent { const numValue = parseFloat(this.value); if (thresholdMin !== undefined && numValue < thresholdMin) { - return true; + return 'down'; } if (thresholdMax !== undefined && numValue > thresholdMax) { - return true; + return 'up'; } return false; diff --git a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.css b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.css new file mode 100644 index 000000000..bc99e1d74 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.css @@ -0,0 +1,64 @@ +.range-display-container { + display: flex; + flex-direction: column; + gap: 4px; + padding: 4px 0; +} + +.range-value-label { + font-size: 14px; + color: rgba(0, 0, 0, 0.87); + font-weight: 500; +} + +.range-progress-bar { + height: 8px; + border-radius: 4px; + min-width: 160px; +} + +/* Material progress bar specific styles */ +.range-progress-bar.mat-mdc-progress-bar { + height: 8px; +} + +/* Light theme progress bar styles */ +.range-progress-bar { + --mdc-linear-progress-active-indicator-color: var(--color-accentedPalette-300); + --mdc-linear-progress-track-color: #e0e0e0; + --mdc-linear-progress-active-indicator-height: 8px; + --mdc-linear-progress-track-height: 8px; +} + +/* Override Material default styles */ +.range-progress-bar ::ng-deep .mdc-linear-progress { + height: 8px !important; +} + +.range-progress-bar ::ng-deep .mdc-linear-progress__bar { + height: 8px !important; +} + +.range-progress-bar ::ng-deep .mdc-linear-progress__bar-inner { + border-top-width: 8px !important; +} + +.range-progress-bar ::ng-deep .mdc-linear-progress__buffer { + height: 8px !important; +} + +.range-progress-bar ::ng-deep .mdc-linear-progress__buffer-bar { + height: 8px !important; +} + +/* Dark theme styles */ +@media (prefers-color-scheme: dark) { + .range-value-label { + color: rgba(255, 255, 255, 0.87); + } + + .range-progress-bar { + --mdc-linear-progress-active-indicator-color: var(--color-accentedPalette-700); + --mdc-linear-progress-track-color: rgba(255, 255, 255, 0.12); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.html b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.html new file mode 100644 index 000000000..673c6a710 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.html @@ -0,0 +1,8 @@ +
+
{{ displayValue }}
+ + +
\ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts new file mode 100644 index 000000000..8ba3118d5 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts @@ -0,0 +1,69 @@ +import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; + +import { Component, Input, OnChanges, OnInit } from '@angular/core'; + +import { CommonModule } from '@angular/common'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; + +@Component({ + selector: 'app-range-record-view', + standalone: true, + templateUrl: './range.component.html', + styleUrls: ['./range.component.css'], + imports: [ + CommonModule, + MatProgressBarModule + ], +}) +export class RangeRecordViewComponent extends BaseRecordViewFieldComponent implements OnInit, OnChanges { + @Input() declare value: number; + static type = 'range'; + + public min: number = 0; + public max: number = 100; + public step: number = 1; + public displayValue: string = ''; + + ngOnInit(): void { + this._parseWidgetParams(); + this._updateDisplayValue(); + } + + ngOnChanges(): void { + this._parseWidgetParams(); + this._updateDisplayValue(); + } + + public getProgressValue(): number { + const numValue = Number(this.value) || 0; + const range = this.max - this.min; + if (range === 0) return 0; + const progress = ((numValue - this.min) / range) * 100; + // Ensure progress is between 0 and 100 + return Math.max(0, Math.min(100, progress)); + } + + private _parseWidgetParams(): void { + if (this.widgetStructure?.widget_params) { + try { + const params = this.widgetStructure.widget_params; + if (params.min !== undefined) { + this.min = Number(params.min) || 0; + } + if (params.max !== undefined) { + this.max = Number(params.max) || 100; + } + if (params.step !== undefined) { + this.step = Number(params.step) || 1; + } + } catch (error) { + console.error('Failed to parse widget params:', error); + } + } + } + + private _updateDisplayValue(): void { + const numValue = Number(this.value) || 0; + this.displayValue = `${numValue} / ${this.max}`; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css index 82dac759f..bc463bdea 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css +++ b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css @@ -1,3 +1,11 @@ .out-of-threshold { - color: #f44336; -} \ No newline at end of file + display: flex; + align-items: center; + gap: 2px; +} + +.out-of-threshold__icon { + font-size: 16px; + width: 16px; + height: 16px; +} diff --git a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html index 8e72705c6..50af36655 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html @@ -1,5 +1,9 @@
- {{displayValue}} + + {{displayValue}} + south + north +