@@ -85,7 +85,7 @@
-
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
index aad686707d..8951764a4b 100644
--- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
+++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
@@ -30,6 +30,8 @@ import {
} from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import {
+ ComplexOperation,
+ complexOperationTranslationMap,
EntityKeyType,
entityKeyTypeTranslationMap,
KeyFilterInfo,
@@ -71,6 +73,10 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
@Input() entityId: EntityId;
+ @Input() operation: ComplexOperation = ComplexOperation.AND;
+
+ complexOperationTranslationMap = complexOperationTranslationMap;
+
keyFilterListFormGroup: UntypedFormGroup;
entityKeyTypeTranslations = entityKeyTypeTranslationMap;
diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts
index 7f04f07664..6273fbd434 100644
--- a/ui-ngx/src/app/shared/models/query/query.models.ts
+++ b/ui-ngx/src/app/shared/models/query/query.models.ts
@@ -401,20 +401,22 @@ export interface FilterInfo {
filter: string;
editable: boolean;
keyFilters: Array
;
+ keyFiltersOperation?: ComplexOperation;
}
export interface FiltersInfo {
datasourceFilters: {[datasourceIndex: number]: FilterInfo};
}
-export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array): string {
+export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array,
+ operation: ComplexOperation = ComplexOperation.AND): string {
const filtersText = keyFilters.map(keyFilter =>
keyFilterToText(translate, datePipe, keyFilter,
- keyFilters.length > 1 ? ComplexOperation.AND : undefined));
+ keyFilters.length > 1 ? operation : undefined));
let result: string;
if (filtersText.length > 1) {
- const andText = translate.instant('filter.operation.and');
- result = filtersText.join(' ' + andText + ' ');
+ const opText = translate.instant(complexOperationTranslationMap.get(operation));
+ result = filtersText.join(' ' + opText + ' ');
} else {
result = filtersText[0];
}
@@ -802,6 +804,7 @@ export const singleEntityFilterFromDeviceId = (deviceId: string): EntityFilter =
export interface EntityCountQuery {
entityFilter: EntityFilter;
keyFilters?: Array;
+ keyFiltersOperation?: ComplexOperation;
}
export interface AbstractDataQuery extends EntityCountQuery {
@@ -940,7 +943,8 @@ export const getFilterId = (filters: Filters, filterInfo: FilterInfo): string =>
const newFilterName = createFilterName(filters, filterInfo.filter);
newFilterId = guid();
filters[newFilterId] = {id: newFilterId, filter: newFilterName,
- keyFilters: filterInfo.keyFilters, editable: filterInfo.editable};
+ keyFilters: filterInfo.keyFilters, editable: filterInfo.editable,
+ keyFiltersOperation: filterInfo.keyFiltersOperation};
}
return newFilterId;
}
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index 4fdc707c1e..1f4da168c7 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -3610,6 +3610,7 @@
"order-priority": "Display order",
"key-filter": "Key filter",
"key-filters": "Key filters",
+ "or-conditions-disabled": "OR conditions between key filters are disabled by the system administrator.",
"key-name": "Key name",
"key-name-required": "Key name is required.",
"key-type": {
diff --git a/ui-ngx/structure.md b/ui-ngx/structure.md
new file mode 100644
index 0000000000..23d85b5f56
--- /dev/null
+++ b/ui-ngx/structure.md
@@ -0,0 +1,746 @@
+# ThingsBoard CE Frontend — Navigation & Testing Guide
+
+## 1. Quick Reference
+
+| Key | Value |
+|-----|-------|
+| Angular | 20 |
+| Angular Material | 20 |
+| Component prefix | `tb` |
+| TypeScript | 5.9 |
+| RxJS | 7.8 |
+
+**Path Aliases** (from `tsconfig.json`):
+
+| Alias | Maps to |
+|-------|---------|
+| `@app/*` | `src/app/*` |
+| `@env/*` | `src/environments/*` |
+| `@core/*` | `src/app/core/*` |
+| `@modules/*` | `src/app/modules/*` |
+| `@home/*` | `src/app/modules/home/*` |
+| `@shared/*` | `src/app/shared/*` |
+
+**Key Directories:**
+
+```
+src/app/
+├── core/
+│ ├── http/ # HTTP services (one per entity type)
+│ └── services/ # Menu, auth, utils
+├── modules/
+│ ├── home/
+│ │ ├── components/ # Reusable home components (entity table, details panel, alarm, dashboard)
+│ │ │ └── entity/ # EntitiesTableComponent, EntityDetailsPageComponent, EntityComponent base
+│ │ └── pages/ # Page modules (one directory per page/feature)
+│ └── login/ # Login, reset password, 2FA
+└── shared/
+ ├── components/ # Shared components (autocompletes, dialogs, time, image)
+ └── models/ # TypeScript interfaces/enums
+```
+
+---
+
+## 2. Route-to-Source Mapping
+
+All page source paths are relative to `src/app/modules/home/pages/`.
+Page types: `entity-table` = EntitiesTableComponent + resolver, `entity-details` = EntityDetailsPageComponent, `settings-form` = standalone form with ConfirmOnExitGuard, `dashboard-view` = DashboardViewComponent with resolver, `custom` = unique component.
+
+### Login (unauthenticated)
+
+Source paths below are relative to `src/app/modules/` (not `home/pages/`).
+
+| URL | Source | Type |
+|-----|--------|------|
+| `/login` | `login/pages/login/` | custom (LoginComponent) |
+| `/login/resetPasswordRequest` | `login/pages/login/` | custom (ResetPasswordRequestComponent) |
+| `/login/resetPassword` | `login/pages/login/` | custom (ResetPasswordComponent) |
+| `/login/createPassword` | `login/pages/login/` | custom (CreatePasswordComponent) |
+| `/login/mfa` | `login/pages/login/` | custom (TwoFactorAuthLoginComponent) |
+
+### Home
+
+| URL | Source | Type | Auth |
+|-----|--------|------|------|
+| `/home` | `home-links/` | custom (HomeLinksComponent + dashboard resolver) | ALL |
+
+### Tenants (SYS_ADMIN)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/tenants` | `tenant/` | entity-table | `TenantsTableConfigResolver` |
+| `/tenants/:entityId` | `tenant/` | entity-details | `TenantsTableConfigResolver` |
+| `/tenants/:tenantId/users` | `tenant/` + `user/` | entity-table | `UsersTableConfigResolver` |
+| `/tenantProfiles` | `tenant-profile/` | entity-table | `TenantProfilesTableConfigResolver` |
+| `/tenantProfiles/:entityId` | `tenant-profile/` | entity-details | `TenantProfilesTableConfigResolver` |
+
+### Alarms Center (TENANT_ADMIN, CUSTOMER_USER)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/alarms` | `alarm/` | custom (RouterTabsComponent) | — |
+| `/alarms/alarms` | `alarm/` | custom (AlarmTableComponent) | — |
+| `/alarms/alarm-rules` | `alarm/` | custom (AlarmRulesTableComponent) | — |
+| `/alarms/alarm-rules/:entityId` | `alarm/` | entity-details | `AlarmRulesTableConfigResolver` |
+
+### Dashboards (TENANT_ADMIN, CUSTOMER_USER)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/dashboards` | `dashboard/` | entity-table | `DashboardsTableConfigResolver` |
+| `/dashboards/:dashboardId` | `dashboard/` | custom (DashboardPageComponent) | `DashboardResolver` |
+
+### Entities (TENANT_ADMIN, CUSTOMER_USER)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/entities/devices` | `device/` | entity-table | `DevicesTableConfigResolver` |
+| `/entities/devices/:entityId` | `device/` | entity-details | `DevicesTableConfigResolver` |
+| `/entities/assets` | `asset/` | entity-table | `AssetsTableConfigResolver` |
+| `/entities/assets/:entityId` | `asset/` | entity-details | `AssetsTableConfigResolver` |
+| `/entities/entityViews` | `entity-view/` | entity-table | `EntityViewsTableConfigResolver` |
+| `/entities/entityViews/:entityId` | `entity-view/` | entity-details | `EntityViewsTableConfigResolver` |
+| `/entities/gateways` | `gateways/` | dashboard-view | `gatewaysDashboardResolver` |
+
+### Profiles (TENANT_ADMIN)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/profiles/deviceProfiles` | `device-profile/` | entity-table | `DeviceProfilesTableConfigResolver` |
+| `/profiles/deviceProfiles/:entityId` | `device-profile/` | entity-details | `DeviceProfilesTableConfigResolver` |
+| `/profiles/assetProfiles` | `asset-profile/` | entity-table | `AssetProfilesTableConfigResolver` |
+| `/profiles/assetProfiles/:entityId` | `asset-profile/` | entity-details | `AssetProfilesTableConfigResolver` |
+
+### Customers (TENANT_ADMIN)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/customers` | `customer/` | entity-table | `CustomersTableConfigResolver` |
+| `/customers/:entityId` | `customer/` | entity-details | `CustomersTableConfigResolver` |
+| `/customers/:customerId/users` | `customer/` + `user/` | entity-table | `UsersTableConfigResolver` |
+| `/customers/:customerId/devices` | `customer/` + `device/` | entity-table | `DevicesTableConfigResolver` |
+| `/customers/:customerId/assets` | `customer/` + `asset/` | entity-table | `AssetsTableConfigResolver` |
+| `/customers/:customerId/dashboards` | `customer/` + `dashboard/` | entity-table | `DashboardsTableConfigResolver` |
+| `/customers/:customerId/edgeInstances` | `customer/` + `edge/` | entity-table | `EdgesTableConfigResolver` |
+
+### Calculated Fields (TENANT_ADMIN)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/calculatedFields` | `calculated-fields/` | custom (CalculatedFieldsTableComponent) | — |
+| `/calculatedFields/:entityId` | `calculated-fields/` | entity-details | `CalculatedFieldsTableConfigResolver` |
+
+### Rule Chains (TENANT_ADMIN)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/ruleChains` | `rulechain/` | entity-table | `RuleChainsTableConfigResolver` |
+| `/ruleChains/:ruleChainId` | `rulechain/` | custom (RuleChainPageComponent, lazy) | `RuleChainResolver` + others |
+
+### Edge Management (TENANT_ADMIN, conditional on edgesSupportEnabled)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/edgeManagement/instances` | `edge/` | entity-table | `EdgesTableConfigResolver` |
+| `/edgeManagement/instances/:entityId` | `edge/` | entity-details | `EdgesTableConfigResolver` |
+| `/edgeManagement/instances/:edgeId/assets` | `edge/` + `asset/` | entity-table | `AssetsTableConfigResolver` |
+| `/edgeManagement/instances/:edgeId/devices` | `edge/` + `device/` | entity-table | `DevicesTableConfigResolver` |
+| `/edgeManagement/instances/:edgeId/entityViews` | `edge/` + `entity-view/` | entity-table | `EntityViewsTableConfigResolver` |
+| `/edgeManagement/instances/:edgeId/dashboards` | `edge/` + `dashboard/` | entity-table | `DashboardsTableConfigResolver` |
+| `/edgeManagement/instances/:edgeId/ruleChains` | `edge/` + `rulechain/` | entity-table | `RuleChainsTableConfigResolver` |
+| `/edgeManagement/ruleChains` | `edge/` + `rulechain/` | entity-table | `RuleChainsTableConfigResolver` |
+
+### Advanced Features (TENANT_ADMIN)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/features/otaUpdates` | `ota-update/` | entity-table | `OtaUpdateTableConfigResolve` |
+| `/features/otaUpdates/:entityId` | `ota-update/` | entity-details | `OtaUpdateTableConfigResolve` |
+| `/features/vc` | `vc/` | custom (VersionControlComponent) | — |
+
+### Resources (SYS_ADMIN, TENANT_ADMIN)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/resources/widgets-library/widget-types` | `widget/` | entity-table | `WidgetTypesTableConfigResolver` |
+| `/resources/widgets-library/widgets-bundles` | `widget/` | entity-table | `WidgetsBundlesTableConfigResolver` |
+| `/resources/widgets-library/widgets-bundles/:widgetsBundleId` | `widget/` | custom (WidgetsBundleWidgetsComponent) | — |
+| `/resources/widgets-library/widget-types/:widgetTypeId` | `widget/` | custom (WidgetEditorComponent) | — |
+| `/resources/images` | `admin/` | custom (ImageGalleryComponent) | — |
+| `/resources/scada-symbols` | `admin/` | custom (ImageGalleryComponent) | — |
+| `/resources/scada-symbols/:type/:key` | `scada-symbol/` | custom (ScadaSymbolComponent) | — |
+| `/resources/resources-library` | `admin/` | entity-table | `ResourcesLibraryTableConfigResolver` |
+| `/resources/resources-library/:entityId` | `admin/` | entity-details | `ResourcesLibraryTableConfigResolver` |
+| `/resources/javascript-library` | `admin/` | entity-table | `JsLibraryTableConfigResolver` |
+| `/resources/javascript-library/:entityId` | `admin/` | entity-details | `JsLibraryTableConfigResolver` |
+
+### Notifications (ALL authorities)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/notification/inbox` | `notification/inbox/` | entity-table | `InboxTableConfigResolver` |
+| `/notification/sent` | `notification/sent/` | entity-table | `SentTableConfigResolver` |
+| `/notification/recipients` | `notification/recipient/` | entity-table | `RecipientTableConfigResolver` |
+| `/notification/templates` | `notification/template/` | entity-table | `TemplateTableConfigResolver` |
+| `/notification/rules` | `notification/rule/` | entity-table | `RuleTableConfigResolver` |
+
+### Mobile Center (SYS_ADMIN, TENANT_ADMIN)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/mobile-center/bundles` | `mobile/bundes/` | entity-table | `MobileBundleTableConfigResolver` |
+| `/mobile-center/applications` | `mobile/applications/` | entity-table | `MobileAppTableConfigResolver` |
+| `/mobile-center/applications/:entityId` | `mobile/applications/` | entity-details | `MobileAppTableConfigResolver` |
+| `/mobile-center/qr-code-widget` | `mobile/qr-code-widget/` | settings-form | — |
+
+### Settings — SYS_ADMIN
+
+| URL | Source | Type |
+|-----|--------|------|
+| `/settings/general` | `admin/` | settings-form (GeneralSettingsComponent) |
+| `/settings/outgoing-mail` | `admin/` | settings-form (MailServerComponent) |
+| `/settings/notifications` | `admin/` | settings-form (SmsProviderComponent) |
+| `/settings/queues` | `admin/` | entity-table (`QueuesTableConfigResolver`) |
+
+### Settings — TENANT_ADMIN
+
+| URL | Source | Type |
+|-----|--------|------|
+| `/settings/home` | `admin/` | settings-form (HomeSettingsComponent) |
+| `/settings/notifications` | `admin/` | settings-form (SmsProviderComponent) |
+| `/settings/repository` | `admin/` | settings-form (RepositoryAdminSettingsComponent) |
+| `/settings/auto-commit` | `admin/` | settings-form (AutoCommitAdminSettingsComponent) |
+| `/settings/trendz` | `admin/` | settings-form (TrendzSettingsComponent) |
+| `/settings/ai-models` | `ai-model/` | entity-table (`AiModelsTableConfigResolver`) |
+
+### Security Settings (SYS_ADMIN)
+
+| URL | Source | Type | Resolver |
+|-----|--------|------|----------|
+| `/security-settings/general` | `admin/` | settings-form (SecuritySettingsComponent) | — |
+| `/security-settings/2fa` | `admin/` | settings-form (TwoFactorAuthSettingsComponent) | — |
+| `/security-settings/oauth2/domains` | `admin/` | entity-table | `DomainTableConfigResolver` |
+| `/security-settings/oauth2/clients` | `admin/` | entity-table | `ClientsTableConfigResolver` |
+| `/security-settings/auditLogs` | `admin/` | custom (audit log table) | — |
+
+### Account (ALL authorities)
+
+| URL | Source | Type |
+|-----|--------|------|
+| `/account/profile` | `profile/` | settings-form (ProfileComponent) |
+| `/account/security` | `security/` | settings-form (SecurityComponent) |
+
+### API Usage (TENANT_ADMIN)
+
+| URL | Source | Type |
+|-----|--------|------|
+| `/usage` | `api-usage/` | dashboard-view (loads `api_usage.json`) |
+
+---
+
+## 3. Entity Table Pattern
+
+Most pages follow a 3-file pattern per entity type inside `pages//`:
+
+```
+-table-config.resolver.ts — Column definitions, actions, component wiring
+.component.ts + .html — Entity detail form (extends EntityComponent)
+-tabs.component.ts + .html — Additional detail tabs (relations, attributes, events, etc.)
+```
+
+**Framework files** (in `home/components/entity/`):
+- `entities-table.component.ts` — The generic table component. Renders columns, actions, pagination from config.
+- `entity-details-page.component.ts` — Full-page entity details with tabs.
+- `entity-details-panel.component.ts` — Slide-out drawer panel for entity details.
+- `entity.component.ts` — Abstract base directive all entity forms extend.
+- `entities-table-config.models.ts` — TypeScript interfaces for table configuration.
+
+**Resolver structure** — each resolver returns an `EntityTableConfig` with:
+- `entityType` — enum value (e.g., `EntityType.DEVICE`)
+- `columns` — array of column definitions (header key, cell property/template, width)
+- `cellActionDescriptors` — per-row action buttons
+- `groupActionDescriptors` — bulk-selection action buttons
+- `headerActionDescriptors` — toolbar action buttons (add, import)
+- `entityComponent` — reference to the detail form component class
+- `entityTabsComponent` — reference to the tabs component class
+- `addDialogComponent` — optional dialog for add flow (e.g., `DeviceWizardDialogComponent`)
+
+### All Table Config Resolvers
+
+| Resolver | Entity Type | Source Dir | Entity Component | Tabs Component |
+|----------|-------------|-----------|------------------|----------------|
+| `TenantsTableConfigResolver` | TENANT | `tenant/` | `TenantComponent` | `TenantTabsComponent` |
+| `TenantProfilesTableConfigResolver` | TENANT_PROFILE | `tenant-profile/` | `TenantProfileComponent` | `TenantProfileTabsComponent` |
+| `DevicesTableConfigResolver` | DEVICE | `device/` | `DeviceComponent` | `DeviceTabsComponent` |
+| `DeviceProfilesTableConfigResolver` | DEVICE_PROFILE | `device-profile/` | `DeviceProfileComponent` | `DeviceProfileTabsComponent` |
+| `AssetsTableConfigResolver` | ASSET | `asset/` | `AssetComponent` | `AssetTabsComponent` |
+| `AssetProfilesTableConfigResolver` | ASSET_PROFILE | `asset-profile/` | `AssetProfileComponent` | `AssetProfileTabsComponent` |
+| `EntityViewsTableConfigResolver` | ENTITY_VIEW | `entity-view/` | `EntityViewComponent` | `EntityViewTabsComponent` |
+| `CustomersTableConfigResolver` | CUSTOMER | `customer/` | `CustomerComponent` | `CustomerTabsComponent` |
+| `UsersTableConfigResolver` | USER | `user/` | `UserComponent` | `UserTabsComponent` |
+| `DashboardsTableConfigResolver` | DASHBOARD | `dashboard/` | `DashboardFormComponent` | `DashboardTabsComponent` |
+| `EdgesTableConfigResolver` | EDGE | `edge/` | `EdgeComponent` | `EdgeTabsComponent` |
+| `RuleChainsTableConfigResolver` | RULE_CHAIN | `rulechain/` | `RuleChainComponent` | `RuleChainTabsComponent` |
+| `OtaUpdateTableConfigResolve` | OTA_PACKAGE | `ota-update/` | `OtaUpdateComponent` | `OtaUpdateTabsComponent` |
+| `CalculatedFieldsTableConfigResolver` | CALCULATED_FIELD | `calculated-fields/` (config in `home/components/calculated-fields/`) | `CalculatedFieldComponent` | `CalculatedFieldsTabsComponent` |
+| `WidgetTypesTableConfigResolver` | WIDGETS_BUNDLE | `widget/` | `WidgetTypeComponent` | `WidgetTypeTabsComponent` |
+| `WidgetsBundlesTableConfigResolver` | WIDGETS_BUNDLE | `widget/` | `WidgetsBundleComponent` | `WidgetsBundleTabsComponent` |
+| `ResourcesLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | `ResourcesLibraryComponent` | `ResourceLibraryTabsComponent` |
+| `JsLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | `JsResourceComponent` | `ResourceTabsComponent` |
+| `QueuesTableConfigResolver` | QUEUE | `admin/queue/` | `QueueComponent` | — |
+| `AiModelsTableConfigResolver` | AI_MODEL | `ai-model/` | — | — |
+| `ClientsTableConfigResolver` | OAUTH2_CLIENT | `admin/oauth2/clients/` | `ClientComponent` | — |
+| `InboxTableConfigResolver` | NOTIFICATION | `notification/inbox/` | — (dialog) | — |
+| `SentTableConfigResolver` | NOTIFICATION_REQUEST | `notification/sent/` | — (dialog) | — |
+| `RecipientTableConfigResolver` | NOTIFICATION_TARGET | `notification/recipient/` | — (dialog) | — |
+| `TemplateTableConfigResolver` | NOTIFICATION_TEMPLATE | `notification/template/` | — (dialog) | — |
+| `RuleTableConfigResolver` | NOTIFICATION_RULE | `notification/rule/` | — (dialog) | — |
+| `MobileAppTableConfigResolver` | MOBILE_APP | `mobile/applications/` | `MobileAppComponent` | — |
+| `MobileBundleTableConfigResolver` | MOBILE_BUNDLE | `mobile/bundes/` | — | — |
+| `AlarmRulesTableConfigResolver` | CALCULATED_FIELD | `alarm/` (config in `home/components/alarm-rules/`) | — | — |
+| `DomainTableConfigResolver` | DOMAIN | `admin/oauth2/domains/` | `DomainComponent` | — |
+
+---
+
+## 4. Shared Component Catalog
+
+### `@shared/components/entity/`
+
+| Selector | Purpose |
+|----------|---------|
+| `tb-entity-autocomplete` | Autocomplete input for selecting a single entity |
+| `tb-entity-select` | Dropdown select for entity |
+| `tb-entity-list` | Chip list for multiple entities |
+| `tb-entity-list-select` | Dropdown with entity list selection |
+| `tb-entity-key-autocomplete` | Autocomplete for entity attribute/telemetry keys |
+| `tb-entity-keys-list` | Chip list for multiple entity keys |
+| `tb-entity-type-select` | Dropdown for entity type enum |
+| `tb-entity-type-list` | Chip list for multiple entity types |
+| `tb-entity-subtype-select` | Dropdown for entity subtype (e.g., device type) |
+| `tb-entity-subtype-list` | Chip list for multiple subtypes |
+| `tb-entity-subtype-autocomplete` | Autocomplete for entity subtype |
+| `tb-entity-gateway-select` | Dropdown for selecting a gateway device |
+
+### `@shared/components/time/`
+
+| Selector | Purpose |
+|----------|---------|
+| `tb-timewindow` | Timewindow selector (realtime/history) |
+| `tb-timewindow-panel` | Expanded panel for timewindow config |
+| `tb-timewindow-config-dialog` | Dialog for advanced timewindow settings |
+| `tb-datetime` | Date-time picker input |
+| `tb-datetime-period` | Date range picker |
+| `tb-timezone` | Timezone display |
+| `tb-timezone-select` | Dropdown for timezone selection |
+| `tb-quick-time-interval` | Quick interval preset selector |
+| `tb-timeinterval` | Duration input (number + unit) |
+| `tb-datapoints-limit` | Numeric input for data point limits |
+| `tb-aggregation-type-select` | Dropdown for aggregation type (AVG, SUM, etc.) |
+| `tb-history-selector` | History navigation selector |
+
+### `@shared/components/image/`
+
+| Selector | Purpose |
+|----------|---------|
+| `tb-image-gallery` | Grid gallery of uploaded images |
+| `tb-image-gallery-dialog` | Dialog wrapper for image gallery |
+| `tb-image-input` | Single image upload input |
+| `tb-gallery-image-input` | Image input that opens gallery for selection |
+| `tb-multiple-gallery-image-input` | Multi-image input with gallery |
+| `tb-upload-image-dialog` | Upload dialog |
+| `tb-embed-image-dialog` | Embed external image dialog |
+| `tb-scada-symbol-input` | SCADA symbol selector input |
+
+### `@shared/components/dialog/`
+
+| Selector | Purpose |
+|----------|---------|
+| `tb-confirm-dialog` | Yes/No confirmation dialog |
+| `tb-alert-dialog` | Information alert dialog |
+| `tb-error-alert-dialog` | Error message dialog |
+| `tb-color-picker-dialog` | Color picker in a dialog |
+| `tb-material-icons-dialog` | Material icon browser dialog |
+| `tb-node-script-test-dialog` | Script testing dialog for rule nodes |
+| `tb-object-edit-dialog` | JSON object editor dialog |
+| `tb-todo-dialog` | Todo/checklist dialog |
+
+### `@shared/components/` (other)
+
+| Selector | Purpose |
+|----------|---------|
+| `tb-json-content` | JSON syntax-highlighted viewer |
+| `tb-json-object-edit` | Editable JSON object component |
+| `tb-json-object-view` | Read-only JSON object display |
+| `tb-markdown` | Markdown editor/viewer |
+| `tb-breadcrumb` | Breadcrumb navigation bar |
+| `tb-icon` | Custom icon component |
+| `tb-logo` | ThingsBoard logo |
+| `tb-copy-button` | Copy-to-clipboard button |
+| `tb-toggle-password` | Password visibility toggle |
+| `tb-file-input` | File upload input |
+| `tb-phone-input` | International phone input |
+| `tb-unit-input` | Measurement unit input |
+| `tb-value-input` | Generic value input with type selection |
+| `tb-color-input` | Color picker input |
+| `tb-color-picker` | Inline color picker |
+| `tb-key-val-map` | Key-value pair map editor |
+| `tb-nav-tree` | Tree navigation component |
+| `tb-user-menu` | User profile dropdown menu |
+| `tb-notification-bell` | Notification bell icon with badge |
+| `tb-footer` | Page footer |
+| `tb-scroll-grid` | Virtual-scroll grid |
+| `tb-social-share-panel` | Social sharing buttons |
+
+### Autocomplete / Select Components
+
+| Selector | Purpose |
+|----------|---------|
+| `tb-dashboard-autocomplete` | Autocomplete for dashboard selection |
+| `tb-dashboard-select` | Dropdown for dashboard |
+| `tb-dashboard-state-autocomplete` | Autocomplete for dashboard states |
+| `tb-country-autocomplete` | Country autocomplete |
+| `tb-string-autocomplete` | Generic string autocomplete |
+| `tb-message-type-autocomplete` | Rule engine message type autocomplete |
+| `tb-queue-autocomplete` | Queue name autocomplete |
+| `tb-relation-type-autocomplete` | Relation type autocomplete |
+| `tb-resource-autocomplete` | Resource autocomplete |
+| `tb-ota-package-autocomplete` | OTA package autocomplete |
+| `tb-branch-autocomplete` | Git branch autocomplete |
+| `tb-rule-chain-select` | Rule chain select dropdown |
+| `tb-widgets-bundle-select` | Widgets bundle select dropdown |
+| `tb-template-autocomplete` | Notification template autocomplete |
+| `tb-material-icon-select` | Material icon picker |
+| `tb-slack-conversation-autocomplete` | Slack conversation autocomplete |
+
+### `@home/components/` (key components)
+
+| Selector | Purpose |
+|----------|---------|
+| `tb-entities-table` | Main entity table (columns, sort, pagination, actions) |
+| `tb-entity-details-panel` | Slide-out panel for entity details |
+| `tb-entity-details-page` | Full-page entity details view |
+| `tb-add-entity-dialog` | Generic add-entity dialog |
+| `tb-entity-filter` | Entity data filter component |
+| `tb-entity-chips` | Chip display for entity references |
+| `tb-details-panel` | Base details panel (toolbar + edit/apply FABs) |
+| `tb-alarm-table` | Alarm list table |
+| `tb-alarm-details-dialog` | Alarm detail dialog with ack/clear |
+| `tb-alarm-assignee` | Alarm assignee selector |
+| `tb-alarm-comment` | Alarm comment thread |
+| `tb-alarm-filter-config` | Alarm filter configuration |
+| `tb-audit-log-table` | Audit log table |
+| `tb-attribute-table` | Entity attribute key-value table |
+| `tb-relation-table` | Entity relation table |
+| `tb-event-table` | Entity event table |
+| `tb-dashboard` | Dashboard renderer |
+| `tb-dashboard-page` | Dashboard page (toolbar + dashboard) |
+| `tb-dashboard-toolbar` | Dashboard edit/view toolbar |
+| `tb-edit-widget` | Widget editor |
+| `tb-router-tabs` | Tab navigation via router (used for section tabs) |
+| `tb-device-credentials` | Device credentials form |
+
+---
+
+## 5. Angular Material DOM Patterns
+
+**No `data-testid` attributes** — selectors must use Angular Material structure, `formControlName`, `matColumnDef`, labels, roles, and classes.
+
+### Login Form
+
+```html
+
+
+
+
+
+```
+
+Key selectors: `#username-input`, `#password-input`, `button[type="submit"]`.
+
+### Main Layout
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Key selectors: `mat-sidenav#sidenav`, `button#main`, `tb-breadcrumb`, `tb-user-menu`.
+
+### Entity Table
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Key selectors: `table[mat-table]`, `mat-row`, `mat-cell`, `mat-paginator`, `[matColumnDef="select"]`, `[matColumnDef="actions"]`. Note: the search input uses Angular template ref `#searchInput` (not an HTML `id`); target it via `.tb-entity-table mat-toolbar input[matInput]` in the search toolbar context.
+
+### Entity Details Page
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Key selectors: `mat-toolbar.details-toolbar`, `button[mat-fab][color="accent"]`, `mat-tab-group`, `mat-tab`.
+
+### Details Panel (Slide-Out Drawer)
+
+```html
+
+
+
+
+```
+
+### Entity Detail Form (inside a tab)
+
+```html
+
+
+
+```
+
+Key selectors: `input[formControlName="name"]`, `mat-slide-toggle[formControlName="..."]`, `textarea[formControlName="description"]`.
+
+### Dialog
+
+```html
+
+```
+
+Key selectors: `[mat-dialog-content]`, `[mat-dialog-actions]`, `button[type="submit"]`, `mat-toolbar[color="primary"] h2`.
+
+### Common Material Selector Patterns
+
+```
+# Find a form field by its control name
+input[formControlName="name"]
+mat-select[formControlName="type"]
+mat-slide-toggle[formControlName="gateway"]
+textarea[formControlName="description"]
+
+# Find table column content
+[matColumnDef="name"] mat-cell
+
+# Find buttons by tooltip
+button[matTooltip="Search"]
+
+# Find a specific tab
+mat-tab-group mat-tab (by index or label text)
+
+# Find menu items (after opening menu)
+button[mat-menu-item]
+
+# Find dialog actions
+[mat-dialog-actions] button[mat-raised-button]
+```
+
+---
+
+## 6. Form Structure Guide
+
+All entity detail forms extend the abstract `EntityComponent` directive (`@home/components/entity/entity.component.ts`).
+
+**Key properties:**
+- `entityForm: UntypedFormGroup` — the reactive form instance
+- `entity: T` — current entity data (input)
+- `isEdit: boolean` — whether form is in edit mode (input)
+
+**Lifecycle:**
+
+1. **`buildForm(entity: T): UntypedFormGroup`** — Called in constructor. Creates the FormGroup with controls, validators, and initial values. Subscribe to valueChanges here for cascading updates.
+
+2. **`updateForm(entity: T): void`** — Called when entity input changes. Uses `entityForm.patchValue({...}, {emitEvent: false})` to populate form without triggering change events.
+
+3. **`updateFormState(): void`** — Called when `isEdit` changes. Calls `entityForm.enable()` or `entityForm.disable()` with `{emitEvent: false}`.
+
+4. **`entityFormValue(): any`** — Returns `entityForm.getRawValue()` (reads disabled fields too), passed through `prepareFormValue()` (default: `deepTrim`).
+
+**Read-only pattern:** Templates use `