Browse Source

feat(iot-hub): add GATEWAY and GATEWAY_CONNECTOR step types with attribute saving

- Add GATEWAY step: creates gateway device, outputs dockerComposeUrl
- Add GATEWAY_CONNECTOR step: saves connector config as shared attributes,
  appends to active_connectors array on the gateway
- Add optional serverAttributes/sharedAttributes fields to all entity steps
  (DEVICE, GATEWAY, DEVICE_PROFILE, DASHBOARD, RULE_CHAIN)
- Attribute files are read from ZIP, variable-resolved, and saved via
  AttributeService after entity creation
pull/15508/head
Andrii Shvaika 6 months ago
parent
commit
85fcb260b1
  1. 85
      ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts
  2. 9
      ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts
  3. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -30,6 +30,9 @@ import { DeviceProfileService } from '@core/http/device-profile.service';
import { DeviceService } from '@core/http/device.service';
import { DashboardService } from '@core/http/dashboard.service';
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 {
connectivityTypeTranslations,
DeviceInstallStep,
@ -108,6 +111,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
private deviceService: DeviceService,
private dashboardService: DashboardService,
private ruleChainService: RuleChainService,
private attributeService: AttributeService,
private iotHubApiService: IotHubApiService
) {
super(store, router, dialogRef);
@ -380,6 +384,8 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
try {
const startTime = Date.now();
const output = await this.createEntity(ep.step);
// Save optional attributes after entity creation
await this.saveStepAttributes(ep.step, output);
// Ensure minimum visible time per step
const elapsed = Date.now() - startTime;
if (elapsed < ENTITY_STEP_MIN_DELAY) {
@ -457,6 +463,55 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
const creds = await firstValueFrom(this.deviceService.getDeviceCredentials(result.id.id, false, {ignoreErrors: true}));
return { id: result.id.id, name: result.name, token: creds.credentialsId };
}
case InstallStepType.GATEWAY: {
const result = await firstValueFrom(this.deviceService.saveDevice(template, {ignoreErrors: true}));
const creds = await firstValueFrom(this.deviceService.getDeviceCredentials(result.id.id, false, {ignoreErrors: true}));
return {
id: result.id.id,
name: result.name,
token: creds.credentialsId,
dockerComposeUrl: `/api/device-connectivity/gateway-launch/${result.id.id}/docker-compose/download`
};
}
case InstallStepType.GATEWAY_CONNECTOR: {
const gatewayOutput = this.entityOutputs.get('gateway');
if (!gatewayOutput) {
throw new Error('GATEWAY step must precede GATEWAY_CONNECTOR');
}
const gatewayEntityId = { entityType: 'DEVICE', id: gatewayOutput.id } as EntityId;
const connectorName = template.name;
// Fetch current active_connectors
let activeConnectors: string[] = [];
try {
const attrs = await firstValueFrom(
this.attributeService.getEntityAttributes(gatewayEntityId, AttributeScope.SHARED_SCOPE, ['active_connectors'], {ignoreErrors: true})
);
const existing = attrs?.find(a => a.key === 'active_connectors');
if (existing?.value && Array.isArray(existing.value)) {
activeConnectors = existing.value;
}
} catch (_e) {
// First connector — no existing attributes
}
// Append connector name
if (!activeConnectors.includes(connectorName)) {
activeConnectors.push(connectorName);
}
// Save attributes
await firstValueFrom(this.attributeService.saveEntityAttributes(
gatewayEntityId, AttributeScope.SHARED_SCOPE,
[
{ key: 'active_connectors', value: activeConnectors },
{ key: connectorName, value: template }
],
{ignoreErrors: true}
));
return { id: gatewayOutput.id, name: connectorName };
}
case InstallStepType.DASHBOARD: {
const result = await firstValueFrom(this.dashboardService.saveDashboard(template, {ignoreErrors: true}));
return { id: result.id.id, name: result.title };
@ -522,10 +577,40 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
return undefined;
}
private async saveStepAttributes(step: DeviceInstallStep, output: EntityStepOutput): Promise<void> {
const entityType = this.stepTypeToEntityType(step.type);
if (!entityType) {
return;
}
const entityId = { entityType, id: output.id } as EntityId;
if (step.serverAttributes) {
const raw = this.zipFiles.get(step.serverAttributes);
if (raw) {
const resolved = JSON.parse(this.resolveVariables(raw));
const attrs = Object.entries(resolved).map(([key, value]) => ({ key, value }));
await firstValueFrom(this.attributeService.saveEntityAttributes(
entityId, AttributeScope.SERVER_SCOPE, attrs, {ignoreErrors: true}
));
}
}
if (step.sharedAttributes) {
const raw = this.zipFiles.get(step.sharedAttributes);
if (raw) {
const resolved = JSON.parse(this.resolveVariables(raw));
const attrs = Object.entries(resolved).map(([key, value]) => ({ key, value }));
await firstValueFrom(this.attributeService.saveEntityAttributes(
entityId, AttributeScope.SHARED_SCOPE, attrs, {ignoreErrors: true}
));
}
}
}
private stepTypeToEntityType(stepType: InstallStepType): string | null {
switch (stepType) {
case InstallStepType.DEVICE_PROFILE: return 'DEVICE_PROFILE';
case InstallStepType.DEVICE: return 'DEVICE';
case InstallStepType.GATEWAY: return 'DEVICE';
case InstallStepType.GATEWAY_CONNECTOR: return 'DEVICE';
case InstallStepType.DASHBOARD: return 'DASHBOARD';
case InstallStepType.RULE_CHAIN: return 'RULE_CHAIN';
default: return null;

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

@ -53,6 +53,8 @@ export enum InstallStepType {
CONVERTER = 'CONVERTER',
INTEGRATION = 'INTEGRATION',
DEVICE = 'DEVICE',
GATEWAY = 'GATEWAY',
GATEWAY_CONNECTOR = 'GATEWAY_CONNECTOR',
DASHBOARD = 'DASHBOARD',
RULE_CHAIN = 'RULE_CHAIN'
}
@ -60,6 +62,8 @@ export enum InstallStepType {
export const ENTITY_STEP_TYPES: Set<string> = new Set([
InstallStepType.DEVICE_PROFILE,
InstallStepType.DEVICE,
InstallStepType.GATEWAY,
InstallStepType.GATEWAY_CONNECTOR,
InstallStepType.DASHBOARD,
InstallStepType.RULE_CHAIN
]);
@ -67,6 +71,8 @@ export const ENTITY_STEP_TYPES: Set<string> = new Set([
export const stepTypeAliasMap: Record<string, string> = {
[InstallStepType.DEVICE_PROFILE]: 'deviceProfile',
[InstallStepType.DEVICE]: 'device',
[InstallStepType.GATEWAY]: 'gateway',
[InstallStepType.GATEWAY_CONNECTOR]: 'gatewayConnector',
[InstallStepType.DASHBOARD]: 'dashboard',
[InstallStepType.RULE_CHAIN]: 'ruleChain'
};
@ -76,6 +82,8 @@ export interface DeviceInstallStep {
name: string;
file?: string;
template?: string;
serverAttributes?: string;
sharedAttributes?: string;
}
export interface DevicePackageInfo {
@ -121,6 +129,7 @@ export interface EntityStepOutput {
id: string;
name: string;
token?: string;
dockerComposeUrl?: string;
}
export type EntityStepStatus = 'pending' | 'running' | 'success' | 'error';

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

@ -3892,6 +3892,8 @@
"device-install-step-type-DASHBOARD": "Dashboard",
"device-install-step-type-DEVICE": "Device",
"device-install-step-type-DEVICE_PROFILE": "Device Profile",
"device-install-step-type-GATEWAY": "Gateway",
"device-install-step-type-GATEWAY_CONNECTOR": "Gateway Connector",
"device-install-step-type-RULE_CHAIN": "Rule Chain",
"device-install-title": "Install \"{{name}}\"",
"device-install-complete-title": "Install \"{{name}}\" — Complete",

Loading…
Cancel
Save