Browse Source

Merge branch 'feature/iot-hub' into feature/lts-4.3-iot-hub

pull/15548/head
Igor Kulikov 3 months ago
parent
commit
cf2b9eeacd
  1. 16
      ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html
  2. 28
      ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss
  3. 33
      ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts
  4. 28
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss
  5. 13
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
  6. 51
      ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts
  7. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

16
ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html

@ -62,6 +62,14 @@
(click)="passwordVisible[field.key] = !passwordVisible[field.key]">
<mat-icon>{{ passwordVisible[field.key] ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>
@if (field.randomGenerator) {
<button mat-icon-button matSuffix type="button"
[matTooltip]="'iot-hub.generate-random-value' | translate"
matTooltipPosition="above"
(click)="regenerateFieldValue(ws, field)">
<mat-icon>autorenew</mat-icon>
</button>
}
@if (ws.formGroup.controls[field.key]?.hasError('required')) {
<mat-error>{{ field.label + ' is required' }}</mat-error>
}
@ -86,6 +94,14 @@
<mat-form-field appearance="outline">
<mat-label>{{ field.label }}</mat-label>
<input matInput [formControlName]="field.key">
@if (field.randomGenerator) {
<button mat-icon-button matSuffix type="button"
[matTooltip]="'iot-hub.generate-random-value' | translate"
matTooltipPosition="above"
(click)="regenerateFieldValue(ws, field)">
<mat-icon>autorenew</mat-icon>
</button>
}
@if (ws.formGroup.controls[field.key]?.hasError('required')) {
<mat-error>{{ field.label + ' is required' }}</mat-error>
}

28
ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss

@ -210,6 +210,34 @@
}
}
.tb-doc-link-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
margin: 4px 8px 4px 0;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
background: #fff;
color: rgba(0, 0, 0, 0.76);
font-size: 14px;
font-weight: 500;
line-height: 20px;
text-decoration: none;
cursor: pointer;
i.material-icons {
font-size: 18px;
line-height: 18px;
color: rgba(0, 0, 0, 0.54);
}
&:hover {
background: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 0, 0, 0.24);
}
}
.tb-callout {
display: flex;
align-items: flex-start;

33
ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts

@ -33,6 +33,7 @@ import { RuleChainService } from '@core/http/rule-chain.service';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import { generateSecret } from '@core/utils';
import {
installMethodLabels as INSTALL_METHOD_LABELS,
peOnlyInstallMethods,
@ -44,6 +45,7 @@ import {
FormFieldDefinition,
FormFieldType,
InstallStepType,
resolveDocLinkPlaceholders,
stepTypeAliasMap
} from '@shared/models/iot-hub/device-package.models';
@ -74,6 +76,7 @@ export interface WizardStep {
}
const ENTITY_STEP_MIN_DELAY = 2000;
const DEFAULT_RANDOM_SIZE = 20;
@Component({
selector: 'tb-device-install-dialog',
@ -422,6 +425,14 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
// --- Variable Resolution ---
resolveVariables(content: string): string {
if (this.packageInfo) {
content = resolveDocLinkPlaceholders(
content,
this.packageInfo.name || '',
{ productURL: this.packageInfo.productURL, datasheetURL: this.packageInfo.datasheetURL },
{ productPage: 'Product page', datasheet: 'Datasheet' }
);
}
return content.replace(/\$\{([^}]+)}/g, (_match, key) => {
if (key in this.formValues) {
return String(this.formValues[key]);
@ -591,7 +602,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
if (field.validators?.length > 0) {
validators.push(Validators.pattern(field.validators[0].pattern));
}
const initialValue = storedValues?.[field.key] ?? field.defaultValue ?? (field.type === FormFieldType.BOOLEAN ? false : '');
const initialValue = this.resolveInitialFieldValue(field, storedValues?.[field.key]);
controls[field.key] = new UntypedFormControl(initialValue, validators);
if (field.type === FormFieldType.PASSWORD) {
this.passwordVisible[field.key] = true; // Show passwords in review mode
@ -604,6 +615,26 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
}
}
private resolveInitialFieldValue(field: FormFieldDefinition, storedValue: any): any {
if (storedValue !== undefined) {
return storedValue;
}
const isStringLike = field.type === FormFieldType.STRING || field.type === FormFieldType.PASSWORD;
if (isStringLike && field.randomByDefault) {
return generateSecret(field.randomSize ?? DEFAULT_RANDOM_SIZE);
}
return field.defaultValue ?? (field.type === FormFieldType.BOOLEAN ? false : '');
}
regenerateFieldValue(ws: WizardStep, field: FormFieldDefinition): void {
const control = ws.formGroup?.controls[field.key];
if (!control) {
return;
}
control.patchValue(generateSecret(field.randomSize ?? DEFAULT_RANDOM_SIZE));
control.markAsDirty();
}
private onStepActivated(): void {
const step = this.currentWizardStep;
if (!step) {

28
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss

@ -551,6 +551,34 @@
code:not([class*=language-]) {
font-size: 14px;
}
.tb-doc-link-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
margin: 4px 8px 4px 0;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
background: #fff;
color: rgba(0, 0, 0, 0.76);
font-size: 14px;
font-weight: 500;
line-height: 20px;
text-decoration: none;
cursor: pointer;
i.material-icons {
font-size: 18px;
line-height: 18px;
color: rgba(0, 0, 0, 0.54);
}
&:hover {
background: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 0, 0, 0.24);
}
}
}
}

13
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts

@ -24,6 +24,7 @@ import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTransl
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { resolveDocLinkPlaceholders } from '@shared/models/iot-hub/device-package.models';
import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
@ -299,7 +300,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
private loadReadme(): void {
const versionId = this.item.id as string;
this.iotHubApiService.getVersionReadme(versionId, { ignoreLoading: true }).subscribe(
content => this.readmeContent = this.prefixResourceUrls(content || '')
content => this.readmeContent = this.resolveDocLinks(this.prefixResourceUrls(content || ''))
);
}
@ -307,4 +308,14 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
const baseUrl = this.iotHubApiService.baseUrl;
return markdown.replace(/([("])(\/api\/resources\/[^)"]*)/g, `$1${baseUrl}$2`);
}
private resolveDocLinks(markdown: string): string {
const dd = this.item.dataDescriptor;
return resolveDocLinkPlaceholders(
markdown,
this.item.name || dd?.name || '',
{ productURL: dd?.productURL, datasheetURL: dd?.datasheetURL },
{ productPage: 'Product page', datasheet: 'Datasheet' }
);
}
}

51
ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts

@ -211,6 +211,8 @@ export interface DevicePackageInfo {
hardwareType: string;
installMethods: string[];
installSteps: Record<string, DeviceInstallStep[]>;
productURL?: string;
datasheetURL?: string;
}
export enum FormFieldType {
@ -241,6 +243,9 @@ export interface FormFieldDefinition {
helpImage?: string;
validators?: FormFieldValidator[];
options?: FormFieldOption[];
randomGenerator?: boolean;
randomSize?: number;
randomByDefault?: boolean;
}
export interface EntityStepOutput {
@ -265,3 +270,49 @@ export interface EntityStepProgress {
conflictType?: ConflictType;
resolution?: string;
}
export interface DocLinks {
productURL?: string;
datasheetURL?: string;
}
export interface DocLinkLabels {
productPage: string;
datasheet: string;
}
export function resolveDocLinkPlaceholders(
markdown: string,
name: string,
links: DocLinks,
labels: DocLinkLabels
): string {
return markdown
.replace(/\$\{product\.button}/g, () =>
links.productURL ? buildDocLinkButton(links.productURL, `${name} ${labels.productPage}`, 'open_in_new') : '')
.replace(/\$\{datasheet\.button}/g, () =>
links.datasheetURL ? buildDocLinkButton(links.datasheetURL, `${name} ${labels.datasheet}`, 'description') : '');
}
function buildDocLinkButton(url: string, text: string, icon: string): string {
const safeUrl = escapeHtmlAttr(url);
const safeText = escapeHtml(text);
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer" class="tb-doc-link-btn">` +
`<i class="material-icons">${icon}</i><span>${safeText}</span></a>`;
}
function escapeHtml(value: string): string {
return value.replace(/[&<>]/g, ch => ch === '&' ? '&amp;' : ch === '<' ? '&lt;' : '&gt;');
}
function escapeHtmlAttr(value: string): string {
return value.replace(/[&<>"']/g, ch => {
switch (ch) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
default: return '&#39;';
}
});
}

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -4190,6 +4190,7 @@
},
"iot-hub": {
"iot-hub": "IoT Hub",
"generate-random-value": "Generate random value",
"home-title": "ThingsBoard IoT Hub",
"home-subtitle-prefix": "Discover ready-to-use",
"and-devices": "& Devices",

Loading…
Cancel
Save