Browse Source

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

pull/15540/head
Igor Kulikov 4 months ago
parent
commit
3b35fb3922
  1. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html
  2. 14
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss
  3. 13
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts
  4. 22
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts
  5. 6
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html
  6. 19
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
  7. 8
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html
  8. 8
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss
  9. 24
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts
  10. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss
  11. 27
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts
  12. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html
  13. 21
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  14. 23
      ui-ngx/src/app/shared/directives/chip-overflow.directive.ts
  15. 17
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html

@ -29,7 +29,7 @@
<th mat-header-cell *matHeaderCellDef>{{ 'iot-hub.item-type' | translate }}</th>
<td mat-cell *matCellDef="let item">
<span class="tb-installed-type" [ngClass]="getItemTypeChipClass(item.itemType)">
<mat-icon class="tb-installed-type-icon">{{ getItemTypeIcon(item.itemType) }}</mat-icon>
<tb-icon class="tb-installed-type-icon">{{ getItemTypeIcon(item.itemType) }}</tb-icon>
{{ getItemTypeLabel(item.itemType) }}
</span>
</td>

14
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss

@ -122,12 +122,14 @@
--mat-icon-color: currentColor;
}
.tb-type-widget { color: rgb(32, 115, 61); background: rgba(46, 166, 88, 0.06); }
.tb-type-dashboard { color: rgb(61, 76, 166); background: rgba(61, 76, 166, 0.06); }
.tb-type-solution-template { color: rgb(0, 137, 186); background: rgba(0, 137, 186, 0.06); }
.tb-type-calc-field { color: rgb(40, 120, 148); background: rgba(40, 120, 148, 0.06); }
.tb-type-rule-chain { color: rgb(178, 121, 29); background: rgba(179, 121, 29, 0.06); }
.tb-type-device { color: rgb(24, 146, 110); background: rgba(24, 146, 110, 0.06); }
// Colors mirror itemTypeChipColors from the iot-hub mp-item.models.
.tb-type-device { color: #3d4ca6; background: rgba(61, 76, 166, 0.06); }
.tb-type-solution-template { color: #2c6cb4; background: rgba(44, 108, 180, 0.06); }
.tb-type-widget { color: #20733d; background: rgba(46, 166, 88, 0.06); }
.tb-type-calc-field { color: #3db5e0; background: rgba(61, 181, 224, 0.06); }
.tb-type-alarm-rule { color: #d7702f; background: rgba(215, 112, 47, 0.06); }
.tb-type-rule-chain { color: #aa5be3; background: rgba(170, 91, 227, 0.06); }
.tb-type-dashboard { color: #607d8b; background: rgba(96, 125, 139, 0.06); }
// "vX.X.X Available" button — Design: border rgba(0,0,0,0.12), rounded-4, px-12 py-6, gap-8
.tb-installed-version-btn {

13
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts

@ -44,7 +44,7 @@ import {
ItemPublishedVersionInfo
} from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service';
@ -153,16 +153,7 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges,
}
getItemTypeIcon(itemType: string): string {
switch (itemType) {
case 'WIDGET': return 'widgets';
case 'DASHBOARD': return 'dashboard';
case 'SOLUTION_TEMPLATE': return 'integration_instructions';
case 'CALCULATED_FIELD': return 'functions';
case 'ALARM_RULE': return 'notification_important';
case 'RULE_CHAIN': return 'settings_ethernet';
case 'DEVICE': return 'memory';
default: return 'category';
}
return getItemTypeIcon(itemType);
}
deleteItem(item: IotHubInstalledItem): void {

22
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts

@ -16,7 +16,7 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models';
import { getItemTypeIcon, ItemType } from '@shared/models/iot-hub/iot-hub-item.models';
import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { TranslateService } from '@ngx-translate/core';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
@ -69,17 +69,21 @@ export class TbIotHubItemCardComponent {
getPlaceholderIcon(): string {
switch (this.item.type) {
case ItemType.WIDGET: return 'widgets';
case ItemType.DASHBOARD: return 'dashboard';
case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions';
case ItemType.CALCULATED_FIELD:
return this.item.icon || cfTypeIcons.get(this.item.dataDescriptor?.cfType) || 'functions';
return this.item.icon
|| cfTypeIcons.get(this.item.dataDescriptor?.cfType)
|| getItemTypeIcon(ItemType.CALCULATED_FIELD);
case ItemType.ALARM_RULE:
return this.item.icon || 'notification_important';
return this.item.icon || getItemTypeIcon(ItemType.ALARM_RULE);
case ItemType.RULE_CHAIN:
return this.item.icon || (this.item.dataDescriptor?.ruleChainType === 'EDGE' ? 'router' : 'device_hub');
case ItemType.DEVICE: return 'memory';
default: return 'extension';
return this.item.icon
|| (this.item.dataDescriptor?.ruleChainType === 'EDGE'
? 'router'
: (this.item.dataDescriptor?.ruleChainType === 'CORE'
? 'device_hub'
: getItemTypeIcon(ItemType.RULE_CHAIN)));
default:
return getItemTypeIcon(this.item.type);
}
}

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

@ -203,7 +203,7 @@
@if (item.dataDescriptor?.connectivity?.length) {
<div class="dlg-meta-group dlg-meta-details-section">
<span class="dlg-meta-label">{{ 'iot-hub.connectivity' | translate }}</span>
<div class="dlg-meta-chips" tb-chip-overflow chipSelector=".dlg-chip" overflowClass="dlg-chip dlg-chip-category" [gap]="4" [minChips]="2">
<div class="dlg-meta-chips" tb-chip-overflow chipSelector=".dlg-chip" overflowClass="dlg-chip dlg-chip-category" [gap]="4" [minChips]="2" showOverflowedTitle>
@for (conn of item.dataDescriptor.connectivity; track conn) {
<span class="dlg-chip dlg-chip-category">{{ conn }}</span>
}
@ -214,7 +214,7 @@
@if (item.categories?.length) {
<div class="dlg-meta-group dlg-meta-details-section">
<span class="dlg-meta-label">{{ 'iot-hub.category' | translate }}</span>
<div class="dlg-meta-chips" tb-chip-overflow chipSelector=".dlg-chip" overflowClass="dlg-chip dlg-chip-category" [gap]="4" [minChips]="2">
<div class="dlg-meta-chips" tb-chip-overflow chipSelector=".dlg-chip" overflowClass="dlg-chip dlg-chip-category" [gap]="4" [minChips]="2" showOverflowedTitle>
@for (cat of item.categories; track cat) {
<span class="dlg-chip dlg-chip-category">{{ cat }}</span>
}
@ -225,7 +225,7 @@
@if (item.useCases?.length) {
<div class="dlg-meta-group dlg-meta-details-section">
<span class="dlg-meta-label">{{ 'iot-hub.use-cases' | translate }}</span>
<div class="dlg-meta-chips" tb-chip-overflow chipSelector=".dlg-chip" overflowClass="dlg-chip dlg-chip-usecase" [gap]="4">
<div class="dlg-meta-chips" tb-chip-overflow chipSelector=".dlg-chip" overflowClass="dlg-chip dlg-chip-usecase" [gap]="4" showOverflowedTitle>
@for (uc of item.useCases; track uc) {
<span class="dlg-chip dlg-chip-usecase">{{ uc }}</span>
}

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

@ -21,7 +21,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, NodeInfo } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { getItemTypeIcon, 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 { TranslateService } from '@ngx-translate/core';
@ -103,16 +103,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
}
getTypeIcon(): string {
switch (this.item.type) {
case ItemType.WIDGET: return 'widgets';
case ItemType.DASHBOARD: return 'dashboard';
case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions';
case ItemType.CALCULATED_FIELD: return 'functions';
case ItemType.ALARM_RULE: return 'notification_important';
case ItemType.RULE_CHAIN: return 'account_tree';
case ItemType.DEVICE: return 'memory';
default: return 'category';
}
return getItemTypeIcon(this.item.type);
}
getCompactIcon(): string {
@ -121,15 +112,15 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
}
if (this.item.type === ItemType.CALCULATED_FIELD) {
const cfType = this.item.dataDescriptor?.cfType;
return cfTypeIcons.get(cfType) || 'functions';
return cfTypeIcons.get(cfType) || getItemTypeIcon(ItemType.CALCULATED_FIELD);
}
if (this.item.type === ItemType.ALARM_RULE) {
return 'notification_important';
return getItemTypeIcon(ItemType.ALARM_RULE);
}
switch (this.item.dataDescriptor?.ruleChainType) {
case 'CORE': return 'device_hub';
case 'EDGE': return 'router';
default: return 'account_tree';
default: return getItemTypeIcon(ItemType.RULE_CHAIN);
}
}

8
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html

@ -40,14 +40,18 @@
@if (getImageUrl(); as imgUrl) {
<img [src]="imgUrl" alt="">
} @else {
<mat-icon class="tb-iot-hub-item-link-thumb-fallback">{{ getTypeIcon() }}</mat-icon>
<tb-icon class="tb-iot-hub-item-link-thumb-fallback">{{ getTypeIcon() }}</tb-icon>
}
</div>
}
<div class="tb-iot-hub-item-link-text">
<span class="tb-iot-hub-item-link-name">{{ item.name }}</span>
<span class="tb-iot-hub-item-link-author">
<mat-icon>person</mat-icon>
@if (item.creatorVerified) {
<mat-icon class="tb-iot-hub-item-link-verified-icon">verified</mat-icon>
} @else {
<mat-icon>person</mat-icon>
}
{{ item.creatorDisplayName }}
</span>
</div>

8
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss

@ -37,6 +37,9 @@
background: rgba(4, 138, 211, 0.04);
border-color: rgba(4, 138, 211, 0.32);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
.tb-iot-hub-item-link-name, .tb-iot-hub-item-link-author {
text-decoration: underline;
}
}
&--unavailable {
@ -128,6 +131,11 @@
width: 14px;
height: 14px;
}
// Verified-creator accent — matches the item card chip color.
.tb-iot-hub-item-link-verified-icon {
color: #00695c;
}
}
.tb-iot-hub-item-link-skeleton-block,

24
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts

@ -17,7 +17,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models';
import { getItemTypeIcon, ItemType } from '@shared/models/iot-hub/iot-hub-item.models';
type CardState = 'loading' | 'loaded' | 'unavailable';
@ -71,29 +71,11 @@ export class TbIotHubItemLinkCardComponent implements OnInit {
getCompactIcon(): string {
const item = this.item!;
if (item.icon) {
return item.icon;
}
switch (item.type) {
case ItemType.CALCULATED_FIELD: return 'functions';
case ItemType.ALARM_RULE: return 'notification_important';
case ItemType.RULE_CHAIN: return 'account_tree';
default: return 'category';
}
return item.icon || getItemTypeIcon(item.type);
}
getTypeIcon(): string {
const item = this.item!;
switch (item.type) {
case ItemType.WIDGET: return 'widgets';
case ItemType.DASHBOARD: return 'dashboard';
case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions';
case ItemType.CALCULATED_FIELD: return 'functions';
case ItemType.ALARM_RULE: return 'notification_important';
case ItemType.RULE_CHAIN: return 'account_tree';
case ItemType.DEVICE: return 'memory';
default: return 'category';
}
return getItemTypeIcon(this.item!.type);
}
getCompactColor(): string {

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss

@ -128,7 +128,7 @@ $gallery-border-hover-color: #2a7dec;
max-width: 100%;
}
a:not(.mdc-button) {
a:not(.mdc-button):not(.tb-iot-hub-item-link-card) {
font-weight: 500;
color: $link-color;
text-decoration: none;

27
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts

@ -109,9 +109,36 @@ export class TbIotHubMarkdownComponent implements OnInit, OnChanges {
parsed = replaceItemLinkPlaceholders(parsed);
parsed = this.resolveImages(parsed);
parsed = this.resolveVariables(parsed);
parsed = this.forceLinksOpenInNewTab(parsed);
return parsed;
}
// Ensure every link rendered through tb-markdown opens in a new tab.
// 1) For markdown links `[text](url)` — append the
// `{:target="_blank"}` suffix to the link TEXT (not the URL)
// so MarkedOptionsService.renderer.link, which checks
// `token.text.endsWith(targetBlankBlock)`, emits
// `target="_blank"` on the rendered <a>. Skip links whose
// text already ends with the suffix and skip image syntax
// (`![alt](url)`).
// 2) For raw <a ...> HTML anchors authored in the markdown — add
// `target="_blank"` when no `target=` attribute is already
// present.
private forceLinksOpenInNewTab(content: string): string {
const TARGET_BLANK_BLOCK = '{:target="_blank"}';
content = content.replace(
/(?<!!)\[([^\]]*?)]\(([^)]+)\)/g,
(match, text: string, url: string) =>
text.endsWith(TARGET_BLANK_BLOCK) ? match : `[${text}${TARGET_BLANK_BLOCK}](${url})`
);
content = content.replace(
/<a\b([^>]*)>/gi,
(match, attrs: string) =>
/\btarget\s*=/i.test(attrs) ? match : `<a${attrs} target="_blank">`
);
return content;
}
private prefixResourceUrls(markdown: string): string {
const baseUrl = this.iotHubApiService.baseUrl;
return markdown.replace(/([("])(\/api\/resources\/[^)"]*)/g, `$1${baseUrl}$2`);

2
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html

@ -109,7 +109,7 @@
@if (getItemImage(item); as imgUrl) {
<img [src]="imgUrl" alt="">
} @else {
<mat-icon class="tb-search-popup-thumb-placeholder">{{ getItemTypeIcon(item.type) }}</mat-icon>
<tb-icon class="tb-search-popup-thumb-placeholder">{{ getItemTypeIcon(item.type) }}</tb-icon>
}
</div>
}

21
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts

@ -25,7 +25,7 @@ import { MediaBreakpoints } from '@shared/models/constants';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { getItemTypeIcon, 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 { TranslateService } from '@ngx-translate/core';
@ -266,15 +266,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
}
getCompactIcon(item: MpItemVersionView): string {
if (item.icon) {
return item.icon;
}
switch (item.type) {
case ItemType.CALCULATED_FIELD: return 'functions';
case ItemType.ALARM_RULE: return 'notification_important';
case ItemType.RULE_CHAIN: return 'settings_ethernet';
default: return 'category';
}
return item.icon || getItemTypeIcon(item.type);
}
getItemImage(item: MpItemVersionView): string | null {
@ -282,14 +274,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
}
getItemTypeIcon(type: ItemType): string {
switch (type) {
case ItemType.WIDGET: return 'widgets';
case ItemType.DASHBOARD: return 'dashboard';
case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions';
case ItemType.ALARM_RULE: return 'notification_important';
case ItemType.DEVICE: return 'memory';
default: return 'category';
}
return getItemTypeIcon(type);
}
getSearchGroupLabel(type: ItemType): string {

23
ui-ngx/src/app/shared/directives/chip-overflow.directive.ts

@ -15,6 +15,7 @@
///
import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnDestroy, Renderer2 } from '@angular/core';
import { coerceBoolean } from '@shared/decorators/coercion';
@Directive({
selector: '[tb-chip-overflow]',
@ -28,6 +29,10 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy {
@Input() overflowClass = 'tb-overflow-chip';
@Input() minChips = 1;
@Input()
@coerceBoolean()
showOverflowedTitle = false;
private resizeObserver?: ResizeObserver;
private mutationObserver?: MutationObserver;
private overflowEl!: HTMLElement;
@ -151,6 +156,7 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy {
// 4. Determine which chips fit
let usedWidth = 0;
let hiddenCount = 0;
const hiddenChips: HTMLElement[] = [];
const minChips = Math.max(1, this.minChips);
for (let i = 0; i < chips.length; i++) {
@ -163,12 +169,29 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy {
usedWidth = nextUsedWidth;
} else {
hiddenCount++;
hiddenChips.push(chips[i]);
}
}
if (hiddenCount > 0) {
this.renderer.setProperty(this.overflowEl, 'textContent', this.overflowTemplate.replace('{n}', String(hiddenCount)));
this.renderer.setStyle(this.overflowEl, 'display', 'inline-flex');
if (this.showOverflowedTitle) {
const title = hiddenChips
.map(chip => (chip.textContent || '').trim())
.filter(text => !!text)
.join(', ');
this.renderer.setAttribute(this.overflowEl, 'title', title);
// Allow pointer interaction so the browser's native title
// tooltip appears on hover.
this.renderer.removeStyle(this.overflowEl, 'pointerEvents');
} else {
this.renderer.removeAttribute(this.overflowEl, 'title');
this.renderer.setStyle(this.overflowEl, 'pointerEvents', 'none');
}
} else {
this.renderer.removeAttribute(this.overflowEl, 'title');
this.renderer.setStyle(this.overflowEl, 'pointerEvents', 'none');
}
}
}

17
ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts

@ -36,6 +36,23 @@ export const itemTypeTranslations = new Map<ItemType, string>(
]
);
// Canonical icon lookup per item type. Values are tb-icon
// identifiers (Material symbol names or `mdi:*` strings) and should
// be used everywhere an icon is rendered for an item type so the
// mapping stays consistent across the app.
export const itemTypeIcons: Record<string, string> = {
[ItemType.WIDGET]: 'widgets',
[ItemType.DASHBOARD]: 'dashboard',
[ItemType.SOLUTION_TEMPLATE]: 'apps',
[ItemType.CALCULATED_FIELD]: 'mdi:function-variant',
[ItemType.RULE_CHAIN]: 'settings_ethernet',
[ItemType.ALARM_RULE]: 'mdi:bell-cog',
[ItemType.DEVICE]: 'devices_other'
};
export const getItemTypeIcon = (type?: string | null): string =>
type && itemTypeIcons[type] ? itemTypeIcons[type] : 'category';
/**
* Item types discoverable to creators in the marketplace UI.
* DASHBOARD is intentionally absent (IoT Hub no longer accepts Dashboard contributions).

Loading…
Cancel
Save