diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000..1d760fe7bf --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,88 @@ +# +# Copyright © 2016-2023 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +changelog: + exclude: + labels: + - Ignore for release + categories: + - title: 'Major Core & Rule Engine' + labels: + - 'Major Core' + - 'Major Rule Engine' + exclude: + labels: + - 'Bug' + - title: 'Major UI' + labels: + - 'Major UI' + exclude: + labels: + - 'Bug' + - title: 'Major Transport' + labels: + - 'Major Transport' + exclude: + labels: + - 'Bug' + - title: 'Major Edge' + labels: + - 'Major Edge' + exclude: + labels: + - 'Bug' + - title: 'Core & Rule Engine' + labels: + - 'Core' + - 'Rule Engine' + exclude: + labels: + - 'Bug' + - title: 'UI' + labels: + - 'UI' + exclude: + labels: + - 'Bug' + - title: 'Transport' + labels: + - 'Transport' + exclude: + labels: + - 'Bug' + - title: 'Edge' + labels: + - 'Edge' + exclude: + labels: + - 'Bug' + - title: 'Bug: Core & Rule Engine' + labels: + - 'Core' + - 'Rule Engine' + - 'Bug' + - title: 'Bug: UI' + labels: + - 'UI' + - 'Bug' + - title: 'Bug: Transport' + labels: + - 'Transport' + - 'Bug' + - title: 'Bug: Edge' + labels: + - 'Edge' + - 'Bug' diff --git a/application/pom.xml b/application/pom.xml index 61fa75cf49..8e7c3cd524 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -1,6 +1,6 @@ + + + + LWM2M Security + + 0 + urn:oma:lwm2m:oma:0:1.2 + 1.1 + 1.2 + Multiple + Mandatory + + + LWM2M Server URI + + Single + Mandatory + String + 0..255 + + + + + Bootstrap-Server + + Single + Mandatory + Boolean + + + + + + Security Mode + + Single + Mandatory + Integer + 0..4 + + + + + Public Key or Identity + + Single + Mandatory + Opaque + + + + + + Server Public Key + + Single + Mandatory + Opaque + + + + + + Secret Key + + Single + Mandatory + Opaque + + + + + + SMS Security Mode + + Single + Optional + Integer + 0..255 + + + + + SMS Binding Key Parameters + + Single + Optional + Opaque + 6 + + + + + SMS Binding Secret Key(s) + + Single + Optional + Opaque + 16,32,48 + + + + + LwM2M Server SMS Number + + Single + Optional + String + + + + + + Short Server ID + + Single + Optional + Integer + 1..65534 + + + + + Client Hold Off Time + + Single + Optional + Integer + + s + + + + Bootstrap-Server Account Timeout + + Single + Optional + Integer + + s + + + + Matching Type + + Single + Optional + Unsigned Integer + 0..3 + + + + + SNI + + Single + Optional + String + + + + + + Certificate Usage + + Single + Optional + Unsigned Integer + 0..3 + + + + + DTLS/TLS Ciphersuite + + Multiple + Optional + Unsigned Integer + + + + + OSCORE Security Mode + + Single + Optional + Objlnk + + + + + + Groups To Use by Client + + Multiple + Optional + Unsigned Integer + 0..65535 + + + + + Signature Algorithms Supported by Server + + Multiple + Optional + Unsigned Integer + 0..65535 + + + + Signature Algorithms To Use by Client + + Multiple + Optional + Unsigned Integer + 0..65535 + + + + + Signature Algorithm Certs Supported by Server + + Multiple + Optional + Unsigned Integer + 0..65535 + + + + + TLS 1.3 Features To Use by Client + + Single + Optional + Unsigned Integer + 0..65535 + + + + + TLS Extensions Supported by Server + + Single + Optional + Unsigned Integer + 0..65535 + + + + + TLS Extensions To Use by Client + + Single + Optional + Unsigned Integer + 0..65535 + + + + + Secondary LwM2M Server URI + + Multiple + Optional + String + 0..255 + + + + MQTT Server + + Single + Optional + Objlnk + + + + + LwM2M COSE Security + + Multiple + Optional + Objlnk + + + + + RDS Destination Port + + Single + Optional + Integer + 0..15 + + + + RDS Source Port + + Single + Optional + Integer + 0..15 + + + + RDS Application ID + + Single + Optional + String + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/1.xml b/application/src/main/data/lwm2m-registry/1.xml new file mode 100644 index 0000000000..ace9efcc6a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/1.xml @@ -0,0 +1,360 @@ + + + + + + + LwM2M Server + + 1 + urn:oma:lwm2m:oma:1:1.2 + 1.2 + 1.2 + Multiple + Mandatory + + + Short Server ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Lifetime + RW + Single + Mandatory + Integer + + s + + + + Default Minimum Period + RW + Single + Optional + Integer + + s + + + + Default Maximum Period + RW + Single + Optional + Integer + + s + + + + Disable + E + Single + Optional + + + + + + + Disable Timeout + RW + Single + Optional + Integer + + s + + + + Notification Storing When Disabled or Offline + RW + Single + Mandatory + Boolean + + + + + + Binding + RW + Single + Mandatory + String + + + + + + Registration Update Trigger + E + Single + Mandatory + + + + + + + Bootstrap-Request Trigger + E + Single + Optional + + + + + + + APN Link + RW + Single + Optional + Objlnk + + + + + + TLS-DTLS Alert Code + R + Single + Optional + Unsigned Integer + 0..255 + + + + + Last Bootstrapped + R + Single + Optional + Time + + + + + + Registration Priority Order + R + Single + Optional + Unsigned Integer + + + + + + Initial Registration Delay Timer + RW + Single + Optional + Unsigned Integer + + s + + + + Registration Failure Block + R + Single + Optional + Boolean + + + + + + Bootstrap on Registration Failure + R + Single + Optional + Boolean + + + + + + Communication Retry Count + RW + Single + Optional + Unsigned Integer + + + + + + Communication Retry Timer + RW + Single + Optional + Unsigned Integer + + s + + + + Communication Sequence Delay Timer + RW + Single + Optional + Unsigned Integer + + s + + + + Communication Sequence Retry Count + RW + Single + Optional + Unsigned Integer + + + + + + Trigger + RW + Single + Optional + Boolean + + + + + + Preferred Transport + RW + Single + Optional + String + The possible values are those listed in the LwM2M Core Specification + + + + Mute Send + RW + Single + Optional + Boolean + + + + + + Alternate APN Links + RW + Multiple + Optional + Objlnk + + + + + + Supported Server Versions + RW + Multiple + Optional + String + + + + + + Default Notification Mode + RW + Single + Optional + Integer + 0..1 + + + + + Profile ID Hash Algorithm + RW + Single + Optional + Unsigned Integer + 0..255 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10.xml b/application/src/main/data/lwm2m-registry/10.xml new file mode 100644 index 0000000000..af87413a94 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10.xml @@ -0,0 +1,217 @@ + + + + + + LWM2M Cellular Connectivity + + 10 + urn:oma:lwm2m:oma:10:1.1 + 1.1 + 1.1 + Single + Optional + + SMSC address + RW + Single + Optional + String + + + + + Disable radio period + RW + Single + Optional + Integer + 0..65535 + + 0 the device SHALL disconnect. When the period has elapsed the device MAY reconnect.]]> + + Module activation code + RW + Single + Optional + String + + + + + Vendor specific extensions + R + Single + Optional + Objlnk + + + + + PSM Timer + RW + Single + Optional + Integer + + s + + + Active Timer + RW + Single + Optional + Integer + + s + + + eDRX parameters for Iu mode + RW + Single + Optional + Opaque + 0..255 + + + + eDRX parameters for WB-S1 mode + RW + Single + Optional + Opaque + 0..255 + + + + eDRX parameters for NB-S1 mode + RW + Single + Optional + Opaque + 0..255 + + + + eDRX parameters for A/Gb mode + RW + Single + Optional + Opaque + 0..255 + + + + Activated Profile Names + R + Multiple + Mandatory + Objlnk + + + + + + Supported Power Saving Modes + R + Single + Optional + Unsigned Integer + 0..255 + + + + + Active Power Saving Modes + RW + Single + Optional + Unsigned Integer + 0..255 + + + + + Release Assistance Indication Usage + RW + Single + Optional + Unsigned Integer + 0..255 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10241.xml b/application/src/main/data/lwm2m-registry/10241.xml new file mode 100644 index 0000000000..5a499100f3 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10241.xml @@ -0,0 +1,81 @@ + + + + + HostDeviceInfo + + 10241 + urn:oma:lwm2m:x:10241 + 1.0 + 1.0 + Multiple + Optional + + Host Device Manufacturer + R + Multiple + Mandatory + String + + + + + Host Device Model Number + R + Multiple + Mandatory + String + + + + + Host Device Unique ID + R + Multiple + Mandatory + String + + + + + Host Device Software Version + R + Multiple + Mandatory + String + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10242.xml b/application/src/main/data/lwm2m-registry/10242.xml new file mode 100644 index 0000000000..09a62806d3 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10242.xml @@ -0,0 +1,659 @@ + + + + + + + 3-Phase Power Meter + + + + 10242 + urn:oma:lwm2m:x:10242 + 1.0 + 1.0 + Multiple + Optional + + + Manufacturer + R + Single + Optional + String + + + + + + + + Model Number + R + Single + Optional + String + + + + + + + + Serial Number + R + Single + Optional + String + + + + + + + + Description + R + Single + Optional + String + + + + + + + + Tension R + R + Single + Mandatory + Float + + V + + + + + + Current R + R + Single + Mandatory + Float + + A + + + + + + Active Power R + R + Single + Optional + Float + + kW + + + + + + Reactive Power R + R + Single + Optional + Float + + kvar + + + + + + Inductive Reactive Power R + R + Single + Optional + Float + + kvar + + + + + + Capacitive Reactive Power R + R + Single + Optional + Float + + kvar + + + + + + Apparent Power R + R + Single + Optional + Float + + kVA + + + + + + Power Factor R + R + Single + Optional + Float + -1..1 + + + + + + + THD-V R + R + Single + Optional + Float + + /100 + + + + + + THD-A R + R + Single + Optional + Float + + /100 + + + + + + Tension S + R + Single + Mandatory + Float + + V + + + + + + Current S + R + Single + Mandatory + Float + + A + + + + + + Active Power S + R + Single + Optional + Float + + kW + + + + + + Reactive Power S + R + Single + Optional + Float + + kvar + + + + + + Inductive Reactive Power S + R + Single + Optional + Float + + kvar + + + + + + Capacitive Reactive Power S + R + Single + Optional + Float + + kvar + + + + + + Apparent Power S + R + Single + Optional + Float + + kVA + + + + + + Power Factor S + R + Single + Optional + Float + -1..1 + + + + + + + THD-V S + R + Single + Optional + Float + + /100 + + + + + + THD-A S + R + Single + Optional + Float + + /100 + + + + + + Tension T + R + Single + Mandatory + Float + + V + + + + + + Current T + R + Single + Mandatory + Float + + A + + + + + + Active Power T + R + Single + Optional + Float + + kW + + + + + + Reactive Power T + R + Single + Optional + Float + + kvar + + + + + + Inductive Reactive Power T + R + Single + Optional + Float + + kvar + + + + + + Capacitive Reactive Power T + R + Single + Optional + Float + + kvar + + + + + + Apparent Power T + R + Single + Optional + Float + + kVA + + + + + + Power Factor T + R + Single + Optional + Float + -1..1 + + + + + + + THD-V T + R + Single + Optional + Float + + /100 + + + + + + THD-A T + R + Single + Optional + Float + + /100 + + + + + + 3-Phase Active Power + R + Single + Optional + Float + + kW + + + + + + 3-Phase Reactive Power + R + Single + Optional + Float + + kvar + + + + + + 3-Phase Inductive Reactive Power + R + Single + Optional + Float + + kvar + + + + + + 3-Phase Capacitive Reactive Power + R + Single + Optional + Float + + kvar + + + + + + 3-Phase Apparent Power + R + Single + Optional + Float + + kVA + + + + + + 3-Phase Power Factor + R + Single + Optional + Float + -1..1 + + + + + + + 3-Phase phi cosine + R + Single + Optional + Float + -1..1 + + + + + + + Active Energy + R + Single + Optional + Float + + kWh + + + + + + Reactive Energy + R + Single + Optional + Float + + kvarh + + + + + + Inductive Reactive Energy + R + Single + Optional + Float + + kvarh + + + + + + Capacitive Reactive Energy + R + Single + Optional + Float + + kvarh + + + + + + Apparent Energy + R + Single + Optional + Float + + kVAh + + + + + + Tension R-S + R + Single + Optional + Float + + V + + + + + + Tension S-T + R + Single + Optional + Float + + V + + + + + + Tension T-R + R + Single + Optional + Float + + V + + + + + + Frequency + R + Single + Optional + Float + + Hz + + + + + + Neutral Current + R + Single + Optional + Float + + A + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10243.xml b/application/src/main/data/lwm2m-registry/10243.xml new file mode 100644 index 0000000000..b73ce20021 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10243.xml @@ -0,0 +1,263 @@ + + + + + + + Single-Phase Power Meter + + + + 10243 + urn:oma:lwm2m:x:10243:2.0 + 1.0 + 2.0 + Multiple + Optional + + + Manufacturer + R + Single + Optional + String + + + + + + + + Model Number + R + Single + Optional + String + + + + + + + + Serial Number + R + Single + Optional + String + + + + + + + + Description + R + Single + Optional + String + + + + + + + + Tension + R + Single + Mandatory + Float + + V + + + + + + Current + R + Single + Mandatory + Float + + A + + + + + + Active Power + R + Single + Optional + Float + + kW + + + + + + Reactive Power + R + Single + Optional + Float + + kvar + + + + + + Inductive Reactive Power + R + Single + Optional + Float + + kvar + + + + + + Capacitive Reactive Power + R + Single + Optional + Float + + kvar + + + + + + Apparent Power + R + Single + Optional + Float + + kVA + + + + + + Power Factor + R + Single + Optional + Float + -1..1 + + + + + + + THD-V + R + Single + Optional + Float + + /100 + + + + + + THD-A + R + Single + Optional + Float + + /100 + + + + + + Active Energy + R + Single + Optional + Float + + kWh + + + + + + Reactive Energy + R + Single + Optional + Float + + kvarh + + + + + + Apparent Energy + R + Single + Optional + Float + + kVAh + + + + + + Frequency + R + Single + Optional + Float + + Hz + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10244.xml b/application/src/main/data/lwm2m-registry/10244.xml new file mode 100644 index 0000000000..9f55cd47de --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10244.xml @@ -0,0 +1,303 @@ + + + VehicleControlUnit + + 10244 + urn:oma:lwm2m:x:10244 + 1.0 + 1.0 + Single + Optional + + Vehicle UI State + R + Single + Mandatory + Integer + 0..15 + + + + Vehicle Speed + R + Single + Mandatory + Integer + + km/h + + + Vehicle Shift Status + R + Single + Mandatory + Integer + 0..3 + + + + Vehicle AP Position + R + Single + Mandatory + Integer + 0..100 + /100 + + + Vehicle Power + R + Single + Optional + Float + + kW + + + Vehicle Drive Energy + R + Single + Optional + Float + + Wh + + + Vehicle Energy Consumption Efficiency + R + Single + Optional + Float + + Wh/km + + + Vehicle Estimated Mileage + R + Single + Optional + Integer + + km + + + Vehicle Charge Cable Status + R + Single + Mandatory + Boolean + + + + + Vehicle Charge Status + R + Single + Mandatory + Integer + 0..15 + + + + Vehicle Charge Voltage + R + Single + Mandatory + Float + + V + + + Vehicle Charge Current + R + Single + Mandatory + Float + + A + + + Vehicle Charge Remaining Time + R + Single + Mandatory + Integer + + min + + + Battery Pack Voltage + R + Single + Mandatory + Float + + V + + + Battery Pack Current + R + Single + Mandatory + Float + + A + + + Battery Pack Remaining Capacity + R + Single + Mandatory + Integer + + Ah + + + Battery Pack SOC + R + Single + Mandatory + Integer + 0..100 + /100 + + + Battery Pack SOH + R + Single + Mandatory + Integer + 0..100 + /100 + + + Battery Cell MinVolt + R + Single + Mandatory + Integer + + mV + + + Battery Cell MaxVolt + R + Single + Mandatory + Integer + + mV + + + Battery Module MinTemp + R + Single + Mandatory + Integer + + Cel + + + Battery Module MaxTemp + R + Single + Mandatory + Integer + + Cel + + + Battery Connection Status + R + Single + Mandatory + Boolean + + + + + + MCU Voltage + R + Single + Mandatory + Integer + + V + + + MCU Temperature + R + Single + Mandatory + Integer + + Cel + + + Motor Speed + R + Single + Mandatory + Integer + + 1/min + + + Motor Temperature + R + Single + Mandatory + Integer + + Cel + + + Motor OT Warning + R + Single + Optional + Boolean + + + + + MCU OT Warning + R + Single + Optional + Boolean + + + + + Battery Pack OT Warning + R + Single + Optional + Boolean + + + + + MCU fault + R + Single + Optional + Boolean + + + + + Motor Error + R + Single + Optional + Boolean + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10245.xml b/application/src/main/data/lwm2m-registry/10245.xml new file mode 100644 index 0000000000..b29349130d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10245.xml @@ -0,0 +1,217 @@ + + + + + + Relay Management + This LWM2M Object provides a range of eNB related measurements and parameters of which several are changeable. Furthermore, it includes Resources to enable/disable the eNB. + 10245 + urn:oma:lwm2m:x:10245 + 1.0 + 1.0 + Single + Optional + + + eNB Availability + R + Single + Mandatory + Boolean + AVAILABLE; UNAVAILABLE + + This field indicates to the CCC whether or not the eNB of the CrowdBox is available for activation: AVAILABLE = TRUE; UNAVAILABLE = FALSE This is set by the CrowdBox itself using an algorithm specific to the use case and based on parameters known to the CrowdBox which may not necessarily be signalled to the network. In the absence of a more specific algorithm, this parameter should be set to AVAILABLE, unless a fault is detected which would prevent activation of the eNB, in which case it should be set to UNAVAILABLE. + + + GPS Status + R + Single + Mandatory + Boolean + UNSYNCHRONISED; SYNCHRONISED + + States whether the CrowdBox GPS receiver is synchronised to GPS time or not: UNSYCHRONISED = FALSE; SYNCHRONISED = TRUE If more than one GPS receiver is used by the CrowdBox, then SYNCHRONISED should be reported only if all receivers are synchronised. + + + Orientation + R + Single + Optional + Integer + -180..180 + deg + Orientation of CrowdBox with respect to magnetic north. The reference orientation of the CrowdBox shall be the pointing direction of the eNB antenna(s) or, in the case of an omni-directional CrowdBox antenna, as defined in the accompanying product documentation. + + + eNB EARFCN + RW + Single + Mandatory + Integer + 0..65535 + + EARFCN currently used by the eNB. Highest valid value in 3GPP is currently 46589. If the requested EARFCN is not supported by the eNB, the response should be "Bad Request". The CrowdBox shall only apply a change of this resource upon execution of the "Enable eNB" command. + + + eNB Bandwidth + RW + Single + Mandatory + Integer + 5, 10, 15, 20 + + Bandwidth of the currently used eNB carrier. If the requested bandwidth is not supported by the eNB, the response should be "Bad Request". The CrowdBox shall only apply a change of this resource upon execution of the "Enable eNB" command. + + + Backhaul Primary EARFCN + RW + Single + Mandatory + Integer + 0..65535 + + EARFCN of primary cell used for the backhaul. If the requested EARFCN is not supported by the CrowdBox UE, the response should be "Bad Request". The CrowdBox shall only apply a change of this resource upon execution of the "Enable eNB" command. + + + Backhaul Secondary EARFCN + RW + Multiple + Mandatory + Integer + 0..65535 + + EARFCN of any secondary cells used for the backhaul, in the event that carrier aggregation is being used. If the requested EARFCN is not supported by the CrowdBox UE, the response should be "Bad Request". The CrowdBox shall only apply a change of this resource upon execution of the "Enable eNB" command. + + + Cumulative Measurement Window + RW + Single + Mandatory + Integer + 0..65535 + s + The current measurement interval over which cumulative statistics are collected for the following resources: Cumulative Number of Unique Users, Cumulative Downlink Throughput per Connected User, Cumulative Uplink Throughput per Connected User. Note that this measurement period is a sliding window rather than a granularity period. Measurements should never be reset, but rather old measurements should be removed from the cumulative total as they fall outside of the window. A value of 0 shall be interpreted as meaning only the current value should be reported. A value of 65535 shall be interpreted as an infinite window size (i.e. old measurements are never discarded). + + + eNB ECI + R + Single + Mandatory + Integer + 0..2^28-1 + + A 28 bit E-UTRAN Cell Identifier (ECI) + + + eNB Status + RW + Single + Mandatory + Boolean + + + This resource indicates the current status of the eNB and can be used by the CCC to change the state from enabled to disabled. TRUE = eNB enabled FALSE = eNB disabled + + + Enable eNB + E + Single + Mandatory + + + + Enables the eNB. In addition the CrowdBox shall also update its configuration to reflect the current state of other relevant parameters. This might require a reboot. + + + eNB Maximum Power + RW + Single + Mandatory + Integer + 0..63 + dBm + Maximum power for the eNB measured as the sum of input powers to all antenna connectors. The maximum power per antenna port is equal to the maximum eNB power divided by the number of antenna ports. If the requested power is above or below the maximum or minimum power levels of the eNB, then the power level should be set to the maximum or minimum respectively. The CrowdBox shall only apply a change of this resource upon execution of the "Enable eNB" command. + + + Backhaul Primary q-OffsetFreq + RW + Single + Mandatory + Integer + -24..24 + dB + q-OffsetFreq parameter for the backhaul primary EARFCN in SIB5 of the CrowdBox eNB BCCH. See TS 36.331 for details. Range: dB-24; dB-22 .. dB24 The CrowdBox shall only apply a change of this resource upon execution of the "Enable eNB" command. + + + Backhaul Secondary q-OffsetFreq + RW + Multiple + Mandatory + Integer + -24..24 + dB + q-OffsetFreq parameter for the backhaul secondary EARFCN in SIB5 of the CrowdBox eNB BCCH. See TS 36.331 for details Range: dB-24; dB-22 .. dB24 The CrowdBox shall only apply a change of this resource upon execution of the "Enable eNB" command. + + + Neighbour CrowdBox EARFCN + RW + Multiple + Mandatory + Integer + 0..66635 + + EARFCN of a neighbour CrowdBox. Each instance of this resource relates to the same instance of resource ID 15. + + + Neighbour CrowdBox q-OffsetFreq + RW + Multiple + Mandatory + Integer + -24..24 + dB + q-OffsetFreq parameter of the Neighbour CrowdBox EARFCN in SIB5 of the Neighbour CrowdBox eNB BCCH. See TS 36.331 for details Range: dB-24; dB-22 .. dB24 Each instance of this resource relates to the same instance of resource ID 14. The CrowdBox shall only apply a change of this resource upon execution of the "Enable eNB" command. + + + Serving Macro eNB cellIndividualOffset + RW + Single + Mandatory + Integer + -24..24 + dB + Specifies the value of the cellIndividualOffset parameter applicable to the CrowdBox macro serving cell that is to be signalled to connected UEs in their measurement configuration information . See TS 36.331 for details. The CrowdBox shall only apply a change of this resource upon execution of the "Enable eNB" command. + + + + + diff --git a/application/src/main/data/lwm2m-registry/10246.xml b/application/src/main/data/lwm2m-registry/10246.xml new file mode 100644 index 0000000000..c3977d9538 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10246.xml @@ -0,0 +1,108 @@ + + + + + + + CrowdBox Measurements + This LWM2M Object provides CrowdBox-related measurements such as serving cell parameters, backhaul timing advance, and neighbour cell reports. + 10246 + urn:oma:lwm2m:x:10246 + 1.0 + 1.0 + Single + Optional + + + Serving Cell ID + R + Single + Mandatory + Integer + 0..2^32-1 + + Serving cell ID as specified by the cellIdentity field broadcast in SIB1 of the serving cell (see TS 36.331). + + + Serving Cell RSRP + R + Single + Mandatory + Integer + 0..97 + + Serving cell RSRP, as defined in TS 36.133, Section 9.1.4. Range: RSRP_00; RSRP_01 .. RSRP_97 + + + Serving Cell RSRQ + R + Single + Mandatory + Integer + -30..46 + + Serving cell RSRQ, as defined in TS 36.133, Section 9.1.7. Range: RSRQ_-30; RSRQ_-29 .. RSRQ_46 + + + Serving Cell SINR + R + Single + Mandatory + Integer + -10..30 + dB + SINR of serving cell as estimated by the CrowdBox. Note that this is a proprietary measurement dependent on the UE chipset manufacturer. The UE chipset used should be stated in the accompanying product documentation. + + + Cumulative Backhaul Timing Advance + R + Single + Optional + Integer + 0..65535 + + The cumulative timing advance signalled by the current serving cell to the CrowdBox. This is the sum of the initial timing advance signalled in the MAC payload of the Random Access Response (11 bits, 0 .. 1282) and subsequent adjustments signalled in the MAC PDU of DL-SCH transmissions (6 bits, -31 .. 32). See TS 36.321 for details. + + + Neighbour Cell Report + R + Multiple + Mandatory + Objlnk + + + A link to the "Neighbour Cell Report" object for each neighbour cell of the CrowdBox. + + + + + diff --git a/application/src/main/data/lwm2m-registry/10247.xml b/application/src/main/data/lwm2m-registry/10247.xml new file mode 100644 index 0000000000..eba05417e8 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10247.xml @@ -0,0 +1,98 @@ + + + + + + + Neighbour Cell Report + This LWM2M Object provides the neighbour cell report. The CrowdBox Measurements Object and the Connected UE Report Object have both Objlnk Resources pointing to this Object. + 10247 + urn:oma:lwm2m:x:10247 + 1.0 + 1.0 + Multiple + Optional + + + Neighbour PCI + R + Single + Mandatory + Integer + 0..503 + + Physical Cell ID of neighbouring LTE cell, as defined in TS 36.211 + + + Neighbour Cell ID + R + Single + Optional + Integer + 0..2^32-1 + + Neighbour cell ID as specified by the cellIdentity field broadcast in SIB1 of the neighbour cell (see TS 36.331). + + + Neighbour Cell Rank + R + Single + Mandatory + Integer + 0..255 + + Current neighbour cell rank. Neighbour cells should be ordered (ranked) by the CrowdBox according to neighbour cell RSRP, with a higher RSRP corresponding to a lower index. Hence the neighbouring cell with the highest RSRP should be neighbour cell 0, the second neighbour cell 1, and so on. + + + Neighbour Cell RSRP + R + Single + Mandatory + Integer + 0..97 + + Neighbour cell RSRP, as defined in TS 36.133, Section 9.1.4. Range: RSRP_00; RSRP_01 .. RSRP_97 + + + Neighbour Cell RSRQ + R + Single + Mandatory + Integer + -30..46 + + Neighbour cell RSRQ, as defined in TS 36.133, Section 9.1.7. Range: RSRQ_-30; RSRQ_-29 .. RSRQ_46 + + + + + diff --git a/application/src/main/data/lwm2m-registry/10248.xml b/application/src/main/data/lwm2m-registry/10248.xml new file mode 100644 index 0000000000..e9f69f7889 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10248.xml @@ -0,0 +1,78 @@ + + + + + + + Connected UE Measurements + This LWM2M Object provides a range of measurements of connected UEs and provides an Object link to the Connected UE report. + 10248 + urn:oma:lwm2m:x:10248 + 1.0 + 1.0 + Single + Optional + + + Number of Connected Users + R + Single + Mandatory + Integer + 0..255 + + The number of different UEs currently connected to the eNB (i.e. in RRC_CONNECTED state). + + + Cumulative Number of Unique Users + R + Single + Mandatory + Integer + 0..65535 + + The number of different UEs that have connected to the eNB over the immediately preceding period specified by the "Cumulative Measurement Window" field. + + + Connected UE Report + R + Multiple + Mandatory + Objlnk + + + Provides an Object link to the Connected UE Report which provides a range of information related to the connected UEs. + + + + + diff --git a/application/src/main/data/lwm2m-registry/10249.xml b/application/src/main/data/lwm2m-registry/10249.xml new file mode 100644 index 0000000000..e1257c3dda --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10249.xml @@ -0,0 +1,138 @@ + + + + + + + Connected UE Report + This LWM2M Object provides a range of information related to the connected UEs. + 10249 + urn:oma:lwm2m:x:10249 + 1.0 + 1.0 + Multiple + Optional + + + Connected User MMEC + R + Single + Mandatory + Integer + 0..255 + + MMEC signalled by the UE to the eNB in the RRCConnectionRequest message (see TS 36.331). + + + Connected User M-TMSI + R + Single + Mandatory + Integer + 0..2^32-1 + + M-TMSI signalled by the UE to the eNB in the RRCConnectionRequest message (see TS 36.331). + + + Serving Cell (CrowdBox) eNB RSRP + R + Single + Mandatory + Integer + 0..97 + + The RSRP of the CrowdBox eNB, as defined in TS 36.133, Section 9.1.4. Range: RSRP_00; RSRP_01 .. RSRP_97 + + + Serving Cell (CrowdBox) eNB RSRQ + R + Single + Mandatory + Integer + -30..46 + + The RSRQ of the CrowdBox eNB, as defined in TS 36.133, Section 9.1.7. Range: RSRQ_-30; RSRQ_-29 .. RSRQ_46 + + + Cumulative Timing Advance per Connected User + R + Single + Optional + Integer + 0..65535 + + The cumulative timing advance signalled by the eNB to each currently connected UE. This is the sum of the initial timing advance signalled in the MAC payload of the Random Access Response (11 bits, 0 .. 1282) and subsequent adjustments signalled in the MAC PDU of DL-SCH transmissions (6 bits, -31 .. 32). See TS 36.321 for details. + + + Last downlink CQI report per Connected User + R + Single + Mandatory + Integer + 0..255 + + The last downlink wideband CQI reported by a connected user the eNB. The CQI format is defined in Table 7.2.3-1 of TS 36.213. + + + Cumulative Downlink Throughput per Connected User + R + Single + Mandatory + Integer + 0..2^32-1 + B + The total number of MAC bytes sent to the connected user over the immediately preceding period specified by the "Cumulative Measurement Window" field. + + + Cumulative Uplink Throughput per Connected User + R + Single + Mandatory + Integer + 0..2^32-1 + B + The total number of MAC bytes received from the connected user over the immediately preceding period specified by the "Cumulative Measurement Window" field. + + + Neighbour Cell Report + R + Multiple + Mandatory + Objlnk + + + A link to the "Neighbour Cell Report" object for each neighbour cell reported to the CrowdBox by the connected UE + + + + + diff --git a/application/src/main/data/lwm2m-registry/10250.xml b/application/src/main/data/lwm2m-registry/10250.xml new file mode 100644 index 0000000000..015149fae2 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10250.xml @@ -0,0 +1,70 @@ + + + + + App Data Container + + 10250 + urn:oma:lwm2m:x:10250 + 1.0 + 1.0 + Single + Optional + + + UL data + R + Single + Mandatory + Opaque + + + + + + + + DL data + W + Single + Mandatory + Opaque + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10251.xml b/application/src/main/data/lwm2m-registry/10251.xml new file mode 100644 index 0000000000..3fb1c00154 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10251.xml @@ -0,0 +1,96 @@ + + + + + + AT Command + + 10251 + urn:oma:lwm2m:x:10251 + 1.0 + 1.0 + Multiple + Optional + + + Command + RW + Single + Mandatory + String + + + + + + Response + R + Multiple + Mandatory + String + + + + + + Status + R + Multiple + Mandatory + String + + + + + + Timeout + RW + Single + Optional + Integer + + + + + + Run + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10252.xml b/application/src/main/data/lwm2m-registry/10252.xml new file mode 100644 index 0000000000..ddaf7cb1f2 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10252.xml @@ -0,0 +1,229 @@ + + + + + Manifest + + 10252 + urn:oma:lwm2m:x:10252 + 1.0 + 1.0 + Single + Optional + + + Manifest + W + Single + Mandatory + Opaque + + + + + + + + State + R + Single + Mandatory + Integer + 0..8 + + + + + + Manifest Result + R + Single + Mandatory + Integer + 0..19 + + + + + + Payload Result + R + Single + Mandatory + Opaque + + + + + + + + Asset Hash + R + Single + Mandatory + Opaque + + + + + + + + Manifest version + R + Single + Mandatory + Integer + + + + + + + + Asset Installation Progress + R + Single + Mandatory + Integer + + + + + + + + Campaign Id + RW + Single + Mandatory + String + + + + + + + + Manual Trigger + E + Single + Mandatory + + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10253.xml b/application/src/main/data/lwm2m-registry/10253.xml new file mode 100644 index 0000000000..27da37d16a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10253.xml @@ -0,0 +1,72 @@ + + + + + + + Confidential Data + + 10253 + urn:oma:lwm2m:x:10253 + 1.0 + 1.0 + Single + Optional + + + Public Key + RW + Single + Mandatory + Opaque + + + + + + + + Application Data + R + Single + Mandatory + Opaque + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10254.xml b/application/src/main/data/lwm2m-registry/10254.xml new file mode 100644 index 0000000000..ea37cf8cd8 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10254.xml @@ -0,0 +1,119 @@ + + + + + Current Loop Input + + 10254 + urn:oma:lwm2m:x:10254:1.0 + 1.0 + 1.0 + Multiple + Optional + + + Current Loop Input Current Value + R + Single + Mandatory + Float + 0; 3.8..20.5 + mA + + + + Min Measured Value + R + Single + Optional + Float + + + + + + Max Measured Value + R + Single + Optional + Float + + + + + + Min Range Value + R + Single + Optional + Float + + + + + + Max Range Value + R + Single + Optional + Float + + + + + + Reset Min and Max Measured Values + E + Single + Optional + + + + + + + Sensor Units + R + Single + Optional + String + + + + + + Application Type + RW + Single + Optional + String + + + + + + Current Calibration + RW + Single + Optional + Float + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10255.xml b/application/src/main/data/lwm2m-registry/10255.xml new file mode 100644 index 0000000000..fd63016952 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10255.xml @@ -0,0 +1,165 @@ + + + + + Device Metadata + + 10255 + urn:oma:lwm2m:x:10255 + 1.0 + 1.0 + Single + Optional + + + Protocol supported + R + Single + Mandatory + Integer + + + + + + + + Bootloader hash + R + Single + Mandatory + Opaque + + + + + + + + OEM bootloader hash + R + Single + Mandatory + Opaque + + + + + + + + Vendor + R + Single + Mandatory + String + + + + + + + + Class + R + Single + Mandatory + String + + + + + + + + Device + R + Single + Mandatory + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10256.xml b/application/src/main/data/lwm2m-registry/10256.xml new file mode 100644 index 0000000000..dbcad740d0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10256.xml @@ -0,0 +1,117 @@ + + + + ECID-Signal Measurement Information + + 10256 + urn:oma:lwm2m:x:10256 + 1.0 + 1.0 + Multiple + Optional + + + physCellId + R + Single + Mandatory + Integer + + + + + + + + ECGI + R + Single + Optional + Integer + + + + + + + + arfcnEUTRA + R + Single + Mandatory + Integer + + + + + + + + rsrp-Result + R + Single + Mandatory + Integer + + + + + + + + rsrq-Result + R + Single + Optional + Integer + + + + + + + + ue-RxTxTimeDiff + R + Single + Optional + Integer + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10257.xml b/application/src/main/data/lwm2m-registry/10257.xml new file mode 100644 index 0000000000..75864d542b --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10257.xml @@ -0,0 +1,275 @@ + + + + Heat / Cooling meter + + 10257 + urn:oma:lwm2m:x:10257 + 1.0 + 1.0 + Multiple + Optional + + + Manufacturer + R + Single + Optional + String + + + + + + Model Number + R + Single + Optional + String + + + + + + Serial Number + R + Single + Optional + String + + + + + + Description + R + Single + Optional + String + + + + + + Error code + R + Multiple + Optional + Integer + + + + + + + Instantaneous active power + R + Single + Optional + Float + + W + + + + Max Measured active power + R + Multiple + Mandatory + Float + + W + + + + Cumulative active power + R + Single + Optional + Float + + Wh + + + + Flow temperature + R + Single + Optional + Float + + Cel + + + + Max Measured flow temperature + R + Single + Optional + Float + + Cel + + + + Return temperature + R + Single + Optional + Float + + Cel + + + + Max Measured return temperature + R + Single + Optional + Float + + Cel + + + + Temperature difference + R + Single + Optional + Float + + K + + + + Flow rate + R + Single + Optional + Float + + m3/s + + + + Max Measured flow + R + Single + Optional + Float + + m3/s + + + + Flow volume + R + Single + Optional + Float + + m3 + + + + Return volume + R + Single + Optional + Float + + m3 + + + + Current Time + RW + Single + Optional + Time + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10258.xml b/application/src/main/data/lwm2m-registry/10258.xml new file mode 100644 index 0000000000..e2a9438170 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10258.xml @@ -0,0 +1,89 @@ + + + + + Current Loop Output + + 10258 + urn:oma:lwm2m:x:10258 + 1.0 + 1.0 + Multiple + Optional + + + Current Loop Output Current Value + RW + Single + Mandatory + Float + 3.8..20.5 + mA + + + + Min Range Value + R + Single + Optional + Float + + + + + + Max Range Value + R + Single + Optional + Float + + + + + + Sensor Units + R + Single + Optional + String + + + + + + Application Type + RW + Single + Optional + String + + + + + + Current Calibration + RW + Single + Optional + Float + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10259.xml b/application/src/main/data/lwm2m-registry/10259.xml new file mode 100644 index 0000000000..d75869a122 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10259.xml @@ -0,0 +1,115 @@ + + + + + + + System Log + + 10259 + urn:oma:lwm2m:x:10259 + 1.0 + 1.0 + Multiple + Optional + + + Name + R + Single + Mandatory + String + + + + + + + + Read All + R + Single + Mandatory + String + + + + + + + + Read + R + Single + Optional + String + + + + + + + + Enabled + RW + Single + Optional + Boolean + + + + + + + + Capture Level + RW + Single + Optional + Integer + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10260.xml b/application/src/main/data/lwm2m-registry/10260.xml new file mode 100644 index 0000000000..5cd084af73 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10260.xml @@ -0,0 +1,95 @@ + + + + + + RDB + + 10260 + urn:oma:lwm2m:x:10260:2.0 + 1.0 + 2.0 + Multiple + Optional + + + Key + RW + Single + Mandatory + String + + + + + + + + Value + RW + Single + Optional + String + + + + + + + + Exists + RW + Single + Optional + Boolean + + + + + + + + Persistent + RW + Single + Optional + Boolean + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10262.xml b/application/src/main/data/lwm2m-registry/10262.xml new file mode 100644 index 0000000000..9fd47ac3af --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10262.xml @@ -0,0 +1,89 @@ + + + + + + + Interval Data Delivery + + 10262 + urn:oma:lwm2m:x:10262 + 1.0 + 1.0 + Multiple + Optional + + + Name + RW + Single + Mandatory + String + + + + + + Interval Data Links + RW + Multiple + Mandatory + Objlnk + + + + + + Latest Payload + R + Multiple + Mandatory + Opaque + + + + + + Schedule + RW + Single + Optional + Objlnk + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10263.xml b/application/src/main/data/lwm2m-registry/10263.xml new file mode 100644 index 0000000000..07b9cde3ca --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10263.xml @@ -0,0 +1,92 @@ + + + + + + + Event Data Delivery + + 10263 + urn:oma:lwm2m:x:10263 + 1.0 + 1.0 + Multiple + Optional + + + Name + RW + Single + Mandatory + String + + + + + + Event Data Links + RW + Multiple + Mandatory + Objlnk + + + + + + Latest Eventlog + R + Multiple + Mandatory + Opaque + + + + + + Schedule + RW + Single + Mandatory + Objlnk + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10264.xml b/application/src/main/data/lwm2m-registry/10264.xml new file mode 100644 index 0000000000..b11c42a21d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10264.xml @@ -0,0 +1,119 @@ + + + + + + + Delivery Schedule + + 10264 + urn:oma:lwm2m:x:10264 + 1.0 + 1.0 + Multiple + Optional + + + Schedule Start Time + RW + Single + Mandatory + Integer + + + + + + Schedule UTC Offset + RW + Single + Mandatory + String + + + + + + Delivery Frequency + RW + Single + Mandatory + Integer + + + + + + Randomised Delivery Window + RW + Single + Optional + Integer + + + + + + Number of Retries + RW + Single + Optional + Integer + + + + + + Retry Period + RW + Single + Optional + Integer + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10265.xml b/application/src/main/data/lwm2m-registry/10265.xml new file mode 100644 index 0000000000..2604ae86d7 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10265.xml @@ -0,0 +1,138 @@ + + + + + + + Leakage Detection Configuration + + 10265 + urn:oma:lwm2m:x:10265 + 1.0 + 1.0 + Single + Optional + + + Sample Times + RW + Multiple + Mandatory + Integer + + + + + + Sample UTC Offset + RW + Single + Optional + String + + + + + + Detection Mode + RW + Single + Mandatory + Integer + 0..3 + + + + + Top Frequency Count + RW + Single + Optional + Integer + + + + + + Frequency Thresholds + RW + Multiple + Optional + Integer + 0..999 + + + + + Frequency Values + R + Multiple + Optional + Integer + + + + + + Firmware Version + R + Single + Mandatory + String + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10266.xml b/application/src/main/data/lwm2m-registry/10266.xml new file mode 100644 index 0000000000..48ee41319a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10266.xml @@ -0,0 +1,246 @@ + + + + + + + Water Flow Readings + + 10266 + urn:oma:lwm2m:x:10266 + 1.0 + 1.0 + Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Optional + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Optional + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10267.xml b/application/src/main/data/lwm2m-registry/10267.xml new file mode 100644 index 0000000000..ffe70b5f2f --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10267.xml @@ -0,0 +1,246 @@ + + + + + + + Daily Maximum Flow Rate Readings + + 10267 + urn:oma:lwm2m:x:10267 + 1.0 + 1.0 + Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Optional + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Optional + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10268.xml b/application/src/main/data/lwm2m-registry/10268.xml new file mode 100644 index 0000000000..a1ccd43888 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10268.xml @@ -0,0 +1,246 @@ + + + + + + + Temperature Readings + + 10268 + urn:oma:lwm2m:x:10268 + 1.0 + 1.0 + Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Optional + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Optional + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10269.xml b/application/src/main/data/lwm2m-registry/10269.xml new file mode 100644 index 0000000000..e4156e055e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10269.xml @@ -0,0 +1,246 @@ + + + + + + + Pressure Readings + + 10269 + urn:oma:lwm2m:x:10269 + 1.0 + 1.0 + Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Optional + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Optional + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10270.xml b/application/src/main/data/lwm2m-registry/10270.xml new file mode 100644 index 0000000000..b7d171920c --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10270.xml @@ -0,0 +1,246 @@ + + + + + + + Battery Level Readings + + 10270 + urn:oma:lwm2m:x:10270 + 1.0 + 1.0 + Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Optional + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Optional + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10271.xml b/application/src/main/data/lwm2m-registry/10271.xml new file mode 100644 index 0000000000..b4f8dfa6d5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10271.xml @@ -0,0 +1,246 @@ + + + + + + + Communications Activity Time Readings + + 10271 + urn:oma:lwm2m:x:10271 + 1.0 + 1.0 + Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Optional + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Optional + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10272.xml b/application/src/main/data/lwm2m-registry/10272.xml new file mode 100644 index 0000000000..0464c98a73 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10272.xml @@ -0,0 +1,232 @@ + + + + + + + Water Meter Customer Leakage Alarm + + 10272 + urn:oma:lwm2m:x:10272 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10273.xml b/application/src/main/data/lwm2m-registry/10273.xml new file mode 100644 index 0000000000..823adae0ed --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10273.xml @@ -0,0 +1,232 @@ + + + + + + + Water Meter Reverse Flow Alarm + + 10273 + urn:oma:lwm2m:x:10273 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10274.xml b/application/src/main/data/lwm2m-registry/10274.xml new file mode 100644 index 0000000000..f61554ca88 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10274.xml @@ -0,0 +1,232 @@ + + + + + + + Water Meter Empty Pipe Alarm + + 10274 + urn:oma:lwm2m:x:10274 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10275.xml b/application/src/main/data/lwm2m-registry/10275.xml new file mode 100644 index 0000000000..10248ea511 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10275.xml @@ -0,0 +1,232 @@ + + + + + + + Water Meter Tamper Alarm + + 10275 + urn:oma:lwm2m:x:10275 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10276.xml b/application/src/main/data/lwm2m-registry/10276.xml new file mode 100644 index 0000000000..5863f0326b --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10276.xml @@ -0,0 +1,232 @@ + + + + + + + Water Meter High Pressure Alarm + + 10276 + urn:oma:lwm2m:x:10276 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10277.xml b/application/src/main/data/lwm2m-registry/10277.xml new file mode 100644 index 0000000000..83c6d4f0d5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10277.xml @@ -0,0 +1,232 @@ + + + + + + + Water Meter Low Pressure Alarm + + 10277 + urn:oma:lwm2m:x:10277 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10278.xml b/application/src/main/data/lwm2m-registry/10278.xml new file mode 100644 index 0000000000..d004f89de6 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10278.xml @@ -0,0 +1,232 @@ + + + + + + + High Temperature Alarm + + 10278 + urn:oma:lwm2m:x:10278 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10279.xml b/application/src/main/data/lwm2m-registry/10279.xml new file mode 100644 index 0000000000..38838e7c94 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10279.xml @@ -0,0 +1,232 @@ + + + + + + + Low Temperature Alarm + + 10279 + urn:oma:lwm2m:x:10279 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10280.xml b/application/src/main/data/lwm2m-registry/10280.xml new file mode 100644 index 0000000000..de2b7b6732 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10280.xml @@ -0,0 +1,232 @@ + + + + + + + Water Network Leak Alarm + + 10280 + urn:oma:lwm2m:x:10280 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10281.xml b/application/src/main/data/lwm2m-registry/10281.xml new file mode 100644 index 0000000000..99eab667f2 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10281.xml @@ -0,0 +1,232 @@ + + + + + + + Low Battery Alarm + + 10281 + urn:oma:lwm2m:x:10281 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10282.xml b/application/src/main/data/lwm2m-registry/10282.xml new file mode 100644 index 0000000000..e603de36b3 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10282.xml @@ -0,0 +1,232 @@ + + + + + + + Daughter Board Failure Alarm + + 10282 + urn:oma:lwm2m:x:10282 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10283.xml b/application/src/main/data/lwm2m-registry/10283.xml new file mode 100644 index 0000000000..fd130fa713 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10283.xml @@ -0,0 +1,232 @@ + + + + + + + Device Reboot Event + + 10283 + urn:oma:lwm2m:x:10283 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10284.xml b/application/src/main/data/lwm2m-registry/10284.xml new file mode 100644 index 0000000000..1e889da48a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10284.xml @@ -0,0 +1,232 @@ + + + + + + + Time Synchronisation Event + + 10284 + urn:oma:lwm2m:x:10284 + 1.0 + 1.0 + Multiple + Optional + + + Event Type + RW + Single + Mandatory + Integer + + + + + + Alarm Realtime + RW + Single + Mandatory + Boolean + + + + + + Alarm State + R + Single + Optional + Boolean + + + + + + Alarm Set Threshold + RW + Single + Optional + Float + + + + + + Alarm Set Operator + RW + Single + Optional + Integer + + + + + + Alarm Clear Threshold + RW + Single + Optional + Float + + + + + + Alarm Clear Operator + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Count + RW + Single + Optional + Integer + + + + + + Alarm Maximum Event Period + RW + Single + Optional + Integer + 1..864000 + s + + + + Latest Delivered Event Time + RW + Single + Optional + Time + + + + + + Latest Recorded Event Time + R + Single + Mandatory + Time + + + + + + Alarm Clear + E + Single + Optional + + + + + + + Alarm Auto Clear + RW + Single + Optional + Boolean + + + + + + Event Code + R + Single + Mandatory + Integer + 100..255 + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10286.xml b/application/src/main/data/lwm2m-registry/10286.xml new file mode 100644 index 0000000000..39e0809788 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10286.xml @@ -0,0 +1,48 @@ + + + + App Fota Container + + 10286 + urn:oma:lwm2m:x:10286 + 1.0 + 1.0 + Single + Optional + + + UL data + R + Single + Mandatory + Opaque + + + + + + + + DL data + W + Single + Mandatory + Opaque + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10290.xml b/application/src/main/data/lwm2m-registry/10290.xml new file mode 100644 index 0000000000..7b89cc06c0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10290.xml @@ -0,0 +1,198 @@ + + + + + + + Voltage Logging + + 10290 + urn:oma:lwm2m:x:10290 + 1.0 + 1.0 + Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Optional + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Optional + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10291.xml b/application/src/main/data/lwm2m-registry/10291.xml new file mode 100644 index 0000000000..a232ed02f9 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10291.xml @@ -0,0 +1,208 @@ + + + + + + + Voltage Transient + + 10291 + urn:oma:lwm2m:x:10291 + 1.0 + 1.0 + Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Mandatory + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Mandatory + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + Sample Frequency + RW + Single + Mandatory + Float + 0.0..86400.0 + s + How often the inputs are read/sampled.This value can be changed by doing a write command + + + + + diff --git a/application/src/main/data/lwm2m-registry/10292.xml b/application/src/main/data/lwm2m-registry/10292.xml new file mode 100644 index 0000000000..00fe33e320 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10292.xml @@ -0,0 +1,208 @@ + + + + + + + Pressure Transient + + 10292 + urn:oma:lwm2m:x:10292 + 1.0 + 1.0 + Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Mandatory + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Mandatory + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + Sample Frequency + RW + Single + Mandatory + Float + 0.0..86400.0 + s + How often the inputs are read/sampled.This value can be changed by doing a write command + + + + + diff --git a/application/src/main/data/lwm2m-registry/10299.xml b/application/src/main/data/lwm2m-registry/10299.xml new file mode 100644 index 0000000000..a16a7c4fc2 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10299.xml @@ -0,0 +1,113 @@ + + + + + + + HostDevice + This LWM2M Object provides a range of host device related information which can be queried by the LWM2M Server. The host device is any integrated device with an embedded cellular radio module. + 10299 + urn:oma:lwm2m:x:10299 + 1.0 + 1.0 + Single + Optional + + + Manufacturer + R + Single + Mandatory + String + + + Host device manufacturers name (OEM). + + + Model + R + Single + Mandatory + String + + Identifier of the model name or number determined by device manufacturer. + UniqueID + R + Single + Mandatory + String + + + Unique ID assigned by an manufacturer or other body. Used to uniquely identify a host device. Examples include serial # or UUID. + + + FirmwareVersion + R + Single + Mandatory + String + + + Current Firmware version of the host device. (manufacturer specified string). + + SoftwareVersion + R + Single + Optional + String + + + Current software version of the host device. (manufacturer specified string). + + HardwareVersion + R + Single + Optional + String + + + Current hardware version of the host device. (manufacturer specified string). + + + DateStamp + R + Single + Optional + String + + + UTC value of the time and date of the last Firmware or Software update. Format:MM:DD:YYYY HH:MM:SS + + + + + diff --git a/application/src/main/data/lwm2m-registry/10300.xml b/application/src/main/data/lwm2m-registry/10300.xml new file mode 100644 index 0000000000..234f0301a4 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10300.xml @@ -0,0 +1,142 @@ + + + + + + + LWM2M Meta Object + + + + 10300 + urn:oma:lwm2m:x:10300 + 1.0 + 1.0 + Multiple + Optional + + + ObjectID + R + Single + Mandatory + Integer + + + + + + + + ObjectURN + R + Single + Mandatory + String + + + + + + + + ObjectInstanceHandle + R + Single + Optional + Objlnk + + + + + + + + URI + R + Multiple + Mandatory + String + + + + + + + + SHAType + R + Single + Optional + Integer + 0..8 + + + + + + + ChecksumValue + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10308.xml b/application/src/main/data/lwm2m-registry/10308.xml new file mode 100644 index 0000000000..0ed1a6ea30 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10308.xml @@ -0,0 +1,145 @@ + + + + + + + AT&T Connectivity Extension + + 10308 + urn:oma:lwm2m:x:10308:2.0 + 1.0 + 2.0 + Multiple + Optional + + + ICCID + R + Single + Mandatory + String + + + + + + + IMSI + R + Single + Mandatory + String + + + + + + + MSISDN + RW + Single + Mandatory + String + + + + + + + APN Retries + RW + Single + Mandatory + Integer + + + + + + + APN Retry Period + RW + Single + Mandatory + Integer + + + s + + + + APN Retry Back-Off Period + RW + Single + Mandatory + Integer + + + s + + + + SINR + R + Single + Mandatory + Integer + <7 to >12.5 + + + + + SRXLEV + R + Single + Mandatory + Integer + + + + + + + CE_LEVEL + R + Single + Mandatory + Integer + 0,1,2 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10309.xml b/application/src/main/data/lwm2m-registry/10309.xml new file mode 100644 index 0000000000..b37b435724 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10309.xml @@ -0,0 +1,115 @@ + + + + + Shareparkinglot + + 10309 + urn:oma:lwm2m:x:10309 + 1.0 + 1.0 + Multiple + Optional + + + LockID + R + Single + Mandatory + Integer + + + + + + LockType + R + Single + Optional + String + + + + + + LightSwitchState + R + Multiple + Optional + Boolean + + + + + + RSSI + R + Multiple + Mandatory + Integer + -30..-120 + dBm + + + + BatteryCapacity + R + Multiple + Optional + Float + 0..100 + %EL + + + + DataUpTime + R + Multiple + Mandatory + Time + + + + + + Latitude + R + Multiple + Optional + String + + + + + + Longitude + R + Multiple + Optional + String + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10311.xml b/application/src/main/data/lwm2m-registry/10311.xml new file mode 100644 index 0000000000..6b6a78d21d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10311.xml @@ -0,0 +1,192 @@ + + + + + + + Solar Radiation + This Object is used to report solar irradiance (SI), i.e. power per unit area received from the Sun in the form of electromagnetic radiation, on a planar surface measured by a pyranometer or similar instrument. A pyranometer measures solar irradiance from the hemisphere above within a wavelength range 0.3 um to 3 um. For example, the application of solar radiation measurement can be meterological networks and solar energy applications. + 10311 + urn:oma:lwm2m:x:10311:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Min Measured Value + R + Single + Optional + Float + + + + The minimum value measured by the sensor since power ON or reset. + + + + Max Measured Value + R + Single + Optional + Float + + + + The maximum value measured by the sensor since power ON or reset. + + + + Min Range Value + R + Single + Optional + Float + + + + The minimum value that can be measured by the sensor. + + + + Max Range Value + R + Single + Optional + Float + + + + The maximum value that can be measured by the sensor. + + + + Reset Min and Max Measured Values + E + Single + Optional + + + + + Reset the Min and Max Measured Values to Current Value. + + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + + Measurement Units Definition. + + + + Application Type + RW + Single + Optional + String + + + + The application type of the sensor or actuator as a string depending on the use case. + + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/10313.xml b/application/src/main/data/lwm2m-registry/10313.xml new file mode 100644 index 0000000000..4875e5d6b0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10313.xml @@ -0,0 +1,253 @@ + + + + + + + Gas Readings + + 10313 + urn:oma:lwm2m:x:103131.0 + 1.0Multiple + Optional + + + Interval Period + R + Single + Mandatory + Integer + 1..864000 + s + + + + Interval Start Offset + R + Single + Optional + Integer + 0..86399 + s + + + + Interval UTC Offset + R + Single + Optional + String + + + + + + Interval Collection Start Time + R + Single + Mandatory + Time + + + + + + Oldest Recorded Interval + R + Single + Mandatory + Time + + + + + + Last Delivered Interval + RW + Single + Optional + Time + + + + + + Latest Recorded Interval + R + Single + Mandatory + Time + + + + + + Interval Delivery Midnight Aligned + RW + Single + Mandatory + Boolean + + + + + + Interval Historical Read + E + Single + Optional + + + + + + + Interval Historical Read Payload + R + Single + Optional + Opaque + + + + + + Interval Change Configuration + E + Single + Optional + + + + + + + Start + E + Single + Optional + + + + + + + Stop + E + Single + Optional + + + + + + + Status + R + Single + Optional + Integer + + + + + + Latest Payload + R + Single + Mandatory + Opaque + + + + + + Sensor Warm-up Time + RW + Single + Optional + Integer + 0..86400 + s + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10314.xml b/application/src/main/data/lwm2m-registry/10314.xml new file mode 100644 index 0000000000..f003fa03c2 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10314.xml @@ -0,0 +1,106 @@ + + + + + + + Particulates + + 10314 + urn:oma:lwm2m:x:10314 + 1.0 + 1.0 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + + + + Min Measured Value + R + Single + Optional + Float + + + + + + Max Measured Value + R + Single + Optional + Float + + + + + + Max Range Value + R + Single + Optional + Float + + + + + + Sensor Units + R + Single + Optional + String + + + + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + + + + Measured Particle Size + R + Single + Mandatory + Float + + m + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10315.xml b/application/src/main/data/lwm2m-registry/10315.xml new file mode 100644 index 0000000000..da7ff8c67e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10315.xml @@ -0,0 +1,129 @@ + + + + + + Robot + + 10315 + urn:oma:lwm2m:x:10315 + 1.0 + 1.0 + Single + Mandatory + + + Robot ID + R + Single + Mandatory + String + 0..255 + + + + + Robot Type + R + Single + Mandatory + String + 0..63 + + + + + Robot Serial Number + R + Single + Mandatory + String + 0..63 + + + + + Battery Level + R + Single + Mandatory + Integer + 0..100 + % + + + + Charging + R + Single + Mandatory + Boolean + + + + + + On time + RW + Single + Mandatory + Integer + + s + + + + Positioning + R + Single + Optional + Boolean + + + + + + Location + R + Single + Optional + Objlnk + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10316.xml b/application/src/main/data/lwm2m-registry/10316.xml new file mode 100644 index 0000000000..d10dabf99b --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10316.xml @@ -0,0 +1,233 @@ + + + + + + RCU + + 10316 + urn:oma:lwm2m:x:10316 + 1.0 + 1.0 + Single + Mandatory + + + RCU ID + R + Single + Mandatory + String + 0..127 + + + + + RCU Serial Number + R + Single + Mandatory + String + 0..63 + + + + + RCU Software Version + R + Single + Mandatory + String + 0..63 + + + + + RCU OS Version + R + Single + Mandatory + String + 0..127 + + + + + RCU CPU Info + R + Single + Mandatory + String + 64 + + + + + RCU RAM Info + R + Single + Mandatory + String + 64 + + + + + RCU ROM Size + R + Single + Mandatory + Integer + + GB + + + + RCU ROM Available Size + R + Single + Mandatory + Integer + + GB + + + + SD Storage + R + Single + Mandatory + Integer + + GB + + + + SD Available Storage + R + Single + Mandatory + Integer + + GB + + + + RCU GPS Location + R + Single + Optional + Objlnk + + + + + + Wi-Fi MAC + R + Single + Optional + String + 12 + + + + + Bluetooth MAC + R + Single + Optional + String + 12 + + + + + Camera Info + R + Single + Optional + String + 64 + + + + + + Battery Level + R + Single + Mandatory + Integer + 0..100 + /100 + + + + + On time + RW + Single + Mandatory + Integer + + s + + + + + Downloaded APP Packages + R + Multiple + Mandatory + String + + + + + + + RCU APPs + R + Multiple + Optional + Objlnk + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10318.xml b/application/src/main/data/lwm2m-registry/10318.xml new file mode 100644 index 0000000000..989db0de7e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10318.xml @@ -0,0 +1,189 @@ + + + + + + RCU PM + + 10318 + urn:oma:lwm2m:x:10318 + 1.0 + 1.0 + Single + Mandatory + + + CPU Usage + R + Single + Mandatory + Integer + 0..100 + /100 + + + + Max CPU Usage + R + Single + Mandatory + Integer + 0..100 + /100 + + + + Memory Usage + R + Single + Mandatory + Integer + 0..100 + /100 + + + + Storage Usage + R + Single + Mandatory + Integer + 0..100 + /100 + + + + + Battery Level + R + Single + Mandatory + Integer + 0..100 + /100 + + + + Network Bandwidth + R + Single + Mandatory + Float + + Mbit/s + + + + Mobile Signal + R + Single + Mandatory + Integer + + + + + + GPS Signal + R + Single + Optional + Integer + + + + + + Wi-Fi Signal + R + Single + Mandatory + Integer + + + + + + UpLink Rate + R + Single + Mandatory + Float + + Mbit/s + + + + DownLink Rate + R + Single + Mandatory + Float + + Mbit/s + + + + Packet Loss Rate + R + Single + Mandatory + Integer + 0..100 + /100 + + + + Network Latency + R + Single + Mandatory + Integer + + ms + + + + + Battery Temperature + R + Single + Mandatory + Float + + Cel + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10319.xml b/application/src/main/data/lwm2m-registry/10319.xml new file mode 100644 index 0000000000..b8ebcef6fb --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10319.xml @@ -0,0 +1,130 @@ + + + + + + RCU Control + + 10319 + urn:oma:lwm2m:x:10319 + 1.0 + 1.0 + Single + Mandatory + + + RCU Diagnostics Mode + R + Single + Optional + Boolean + + + + + + RCU Log Recording + R + Single + Optional + Boolean + + + + + + + RCU Shutdown + E + Single + Mandatory + + + + + + + RCU Restart + E + Single + Mandatory + + + + + + + RCU Deactivate + E + Single + Mandatory + + + + + + + RCU Reset + E + Single + Mandatory + + + + + + + RCU Diagnostics Mode Control + E + Single + Mandatory + + + + + + + RCU Log Recording Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10320.xml b/application/src/main/data/lwm2m-registry/10320.xml new file mode 100644 index 0000000000..fb48c814f0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10320.xml @@ -0,0 +1,141 @@ + + + + + + CCU + + 10320 + urn:oma:lwm2m:x:10320 + 1.0 + 1.0 + Multiple + Optional + + + CCU ID + R + Single + Mandatory + String + + + + + + CCU FM Version + R + Single + Mandatory + String + + + + + + CCU SW Version + R + Single + Mandatory + String + + + + + + CCU Memory Size + R + Single + Mandatory + Integer + + GB + + + + CCU Storage + R + Single + Mandatory + Integer + + GB + + + + CCU Available Storage + R + Single + Mandatory + Integer + + GB + + + + + On time + RW + Single + Mandatory + Integer + + s + + + + + Downloaded APP Packages + R + Multiple + Optional + String + + + + + + CCU APPs + R + Multiple + Optional + Objlnk + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10322.xml b/application/src/main/data/lwm2m-registry/10322.xml new file mode 100644 index 0000000000..98822e0efe --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10322.xml @@ -0,0 +1,87 @@ + + + + + + CCU PM + + 10322 + urn:oma:lwm2m:x:10322 + 1.0 + 1.0 + Multiple + Optional + + + CPU Usage + R + Single + Optional + Integer + 0..100 + % + + + + Max CPU Usage + R + Single + Optional + Integer + 0..100 + % + + + + Memory Usage + R + Single + Optional + Integer + 0..100 + % + + + + Storage Usage + R + Single + Optional + Integer + 0..100 + % + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10323.xml b/application/src/main/data/lwm2m-registry/10323.xml new file mode 100644 index 0000000000..dcbdaeb91d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10323.xml @@ -0,0 +1,90 @@ + + + + + + CCU Control + + 10323 + urn:oma:lwm2m:x:10323 + 1.0 + 1.0 + Multiple + Optional + + + + CCU Restart + E + Single + Mandatory + + + + + + + CCU Reset + E + Single + Mandatory + + + + + + + CCU Self-checking + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10324.xml b/application/src/main/data/lwm2m-registry/10324.xml new file mode 100644 index 0000000000..b3db2b4f06 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10324.xml @@ -0,0 +1,77 @@ + + + + + + ECU + + 10324 + urn:oma:lwm2m:x:10324 + 1.0 + 1.0 + Multiple + Optional + + + ECU ID + R + Single + Mandatory + String + + + + + + ECU FM Version + R + Single + Mandatory + String + + + + + + On time + RW + Single + Mandatory + Integer + + s + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10326.xml b/application/src/main/data/lwm2m-registry/10326.xml new file mode 100644 index 0000000000..cdc01a0e61 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10326.xml @@ -0,0 +1,722 @@ + + + + + + Robot PM + + 10326 + urn:oma:lwm2m:x:10326 + 1.0 + 1.0 + Single + Mandatory + + + Battery Level + R + Single + Mandatory + Integer + 0..100 + /100 + , for example: ]]> + + + + Battery Temperature + R + Single + Mandatory + Integer + + Cel + + + + Temperature + R + Single + Optional + Float + + Cel + + + + Humidity + R + Single + Optional + Integer + 0..100 + /100 + + + + PM2.5 + R + Single + Optional + Integer + + ug/m3 + + + + Smog + R + Single + Optional + Float + + ug/m3 + + + + CO + R + Single + Optional + Float + + ppm + + + + CO2 + R + Single + Optional + Float + + ppm + + + + PM10 + R + Single + Optional + Integer + + ug/m3 + + + + Speed + R + Single + Optional + Float + + m/h + + + + Water Used + R + Single + Optional + Integer + 0..100 + /100 + + + + Dust Box Used + R + Single + Optional + Integer + 0..100 + /100 + + + + Obstacle Distance + R + Single + Optional + Integer + + cm + + + + + Robot Temperate + R + Single + Optional + Float + + Cel + + + + Confidence Index + R + Single + Optional + Integer + 0..100 + /100 + + + + + Data Traffic Used + R + Single + Mandatory + Float + + Mbit/s + + + + Images Handled + R + Single + Optional + Integer + + + + + + HARI S-Voice Requests + R + Single + Optional + Integer + + + + + + HARI S-Vision Requests + R + Single + Optional + Integer + + + + + + HARI S-Motion Requests + R + Single + Optional + Integer + + + + + + HARI S-Map Requests + R + Single + Optional + Integer + + + + + + Successful HARI S-Voice Requests + R + Single + Optional + Integer + + + + + + Successful HARI S-Vision Requests + R + Single + Optional + Integer + + + + + + Successful HARI S-Motion Requests + R + Single + Optional + Integer + + + + + + Successful HARI S-Map Requests + R + Single + Optional + Integer + + + + + + Questions Answered + R + Single + Optional + Integer + + + + + + Unknown Questions + R + Single + Optional + Integer + + + + + + Mileage + R + Single + Optional + Integer + + m + + + + Cleaned Times + R + Single + Optional + Integer + + + + + + Cleaned Area + R + Single + Optional + Float + + m2 + + + + Cleaned Time + R + Single + Optional + Integer + + s + + + + ASR Recognized + R + Single + Optional + Integer + + B + + + + Incorrect ASR Recognitions + R + Single + Optional + Integer + + B + + + + Tried TTS Texts + R + Single + Optional + Integer + + B + + + + Successful TTS Texts + R + Single + Optional + Integer + + B + + + + ASR Recognized (CH) + R + Single + Optional + Integer + + B + + + + Tried TTS Texts (CH) + R + Single + Optional + Integer + + B + + + + Successful TTS Texts (CH) + R + Single + Optional + Integer + + B + + + + ASR Recognized (EN) + R + Single + Optional + Integer + + B + + + + Tried TTS Texts (EN) + R + Single + Optional + Integer + + B + + + + Successful TTS Texts (EN) + R + Single + Optional + Integer + + B + + + + ASR Recognized (ES) + R + Single + Optional + Integer + + B + + + + Tried TTS Texts (ES) + R + Single + Optional + Integer + + B + + + + Successful TTS Texts (ES) + R + Single + Optional + Integer + + B + + + + ASR Recognized (JA) + R + Single + Optional + Integer + + B + + + + Tried TTS Texts (JA) + R + Single + Optional + Integer + + B + + + + Successful TTS Texts (JA) + R + Single + Optional + Integer + + B + + + + ASR Recognized (SCCH) + R + Single + Optional + Integer + + B + + + + Tried TTS Texts (SCCH) + R + Single + Optional + Integer + + B + + + + Successful TTS Texts (SCCH) + R + Single + Optional + Integer + + B + + + + ASR Recognized (GDCH) + R + Single + Optional + Integer + + B + + + + Tried TTS Texts (GDCH) + R + Single + Optional + Integer + + B + + + + Successful TTS Texts (GDCH) + R + Single + Optional + Integer + + B + + + + ASR Recognized (TCH) + R + Single + Optional + Integer + + B + + + + Tried TTS Texts (TCH) + R + Single + Optional + Integer + + B + + + + Successful TTS Texts (TCH) + R + Single + Optional + Integer + + B + + + + + Objects Recognition Tries + R + Single + Optional + Integer + + + + + + Successful Object Recognition + R + Single + Optional + Integer + + + + + + Face Recognition Tries + R + Single + Optional + Integer + + + + + + Successful Face Recognitions + R + Single + Optional + Integer + + + + + + Vehicle Plate Recognition Tries + R + Single + Optional + Integer + + + + + + Successful Vehicle Plate Recognitions + R + Single + Optional + Integer + + + + + + Tasks Assigned + R + Single + Mandatory + Integer + + + + + + Successful Tasks Executed + R + Single + Mandatory + Integer + + + + + + Images Uploaded + R + Single + Optional + Integer + + + + + + Videos Uploaded + R + Single + Optional + Integer + + + + + + Images Matted + R + Single + Optional + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10327.xml b/application/src/main/data/lwm2m-registry/10327.xml new file mode 100644 index 0000000000..5f0e232a22 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10327.xml @@ -0,0 +1,68 @@ + + + + + + Compressor + + 10327 + urn:oma:lwm2m:x:10327 + 1.0 + 1.0 + Multiple + Optional + + + Compressor Name + R + Single + Mandatory + String + + + + + + + Compressor Status + R + Single + Mandatory + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10328.xml b/application/src/main/data/lwm2m-registry/10328.xml new file mode 100644 index 0000000000..fdf39d5dc1 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10328.xml @@ -0,0 +1,80 @@ + + + + + + SCA PM + + 10328 + urn:oma:lwm2m:x:10328 + 1.0 + 1.0 + Multiple + Optional + + + SCA Name + R + Single + Mandatory + String + + + + + + + + + SCA Current + R + Single + Mandatory + Float + + A + + + + SCA Temperate + R + Single + Mandatory + Float + + Cel + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10329.xml b/application/src/main/data/lwm2m-registry/10329.xml new file mode 100644 index 0000000000..7470bec6e1 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10329.xml @@ -0,0 +1,433 @@ + + + + + + Robot Control + + 10329 + urn:oma:lwm2m:x:10329 + 1.0 + 1.0 + Single + Mandatory + + + Collision Detection + R + Single + Optional + Boolean + + + + + + Drop Detection + R + Single + Optional + Boolean + + + + + + Automatic Navigation + R + Single + Optional + Boolean + + + + + + Robot Shutdown + E + Single + Mandatory + + + + + + + Robot Reboot + E + Single + Mandatory + + + + + + + Robot Reset + E + Single + Mandatory + + + + + + + Robot Wakeup + E + Single + Mandatory + + + + + + + Robot Sleep + E + Single + Mandatory + + + + + + + Robot Self-checking + E + Single + Mandatory + + + + + + + Emergency Braking + E + Single + Mandatory + + + + + + + Emergency Braking Release + E + Single + Mandatory + + + + + + + Action Execution + E + Single + Optional + + + + + + + Action List Upload + E + Single + Optional + + + + + + + Action List Download + E + Single + Optional + + + + + + + Group Dancing Program Control + E + Single + Optional + + + + + + + Navigation Map Upload + E + Single + Optional + + + + + + + Group Dancing Program Control + E + Single + Optional + + + + + + + Navigation Map Download + E + Single + Optional + + + + + + + Route list Execution + E + Single + Optional + + + + + + + Route list Upload + E + Single + Optional + + + + + + + Route list Download + E + Single + Optional + + + + + + + Automatic Navigation Control + E + Single + Optional + + + + + + + Manual Navigation + E + Single + Optional + + + + + + + Moving to Charging Station + E + Single + Optional + + + + + + + Moving to Specified location + E + Single + Optional + + + + + + + Low Frequency Patrol Broadcasting + E + Single + Optional + + + + + + + Task Start + E + Single + Optional + + + + + + + Task Stop + E + Single + Optional + + + + + + + Task Suspend + E + Single + Optional + + + + + + + Task Resume + E + Single + Optional + + + + + + + Video Upload + E + Single + Optional + + + + + + + Picture Upload + E + Single + Optional + + + + + + + Default Language Switching + E + Single + Optional + + + + + + + Intonation Change + E + Single + Optional + + + + + + + Intonation Change + E + Single + Optional + + + + + + + Speaking with Action + E + Single + Optional + + + + + + + Collision Detection Control + E + Single + Mandatory + + + + + + + Drop Detection Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10330.xml b/application/src/main/data/lwm2m-registry/10330.xml new file mode 100644 index 0000000000..75c5a2ecdd --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10330.xml @@ -0,0 +1,120 @@ + + + + + + Network Info + + 10330 + urn:oma:lwm2m:x:10330 + 1.0 + 1.0 + Single + Mandatory + + + IMEI + R + Single + Mandatory + String + 15 + + + + + IMSI + R + Single + Mandatory + String + 15 + + + + + Radio Connectivity + R + Single + Mandatory + Objlnk + + + + + + + + GPS Signal Status + R + Single + Optional + Integer + 1..4 + + + + + VBN Connection Status + R + Single + Mandatory + Integer + 0..1 + + + + + HARI Connection Status + R + Single + Mandatory + Integer + 0..1 + + + + + CCU Connection Status + R + Multiple + Optional + Integer + 0..1 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10331.xml b/application/src/main/data/lwm2m-registry/10331.xml new file mode 100644 index 0000000000..ca555cc0d9 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10331.xml @@ -0,0 +1,221 @@ + + + + + + Robot Service Info + + 10331 + urn:oma:lwm2m:x:10331 + 1.0 + 1.0 + Single + Mandatory + + + Current status + R + Single + Mandatory + String + 0..127 + + + + + Services Providing + R + Single + Optional + String + 0..127 + + + + + Advertising Contents + R + Single + Mandatory + String + + + + + + Current Language + R + Single + Optional + String + 0..127 + + + + + Volume + R + Single + Optional + String + 0..100 + /100 + + + + Moving Status + R + Single + Optional + Integer + 0..2 + + + + + Moving Speed + R + Single + Optional + Float + + m/h + + + + Location + R + Single + Optional + Objlnk + + + + + + + + Map List + R + Single + Optional + String + + + + + + Planned Route list + R + Single + Optional + String + + + + + + Current Route + R + Single + Optional + String + + + + + + Route to-do List + R + Single + Optional + String + + + + + + Synchronous Whistle + R + Single + Mandatory + Boolean + + + + + + Current Actions + R + Single + Optional + String + + + + + + ASR Type + R + Single + Optional + Integer + 0..2 + + + + + + TTS Vendor + R + Single + Optional + String + + + + + + TTS Speaker + R + Single + Optional + String + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10332.xml b/application/src/main/data/lwm2m-registry/10332.xml new file mode 100644 index 0000000000..f582373043 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10332.xml @@ -0,0 +1,69 @@ + + + + + + Robot Selfcheck Info + + 10332 + urn:oma:lwm2m:x:10332 + 1.0 + 1.0 + Multiple + Optional + + + Entity + R + Single + Mandatory + String + 4..63 + + + .]]> + + + + Status + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10333.xml b/application/src/main/data/lwm2m-registry/10333.xml new file mode 100644 index 0000000000..aaeb25487d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10333.xml @@ -0,0 +1,88 @@ + + + + + + PM Threshold + + 10333 + urn:oma:lwm2m:x:10333 + 1.0 + 1.0 + Single + Optional + + + Entity + RW + Multiple + Mandatory + String + 4..63 + + .]]> + + + Performance Type + RW + Multiple + Mandatory + String + + + , for example:/10322/2/4]]> + + + High Threshold + RW + Multiple + Mandatory + Float + + + + + + Low Threshold + RW + Multiple + Mandatory + Float + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10334.xml b/application/src/main/data/lwm2m-registry/10334.xml new file mode 100644 index 0000000000..479406630c --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10334.xml @@ -0,0 +1,129 @@ + + + + + + Robot Alarm + + 10334 + urn:oma:lwm2m:x:10334 + 1.0 + 1.0 + Multiple + Optional + + + Entity + R + Single + Mandatory + String + 4..63 + + .]]> + + + Probable Cause + R + Single + Mandatory + Integer + 0..65535 + + + + + Specific Problem + R + Single + Mandatory + String + + + + + + Alarm Type + R + Single + Mandatory + Integer + 2..6 + + + + + Severity + R + Single + Mandatory + Integer + 1..5 + + + + + Report Time + R + Single + Mandatory + Time + + + + + + Sequence No + R + Single + Mandatory + Integer + 0..2^63-1 + + + + + Additional Info + R + Single + Optional + String + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10335.xml b/application/src/main/data/lwm2m-registry/10335.xml new file mode 100644 index 0000000000..91fe8cd9c5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10335.xml @@ -0,0 +1,97 @@ + + + + + + Event + + 10335 + urn:oma:lwm2m:x:10335 + 1.0 + 1.0 + Multiple + Optional + + + Entity + R + Single + Mandatory + String + 4..63 + + .]]> + + + Event Type + R + Single + Mandatory + Integer + 0..65535 + + + + + Time + R + Single + Mandatory + Time + + + + + + Sequence No + R + Single + Mandatory + Integer + 0..2^63-1 + + + + + Additional Info + R + Single + Optional + String + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10336.xml b/application/src/main/data/lwm2m-registry/10336.xml new file mode 100644 index 0000000000..5016e6ee13 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10336.xml @@ -0,0 +1,84 @@ + + + + + + MIC + + 10336 + urn:oma:lwm2m:x:10336 + 1.0 + 1.0 + Single + Optional + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10337.xml b/application/src/main/data/lwm2m-registry/10337.xml new file mode 100644 index 0000000000..6204020400 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10337.xml @@ -0,0 +1,118 @@ + + + + + + SCA + + 10337 + urn:oma:lwm2m:x:10337 + 1.0 + 1.0 + Multiple + Optional + + + SCA Name + R + Single + Mandatory + String + + + + + + + + + + SCA Motion Status + R + Single + Optional + Integer + 0..2 + + + + + + SCA Motion Control + E + Single + Optional + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10338.xml b/application/src/main/data/lwm2m-registry/10338.xml new file mode 100644 index 0000000000..c66df8addf --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10338.xml @@ -0,0 +1,124 @@ + + + + + + Speaker + + 10338 + urn:oma:lwm2m:x:10338 + 1.0 + 1.0 + Single + Optional + + + Speaker status + R + Single + Mandatory + Boolean + + + + + + Voice Warning + R + Single + Mandatory + Boolean + + + + + + + Voice Control + E + Single + Mandatory + + + + + + + Voice Warning Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10339.xml b/application/src/main/data/lwm2m-registry/10339.xml new file mode 100644 index 0000000000..5376de4a6c --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10339.xml @@ -0,0 +1,114 @@ + + + + + + Tripod Head + + 10339 + urn:oma:lwm2m:x:10339 + 1.0 + 1.0 + Single + Optional + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + Tripod Direction Control + E + Single + Optional + + + + + + + Tripod Automatic Control + E + Single + Optional + + + + + + + Tripod Reset + E + Single + Optional + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10340.xml b/application/src/main/data/lwm2m-registry/10340.xml new file mode 100644 index 0000000000..679b46f150 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10340.xml @@ -0,0 +1,218 @@ + + + + + + Camera + + 10340 + urn:oma:lwm2m:x:10340 + 1.0 + 1.0 + Multiple + Optional + + + Camera Name + R + Single + Mandatory + String + + + + + + + + Camera Status + R + Single + Mandatory + Boolean + + + + + + Connection Status + R + Single + Mandatory + Boolean + + + + + + Working Status + R + Single + Mandatory + Integer + 0..15 + + + + + Local Recording + R + Single + Mandatory + Boolean + + + + + + Image Matting + R + Single + Mandatory + Boolean + + + + + + Camera Snapshot + R + Single + Mandatory + Boolean + + + + + + Camera Recording + R + Single + Mandatory + Boolean + + + + + + + Camera Control + E + Single + Mandatory + + + + + + + Local Recording Control + E + Single + Mandatory + + + + + + + Image Matting Control + E + Single + Mandatory + + + + + + + Camera Snapshot Control + E + Single + Mandatory + + + + + + + Camera Recording Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10341.xml b/application/src/main/data/lwm2m-registry/10341.xml new file mode 100644 index 0000000000..c632fd5c57 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10341.xml @@ -0,0 +1,84 @@ + + + + + + GPS + + 10341 + urn:oma:lwm2m:x:10341 + 1.0 + 1.0 + Single + Optional + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10342.xml b/application/src/main/data/lwm2m-registry/10342.xml new file mode 100644 index 0000000000..644bcf9071 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10342.xml @@ -0,0 +1,84 @@ + + + + + + IMU + + 10342 + urn:oma:lwm2m:x:10342 + 1.0 + 1.0 + Single + Optional + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10343.xml b/application/src/main/data/lwm2m-registry/10343.xml new file mode 100644 index 0000000000..a34c80f3a6 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10343.xml @@ -0,0 +1,96 @@ + + + + + + LiDAR + + 10343 + urn:oma:lwm2m:x:10343 + 1.0 + 1.0 + Multiple + Optional + + + LiDAR Name + R + Single + Mandatory + String + + + + + + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10344.xml b/application/src/main/data/lwm2m-registry/10344.xml new file mode 100644 index 0000000000..d28d343ae3 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10344.xml @@ -0,0 +1,96 @@ + + + + + + Arm + + 10344 + urn:oma:lwm2m:x:10344 + 1.0 + 1.0 + Multiple + Optional + + + Arm Name + R + Single + Mandatory + String + + + + + + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10345.xml b/application/src/main/data/lwm2m-registry/10345.xml new file mode 100644 index 0000000000..ad489a7faa --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10345.xml @@ -0,0 +1,96 @@ + + + + + + Leg + + 10345 + urn:oma:lwm2m:x:10345 + 1.0 + 1.0 + Multiple + Optional + + + Leg Name + R + Single + Mandatory + String + + + + + + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10346.xml b/application/src/main/data/lwm2m-registry/10346.xml new file mode 100644 index 0000000000..4ee595e16a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10346.xml @@ -0,0 +1,96 @@ + + + + + + Servomotor + + 10346 + urn:oma:lwm2m:x:10346 + 1.0 + 1.0 + Multiple + Optional + + + Servomotor Name + R + Single + Mandatory + String + + + + + + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10347.xml b/application/src/main/data/lwm2m-registry/10347.xml new file mode 100644 index 0000000000..6c0808ed61 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10347.xml @@ -0,0 +1,154 @@ + + + + + + Screen + + 10347 + urn:oma:lwm2m:x:10347 + 1.0 + 1.0 + Single + Optional + + + Screen Status + R + Single + Mandatory + Boolean + + + + + + Startup Page + R + Single + Optional + String + + + The Startup Page of the screen. + + + Current Displaying Page or Current Screen Play List + R + Single + Optional + String + + + Current Displaying Page or Current Screen Play List. + + + + Screen Control + E + Single + Mandatory + + + + + + + Startup Page Set + E + Single + Mandatory + + + + + + + Screen Page Set + E + Single + Mandatory + + + + + + + Screen Play List Set + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10348.xml b/application/src/main/data/lwm2m-registry/10348.xml new file mode 100644 index 0000000000..76451605f0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10348.xml @@ -0,0 +1,96 @@ + + + + + + Wheel + + 10348 + urn:oma:lwm2m:x:10348 + 1.0 + 1.0 + Multiple + Optional + + + Wheel Name + R + Single + Mandatory + String + + + + + + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10349.xml b/application/src/main/data/lwm2m-registry/10349.xml new file mode 100644 index 0000000000..a4212558c4 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10349.xml @@ -0,0 +1,84 @@ + + + + + + Chassis + + 10349 + urn:oma:lwm2m:x:10349 + 1.0 + 1.0 + Single + Optional + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10350.xml b/application/src/main/data/lwm2m-registry/10350.xml new file mode 100644 index 0000000000..1942cabe0c --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10350.xml @@ -0,0 +1,116 @@ + + + + + + Light + + 10350 + urn:oma:lwm2m:x:10350 + 1.0 + 1.0 + Multiple + Optional + + + Light Name + R + Single + Mandatory + String + + + + + + + + Light Status + R + Single + Mandatory + Boolean + + + + + + + Light Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10351.xml b/application/src/main/data/lwm2m-registry/10351.xml new file mode 100644 index 0000000000..473b440b02 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10351.xml @@ -0,0 +1,79 @@ + + + + + + Door + + 10351 + urn:oma:lwm2m:x:10351 + 1.0 + 1.0 + Multiple + Optional + + + Door Name + R + Single + Mandatory + String + + + + + + + Door Status + R + Single + Mandatory + Boolean + + + + + + + Door Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10352.xml b/application/src/main/data/lwm2m-registry/10352.xml new file mode 100644 index 0000000000..1e7503ef42 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10352.xml @@ -0,0 +1,114 @@ + + + + + + Thermal Imager + + 10352 + urn:oma:lwm2m:x:10352 + 1.0 + 1.0 + Single + Optional + + + + Highest Temperature + R + Single + Mandatory + Float + -100.0..100.0 + Cel + The Highest Temperature of the thermal imager. + + + Lowest Temperature + R + Single + Mandatory + Float + -100.0..100.0 + Cel + The Lowest Temperature of the thermal imager. + + + Average Temperature + R + Single + Mandatory + Float + -100.0..100.0 + Cel + The Average Temperature of the thermal imager. + + + + + diff --git a/application/src/main/data/lwm2m-registry/10353.xml b/application/src/main/data/lwm2m-registry/10353.xml new file mode 100644 index 0000000000..0b6bb56cc4 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10353.xml @@ -0,0 +1,87 @@ + + + + + + Warning Light + + 10353 + urn:oma:lwm2m:x:10353 + 1.0 + 1.0 + Single + Optional + + + Light Status + R + Single + Mandatory + Boolean + + + + + + Light Warning + R + Single + Mandatory + Boolean + + + + + + Light Control + E + Single + Mandatory + + + + + + + Light Warning Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10354.xml b/application/src/main/data/lwm2m-registry/10354.xml new file mode 100644 index 0000000000..60d5fc0050 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10354.xml @@ -0,0 +1,217 @@ + + + + + + APP + + 10354 + urn:oma:lwm2m:x:10354 + 1.0 + 1.0 + Multiple + Mandatory + + + APP Name + RW + Single + Mandatory + String + + + The name of the APP, human readable string. + + + APP Version + RW + Single + Mandatory + String + + + The version of the APP, human readable string. + + + APP Build No + RW + Single + Optional + String + + + The Build No of the APP, human readable string. + + + APP Patch No + RW + Single + Optional + String + + + The Patch No of the APP, human readable string. + + + Package URI + W + Single + Optional + String + 0..255 bytes + + + + + Vendor Name + RW + Single + Optional + String + + + The vendor of the package. + + + Installation Target + RW + Single + Mandatory + Objlnk + + + + + + APP Status + R + Single + Mandatory + Integer + 0..5 + + The Status of the APP, 0:Downloading, 1:Downloaded, 2:Installed, 3:Verified, 4:Activated, 5:Stopped. + + + APP Restart + E + Single + Mandatory + + + + + + + APP Start + E + Single + Mandatory + + + + + + + APP Stop + E + Single + Mandatory + + + + + + + APP Download + E + Single + Mandatory + + + + + + + APP Install + E + Single + Mandatory + + + + + + + APP Uninstall + E + Single + Mandatory + + + + + + + APP Activate + E + Single + Mandatory + + + + + + + APP Deactivate + E + Single + Mandatory + + + + + + + APP Verify + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10355.xml b/application/src/main/data/lwm2m-registry/10355.xml new file mode 100644 index 0000000000..fb38ee2767 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10355.xml @@ -0,0 +1,95 @@ + + + + + + General Info + + 10355 + urn:oma:lwm2m:x:10355 + 1.0 + 1.0 + Single + Optional + + + Robot General Info + R + Single + Mandatory + Objlnk + + + + + + + + RCU General Info + R + Single + Mandatory + Objlnk + + + + + + + + CCU General Info + R + Multiple + Optional + Objlnk + + + + + + + + ECU General Info + R + Multiple + Optional + Objlnk + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10356.xml b/application/src/main/data/lwm2m-registry/10356.xml new file mode 100644 index 0000000000..2e362ce052 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10356.xml @@ -0,0 +1,203 @@ + + + + + + Service Info + + 10356 + urn:oma:lwm2m:x:10356 + 1.0 + 1.0 + Single + Optional + + + Robot Service Info + R + Single + Mandatory + Objlnk + + + + + + + + SCA Info + R + Multiple + Optional + Objlnk + + + + + + + + Speaker Info + R + Single + Optional + Objlnk + + + + + + + + Camera Info + R + Multiple + Optional + Objlnk + + + + + + + + Screen Info + R + Single + Optional + Objlnk + + + + + + + + Light Info + R + Multiple + Optional + Objlnk + + + + + + + + Warning Light Info + R + Single + Optional + Objlnk + + + + + + + + Door Info + R + Multiple + Optional + Objlnk + + + + + + + + Thermal Imager Info + R + Single + Optional + Objlnk + + + + + + + + Compressor Info + R + Multiple + Optional + Objlnk + + + + + + + + Lock Info + R + Multiple + Optional + Objlnk + + + + + + + + Collision Sensor Info + R + Multiple + Optional + Objlnk + + + + + + + + Drop Sensor Info + R + Multiple + Optional + Objlnk + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10357.xml b/application/src/main/data/lwm2m-registry/10357.xml new file mode 100644 index 0000000000..9487b2813d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10357.xml @@ -0,0 +1,95 @@ + + + + + + PM + + 10357 + urn:oma:lwm2m:x:10357 + 1.0 + 1.0 + Single + Optional + + + Robot PM + R + Single + Mandatory + Objlnk + + + + + + + + RCU PM + R + Single + Mandatory + Objlnk + + + + + + + + CCU PM + R + Multiple + Optional + Objlnk + + + + + + + + SCA PM + R + Multiple + Optional + Objlnk + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10358.xml b/application/src/main/data/lwm2m-registry/10358.xml new file mode 100644 index 0000000000..27a3684b22 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10358.xml @@ -0,0 +1,70 @@ + + + + + + Fan PM + + 10358 + urn:oma:lwm2m:x:10358 + 1.0 + 1.0 + Multiple + Optional + + + Fan Name + R + Single + Mandatory + String + + + + + + + + + Fan Speed + R + Single + Optional + Integer + + 1/min + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10359.xml b/application/src/main/data/lwm2m-registry/10359.xml new file mode 100644 index 0000000000..2fc8af9677 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10359.xml @@ -0,0 +1,78 @@ + + + + + + Lock + + 10359 + urn:oma:lwm2m:x:10359 + 1.0 + 1.0 + Multiple + Optional + + + Lock Name + R + Single + Mandatory + String + + + + + + Lock Status + R + Single + Optional + Boolean + + + + + + + Lock Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10360.xml b/application/src/main/data/lwm2m-registry/10360.xml new file mode 100644 index 0000000000..935fea848c --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10360.xml @@ -0,0 +1,70 @@ + + + + + + Ultrasonic Sensor + + 10360 + urn:oma:lwm2m:x:10360 + 1.0 + 1.0 + Multiple + Optional + + + Name + R + Single + Mandatory + String + + + + + + + + + Status + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10361.xml b/application/src/main/data/lwm2m-registry/10361.xml new file mode 100644 index 0000000000..3525647afd --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10361.xml @@ -0,0 +1,92 @@ + + + + + + Collision Sensor + + 10361 + urn:oma:lwm2m:x:10361 + 1.0 + 1.0 + Multiple + Optional + + + Name + R + Single + Mandatory + String + + + + + + + + Status + R + Single + Mandatory + Integer + + + + + + + Collision Detection + R + Single + Optional + Boolean + + + + + + + Collision Detection Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10362.xml b/application/src/main/data/lwm2m-registry/10362.xml new file mode 100644 index 0000000000..8a928e4425 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10362.xml @@ -0,0 +1,91 @@ + + + + + + Drop Sensor + + 10362 + urn:oma:lwm2m:x:10362 + 1.0 + 1.0 + Multiple + Optional + + + Name + R + Single + Mandatory + String + + + + + + + + Status + R + Single + Mandatory + Integer + + + + + + Drop Detection + R + Single + Optional + Boolean + + + + + + + Drop Detection Control + E + Single + Mandatory + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10363.xml b/application/src/main/data/lwm2m-registry/10363.xml new file mode 100644 index 0000000000..ca9d064f2f --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10363.xml @@ -0,0 +1,57 @@ + + + + + + Temperature Sensor + + 10363 + urn:oma:lwm2m:x:10363 + 1.0 + 1.0 + Single + Optional + + + Status + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10364.xml b/application/src/main/data/lwm2m-registry/10364.xml new file mode 100644 index 0000000000..3519665e62 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10364.xml @@ -0,0 +1,57 @@ + + + + + + Humidity Sensor + + 10364 + urn:oma:lwm2m:x:10364 + 1.0 + 1.0 + Single + Optional + + + Status + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10365.xml b/application/src/main/data/lwm2m-registry/10365.xml new file mode 100644 index 0000000000..792844271e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10365.xml @@ -0,0 +1,57 @@ + + + + + + Gas-Dust Sensor + + 10365 + urn:oma:lwm2m:x:10365 + 1.0 + 1.0 + Single + Optional + + + Status + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10366.xml b/application/src/main/data/lwm2m-registry/10366.xml new file mode 100644 index 0000000000..44e94ae51d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10366.xml @@ -0,0 +1,59 @@ + + + + + + Fan + + 10366 + urn:oma:lwm2m:x:10366 + 1.0 + 1.0 + Multiple + Optional + + + Fan Name + R + Single + Mandatory + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10368.xml b/application/src/main/data/lwm2m-registry/10368.xml new file mode 100644 index 0000000000..68d3dd3ee3 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10368.xml @@ -0,0 +1,96 @@ + + + + + + SpringMotor + + 10368 + urn:oma:lwm2m:x:10368 + 1.0 + 1.0 + Multiple + Optional + + + SpringMotor Name + R + Single + Mandatory + String + + + + + + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10369.xml b/application/src/main/data/lwm2m-registry/10369.xml new file mode 100644 index 0000000000..dcf8190bda --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10369.xml @@ -0,0 +1,84 @@ + + + + + + MCU + + 10369 + urn:oma:lwm2m:x:10369 + 1.0 + 1.0 + Single + Optional + + + Host Device Unique ID + R + Single + Optional + String + + + + + + + + Host Device Manufacturer + R + Single + Optional + String + + + + + + + + Host Device Model Number + R + Single + Optional + String + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10371.xml b/application/src/main/data/lwm2m-registry/10371.xml new file mode 100644 index 0000000000..73a4476865 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10371.xml @@ -0,0 +1,85 @@ + + + + + Reboot Status + + 10371 + urn:oma:lwm2m:x:10371 + 1.0 + 1.0 + Single + Optional + + + Reboot State + R + Single + Mandatory + Integer + 0..2 + + + + + Reboot Error Message + R + Single + Mandatory + String + + + + + + Reset Factory State + R + Single + Optional + Integer + 0..3 + + + + + Reset Factory Error Message + R + Single + Optional + String + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10374.xml b/application/src/main/data/lwm2m-registry/10374.xml new file mode 100644 index 0000000000..a51646b95f --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10374.xml @@ -0,0 +1,161 @@ + + + + Modbus Connection + + 10374 + urn:oma:lwm2m:x:10374 + 1.0 + 1.0 + Multiple + Optional + + + Address + RW + Single + Mandatory + String + + + + + + Physical Layer Type + RW + Single + Mandatory + Integer + 0..2 + + + + + Enable + RW + Single + Mandatory + Boolean + + + + + + Baudrate + RW + Single + Optional + Integer + + bit/s + + + + Stop Bits + RW + Single + Optional + Integer + 1..2 + + + + + Parity + RW + Single + Optional + Integer + 0..2 + + + + + Hardware Control Flow Mode + RW + Single + Optional + Integer + 0..2 + + + + + Available RTU Ports + R + Multiple + Optional + String + + + + + + State + R + Single + Mandatory + Integer + 0..4 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10375.xml b/application/src/main/data/lwm2m-registry/10375.xml new file mode 100644 index 0000000000..5818491c51 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10375.xml @@ -0,0 +1,148 @@ + + + + Modbus Register Cluster + + 10375 + urn:oma:lwm2m:x:10375 + 1.0 + 1.0 + Multiple + Optional + + + Connection + RW + Single + Mandatory + Objlnk + + + + + + Register Type + RW + Single + Mandatory + Integer + 1..4 + + + + + Unit ID + RW + Single + Mandatory + Integer + 0..247, 255 + + + + + Starting Register Address + RW + Single + Mandatory + Integer + 0..65535 + + + + + Quantity of Registers + RW + Single + Optional + Integer + 1..2000 + + + + + Values + RW + Multiple + Mandatory + Integer + 0..65535 + + + + + Minimum Measured Values + R + Multiple + Optional + Integer + 0..65535 + + + + + Maximum Measured Values + R + Multiple + Optional + Integer + 0..65535 + + + + + Reset Minimum and Maximum Measured Values + E + Single + Optional + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10376.xml b/application/src/main/data/lwm2m-registry/10376.xml new file mode 100644 index 0000000000..faec95398e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10376.xml @@ -0,0 +1,211 @@ + + + + + Periodic Activity + + + + 10376 + urn:oma:lwm2m:x:10376 + 1.0 + 1.0 + Multiple + Optional + + + Activity Name + R + Single + Mandatory + String + + + + + + + Activity Description + R + Single + Mandatory + String + + + + + + + Activity Settings + RW + Single + Optional + Opaque + + + + + + + Start Mask + RW + Single + Mandatory + String + + + + + + Periodic Interval + RW + Single + Mandatory + Integer + + s + + + + + Run Period + RW + Single + Optional + Integer + + s + + + + + Record + R + Single + Optional + Opaque + + + + + + Record Head Index + R + Single + Optional + Integer + + + + + + + Record Tail Index + R + Single + Optional + Integer + + + + + + + Record Read Index + RW + Single + Optional + Integer + + + + + + + Record Dispatch Start Mask + RW + Single + Optional + String + + + + + + Record Dispatch Interval + RW + Single + Optional + Integer + + s + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10377.xml b/application/src/main/data/lwm2m-registry/10377.xml new file mode 100644 index 0000000000..f46ace72aa --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10377.xml @@ -0,0 +1,251 @@ + + + + + Data Monitoring + + 10377 + urn:oma:lwm2m:x:10377 + 1.0 + 1.0 + Multiple + Optional + + + Monitoring Name + R + Single + Mandatory + String + + + + + + + Monitoring Description + R + Single + Mandatory + String + + + + + + + Monitoring Settings + RW + Single + Optional + Opaque + + + + + + + Data + R + Single + Mandatory + Opaque + + + + + + + Data Sampling Start Mask + RW + Single + Optional + String + + + + + + Data Sampling Interval + RW + Single + Optional + Integer + + s + + + + Data Sampling Run Period + RW + Single + Optional + Integer + + s + + + + Reference A + RW + Single + Mandatory + Opaque + + + + + + + Comparison A + RW + Single + Mandatory + Integer + + + Reference A +2: Data >= Reference A +3: Data < Reference A +4: Data <= Reference A +5: Data equals Reference A +6: Data not equal Reference A +]]> + + + Reference B + RW + Single + Optional + Opaque + + + + + + + Comparison B + RW + Single + Optional + Integer + + + Reference B +2: Data >= Reference B +3: Data < Reference B +4: Data <= Reference B +5: Data equals Reference B +6: Data not equal Reference B +]]> + + + Reference C + RW + Single + Optional + Opaque + + + + + + + Comparison C + RW + Single + Optional + Integer + + + Reference C +2: Data >= Reference C +3: Data < Reference C +4: Data <= Reference C +5: Data equals Reference C +6: Data not equal Reference C +]]> + + + Monitoring Results + RW + Single + Mandatory + Opaque + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/10378.xml b/application/src/main/data/lwm2m-registry/10378.xml new file mode 100644 index 0000000000..bbd45811d0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/10378.xml @@ -0,0 +1,91 @@ + + + + + Edge Application Server Configuration + + 10378 + urn:oma:lwm2m:x:10378 + 1.0 + 1.0 + Multiple + Optional + + + Application Client ID + RW + Single + Mandatory + String + + + + + + EAS ID + RW + Multiple + Mandatory + String + + + + + + EAS Endpoint + RW + Multiple + Mandatory + String + + + + + + Type + RW + Multiple + Optional + Integer + 1..2 + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/11.xml b/application/src/main/data/lwm2m-registry/11.xml new file mode 100644 index 0000000000..438b2c3df2 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/11.xml @@ -0,0 +1,408 @@ + + + + + LWM2M APN Connection Profile + + 11 + urn:oma:lwm2m:oma:11:1.1 + 1.1 + 1.1 + Multiple + Optional + + Profile name + RW + Single + Mandatory + String + + + + + APN + RW + Single + Optional + String + + + + + Auto select APN by device + RW + Single + Optional + Boolean + + + + + Enable status + RW + Single + Optional + Boolean + + + + + Authentication Type + RW + Single + Mandatory + Integer + + + + + User Name + RW + Single + Optional + String + + + + + Secret + RW + Single + Optional + String + + + + + Reconnect Schedule + RW + Single + Optional + String + + + + + Validity (MCC, MNC) + RW + Multiple + Optional + String + + + + + Connection establishment time (1) + R + Multiple + Optional + Time + + + + + Connection establishment result (1) + R + Multiple + Optional + Integer + + + + + + Connection establishment reject cause (1) + R + Multiple + Optional + Integer + 0..111 + + + + Connection end time (1) + R + Multiple + Optional + Time + + + + + TotalBytesSent + R + Single + Optional + Integer + + + + + TotalBytesReceived + R + Single + Optional + Integer + + + + + IP address (2) + RW + Multiple + Optional + String + + + + + Prefix length(2) + RW + Multiple + Optional + String + + + + + Subnet mask (2) + RW + Multiple + Optional + String + + + + + Gateway (2) + RW + Multiple + Optional + String + + + + + Primary DNS address (2) + RW + Multiple + Optional + String + + + + + Secondary DNS address (2) + RW + Multiple + Optional + String + + + + + QCI (3) + R + Single + Optional + Integer + 1..9 + + + + Vendor specific extensions + R + Single + Optional + Objlnk + + + + + TotalPacketsSent + R + Single + Optional + Integer + + + + + PDN Type + RW + Single + Optional + Integer + + + + + + APN Rate Control + R + Single + Optional + Integer + + + + + + Serving PLMN Rate control + RW + Single + Optional + Unsigned Integer + + + + + Uplink Time Unit + RW + Single + Optional + Unsigned Integer + 0..4 + + + + APN Rate Control for Exception Data + R + Single + Optional + Integer + + + + + + APN Exception Data Uplink Time Unit + RW + Single + Optional + Unsigned Integer + 0..4 + + + + Supported RAT Types + RW + Multiple + Optional + Unsigned Integer + + + + + RDS Application ID + RW + Multiple + Optional + String + + + + + RDS Destination Port + RW + Multiple + Optional + Integer + 0..15 + + + + RDS Source Port + RW + Multiple + Optional + Integer + 0..15 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/12.xml b/application/src/main/data/lwm2m-registry/12.xml new file mode 100644 index 0000000000..d4aad43dca --- /dev/null +++ b/application/src/main/data/lwm2m-registry/12.xml @@ -0,0 +1,551 @@ + + + + + + + WLAN connectivity + + 12 + urn:oma:lwm2m:oma:12:1.1 + 1.0 + 1.1 + Multiple + Optional + + Interface name + RW + Single + Mandatory + String + + + + + Enable + RW + Single + Mandatory + Boolean + + + + + Radio Enabled + RW + Single + Optional + Integer + + + + + Status + R + Single + Mandatory + Integer + + + + + BSSID + R + Single + Mandatory + String + 12 + + + + SSID + RW + Single + Mandatory + String + 1..32 + + + + Broadcast SSID + RW + Single + Optional + Boolean + + + + + Beacon Enabled + RW + Single + Optional + Boolean + + + + + Mode + RW + Single + Mandatory + Integer + + + + + Channel + RW + Single + Mandatory + Integer + 0..255 + + + + Auto Channel + RW + Single + Optional + Boolean + + + + + Supported Channels + RW + Multiple + Optional + Integer + + + + + Channels In Use + RW + Multiple + Optional + Integer + + + + + Regulatory Domain + RW + Single + Optional + String + 3 + + + + Standard + RW + Single + Mandatory + Integer + + + + + Authentication Mode + RW + Single + Mandatory + Integer + + + + + Encryption Mode + RW + Single + Optional + Integer + + + + + WPA Pre Shared Key + W + Single + Optional + String + 64 + + + + WPA Key Phrase + W + Single + Optional + String + 1..64 + + + + WEP Encryption Type + RW + Single + Optional + Integer + + + + + WEP Key Index + RW + Single + Optional + Integer + 1..4 + + + + WEP Key Phrase + W + Single + Optional + String + 1..64 + + + + WEP Key 1 + W + Single + Optional + String + 10,26 + + + + WEP Key 2 + W + Single + Optional + String + 10,26 + + + + WEP Key 3 + W + Single + Optional + String + 10,26 + + + + WEP Key 4 + W + Single + Optional + String + 10,26 + + + + RADIUS Server + RW + Single + Optional + String + 1..256 + + + + RADIUS Server Port + RW + Single + Optional + Integer + + + + + RADIUS Secret + W + Single + Optional + String + 1..256 + + + + WMM Supported + R + Single + Optional + Boolean + + + + + WMM Enabled + RW + Single + Optional + Boolean + + + + + MAC Control Enabled + RW + Single + Optional + Boolean + + + + + MAC Address List + RW + Multiple + Optional + String + 12 + + + + Total Bytes Sent + R + Single + Optional + Integer + + + + + Total Bytes Received + R + Single + Optional + Integer + + + + + Total Packets Sent + R + Single + Optional + Integer + + + + + Total Packets Received + R + Single + Optional + Integer + + + + + Transmit Errors + R + Single + Optional + Integer + + + + + Receive Errors + R + Single + Optional + Integer + + + + + Unicast Packets Sent + R + Single + Optional + Integer + + + + + Unicast Packets Received + R + Single + Optional + Integer + + + + + Multicast Packets Sent + R + Single + Optional + Integer + + + + + Multicast Packets Received + R + Single + Optional + Integer + + + + + Broadcast Packets Sent + R + Single + Optional + Integer + + + + + Broadcast Packets Received + R + Single + Optional + Integer + + + + + Discard Packets Sent + R + Single + Optional + Integer + + + + + Discard Packets Received + R + Single + Optional + Integer + + + + + Unknown Packets Received + R + Single + Optional + Integer + + + + + Vendor specific extensions + R + Single + Optional + Objlnk + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/13.xml b/application/src/main/data/lwm2m-registry/13.xml new file mode 100644 index 0000000000..9b30ecd225 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/13.xml @@ -0,0 +1,209 @@ + + + + + LWM2M Bearer Selection + + 13 + urn:oma:lwm2m:oma:13:1.1 + 1.1 + 1.1 + Single + Optional + + Preferred Communications Bearer + RW + Multiple + Optional + Integer + 0..100 + + + + Acceptable RSSI (GSM) + RW + Single + Optional + Integer + -110..-48 + + + + Acceptable RSCP (UMTS) + RW + Single + Optional + Integer + -120..-25 + + + + Acceptable RSRP (LTE) + RW + Single + Optional + Integer + -140..-44 + + + + Acceptable RSSI (1xEV-DO) + RW + Single + Optional + Integer + + + + + Cell lock list + RW + Single + Optional + String + + + + + Operator list + RW + Single + Optional + String + + + + + Operator list mode + RW + Single + Optional + Boolean + + + + + List of available PLMNs + R + Single + Optional + String + + + + + Vendor specific extensions + R + Single + Optional + Objlnk + + + + + Acceptable RSRP (NB-IoT) + RW + Single + Optional + Integer + -158..-44 + + + + Higher Priority PLMN Search Timer + RW + Single + Optional + Integer + + + + + Attach without PDN connection + RW + Single + Optional + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/14.xml b/application/src/main/data/lwm2m-registry/14.xml new file mode 100644 index 0000000000..a1d7f7239f --- /dev/null +++ b/application/src/main/data/lwm2m-registry/14.xml @@ -0,0 +1,139 @@ + + + + + + + LWM2M Software Component + + 14 + urn:oma:lwm2m:oma:14 + 1.0 + 1.0 + Multiple + Optional + + + Component Identity + R + Single + Optional + String + 0..255 bytes + + + + + Component Pack + R + Single + Optional + Opaque + + + + + + Component Version + R + Single + Optional + String + 0..255 bytes + + + + + Activate + E + Single + Optional + + + + + + + Deactivate + E + Single + Optional + + + + + + + Activation State + R + Single + Optional + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/15.xml b/application/src/main/data/lwm2m-registry/15.xml new file mode 100644 index 0000000000..f4363e3f33 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/15.xml @@ -0,0 +1,175 @@ + + + + + + + DevCapMgmt + + 15 + urn:oma:lwm2m:oma:15:2.0 + 1.0 + 2.0 + Multiple + Optional + + Property + R + Single + Mandatory + String + + + + + Group + R + Single + Mandatory + Integer + 0..15 + + + + Description + R + Single + Optional + String + + + + + Attached + R + Single + Optional + Boolean + + + + + Enabled + R + Single + Mandatory + Boolean + + + + + opEnable + E + Single + Mandatory + + + + + + opDisable + E + Single + Mandatory + + + + + + NotifyEn + RW + Single + Optional + Boolean + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/16.xml b/application/src/main/data/lwm2m-registry/16.xml new file mode 100644 index 0000000000..f8f2dc40df --- /dev/null +++ b/application/src/main/data/lwm2m-registry/16.xml @@ -0,0 +1,127 @@ + + + + + + + Portfolio + + 16 + urn:oma:lwm2m:oma:161.0 + 1.0Multiple + Optional + + Identity + RW + Multiple + Mandatory + String + + + + + GetAuthData + E + Single + Optional + + + + + + AuthData + R + Multiple + Optional + Opaque + + + + + AuthStatus + R + Single + Optional + Integer + 0..2 + + + + + + diff --git a/application/src/main/data/lwm2m-registry/18830.xml b/application/src/main/data/lwm2m-registry/18830.xml new file mode 100644 index 0000000000..87e0747d5c --- /dev/null +++ b/application/src/main/data/lwm2m-registry/18830.xml @@ -0,0 +1,173 @@ + + + + MQTT Broker + + 18830 + urn:oma:lwm2m:x:18830 + 1.1 + 1.0 + Multiple + Optional + + + URI + RW + Single + Mandatory + String + + + + + + Client Identifier + RW + Single + Mandatory + String + + + + + + Clean Session + RW + Single + Mandatory + Boolean + + + + + + Keep Alive + RW + Single + Mandatory + Unsigned Integer + 0..65535 + s + + + + User Name + RW + Single + Optional + String + + + + + + Password + RW + Single + Optional + Opaque + 0..65535 + + + + + Security Mode + RW + Single + Mandatory + Integer + 0..4 + + + + + Public Key or Identity + RW + Single + Optional + Opaque + + + + + + MQTT Broker Public Key + RW + Single + Optional + Opaque + + + + + + Secret Key + W + Single + Optional + Opaque + + + + + + Certificate Usage + RW + Single + Optional + Integer + 0..3 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/18831.xml b/application/src/main/data/lwm2m-registry/18831.xml new file mode 100644 index 0000000000..68dc873ed0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/18831.xml @@ -0,0 +1,120 @@ + + + + MQTT Publication + This LwM2M Object is used to configure data reporting over MQTT. + 18831 + urn:oma:lwm2m:x:18831 + 1.1 + 1.0 + Multiple + Optional + + + Source + RW + Single + Mandatory + Corelnk + + + , or ; ). If this Resource is empty, the published data are implementation dependent.]]> + + + Broker + RW + Single + Mandatory + Objlnk + + + + + + Topic + RW + Single + Mandatory + String + + + + + + QoS + RW + Single + Mandatory + Unsigned Integer + 0..2 + + + + + Retain + RW + Single + Mandatory + Boolean + + + + + + Active + RW + Single + Optional + Boolean + + + + + + Encoding + RW + Single + Optional + Unsigned Integer + 0..65535 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/19.xml b/application/src/main/data/lwm2m-registry/19.xml new file mode 100644 index 0000000000..c4f5cbe6ab --- /dev/null +++ b/application/src/main/data/lwm2m-registry/19.xml @@ -0,0 +1,144 @@ + + + + + BinaryAppDataContainer + + 19 + urn:oma:lwm2m:oma:19 + 1.0 + 1.0 + Multiple + Optional + + Data + RW + Multiple + Mandatory + Opaque + + + + + Data Priority + RW + Single + Optional + Integer + 1 bytes + + + + Data Creation Time + RW + Single + Optional + Time + + + + + Data Description + RW + Single + Optional + String + 32 bytes + + + + Data Format + RW + Single + Optional + String + 32 bytes + + + + App ID + RW + Single + Optional + Integer + 2 bytes + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2.xml b/application/src/main/data/lwm2m-registry/2.xml new file mode 100644 index 0000000000..4ea5805b36 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2.xml @@ -0,0 +1,123 @@ + + + + + + + LwM2M Access Control + + 2 + urn:oma:lwm2m:oma:2:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Object ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Object Instance ID + R + Single + Mandatory + Integer + 0..65535 + + + + + ACL + RW + Multiple + Optional + Integer + 0..31 + + + + + Access Control Owner + RW + Single + Mandatory + Integer + 0..65535 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/20.xml b/application/src/main/data/lwm2m-registry/20.xml new file mode 100644 index 0000000000..68aa96048c --- /dev/null +++ b/application/src/main/data/lwm2m-registry/20.xml @@ -0,0 +1,142 @@ + + + + + + + + Event Log + + 20 + urn:oma:lwm2m:oma:20:2.0 + 1.0 + 2.0 + Multiple + Optional + + LogClass + RW + Single + Optional + Integer + 255 + + + + LogStart + E + Single + Optional + + + + + + LogStop + E + Single + Optional + + + + + + LogStatus + R + Single + Optional + Integer + 8-Bits + + + + LogData + R + Single + Mandatory + Opaque + + + + + LogDataFormat + RW + Single + Optional + Integer + 255 + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2048.xml b/application/src/main/data/lwm2m-registry/2048.xml new file mode 100644 index 0000000000..0bc99cffd1 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2048.xml @@ -0,0 +1,90 @@ + + + + + + + CmdhPolicy + + 2048 + urn:oma:lwm2m:ext:20481.0 + 1.0Multiple + Optional + + Name + RW + Single + Mandatory + String + + + + + DefaultRule + RW + Single + Mandatory + Objlnk + + + + + LimiRules + RW + Multiple + Mandatory + Objlnk + + + + + NetworkAccessECRules + RW + Multiple + Mandatory + Objlnk + + + + + BufferRules + RW + Multiple + Mandatory + Objlnk + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2049.xml b/application/src/main/data/lwm2m-registry/2049.xml new file mode 100644 index 0000000000..7509b22160 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2049.xml @@ -0,0 +1,59 @@ + + + + + + + ActiveCmdhPolicy + + 2049 + urn:oma:lwm2m:ext:20491.0 + 1.0Single + Optional + + ActiveLink + RW + Single + Mandatory + Objlnk + + + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2050.xml b/application/src/main/data/lwm2m-registry/2050.xml new file mode 100644 index 0000000000..da27a90de0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2050.xml @@ -0,0 +1,64 @@ + + + + + + + CmdhDefaults + + 2050 + urn:oma:lwm2m:ext:20501.0 + 1.0Multiple + Optional + + DefaultEcRules + RW + Multiple + Mandatory + Objlnk + + + + + + DefaultEcParamRules + RW + Multiple + Mandatory + Objlnk + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2051.xml b/application/src/main/data/lwm2m-registry/2051.xml new file mode 100644 index 0000000000..1f4ecda5d1 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2051.xml @@ -0,0 +1,99 @@ + + + + + + + CmdhDefEcValues + + 2051 + urn:oma:lwm2m:ext:20511.0 + 1.0Multiple + Optional + + Order + RW + Single + Mandatory + Integer + + + + + DefEcValue + RW + Single + Mandatory + String + + + + + RequestOrigin + RW + Multiple + Mandatory + String + + + + + RequestContext + RW + Single + Optional + String + + + + + RequestContextNotification + RW + Single + Optional + Boolean + + + + + RequestCharacteristics + RW + Single + Optional + String + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2052.xml b/application/src/main/data/lwm2m-registry/2052.xml new file mode 100644 index 0000000000..abaae1b54e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2052.xml @@ -0,0 +1,111 @@ + + + + + + + CmdhEcDefParamValues + + 2052 + urn:oma:lwm2m:ext:20521.0 + 1.0Multiple + Optional + + ApplicableEventCategory + RW + Multiple + Mandatory + Integer + + + + + DefaultRequestExpTime + RW + Single + Mandatory + Integer + + ms + + + + + + + + + DefaultResultExpTime + RW + Single + Mandatory + Integer + + ms + + + + DefaultOpExecTime + RW + Single + Mandatory + Integer + + ms + + + DefaultRespPersistence + RW + Single + Mandatory + Integer + + ms + + + DefaultDelAggregation + RW + Single + Mandatory + Integer + + ms + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2053.xml b/application/src/main/data/lwm2m-registry/2053.xml new file mode 100644 index 0000000000..a765a7894a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2053.xml @@ -0,0 +1,165 @@ + + + + + + + CmdhLimits + + 2053 + urn:oma:lwm2m:ext:20531.0 + 1.0Multiple + Optional + + Order + RW + Single + Mandatory + Integer + + + + + RequestOrigin + RW + Multiple + Mandatory + String + + + + + + + + + + + RequestContext + RW + Single + Optional + String + + + + + + RequestContextNotificatio + RW + Single + Optional + Boolean + + + + + RequestCharacteristics + RW + Single + Optional + String + + + + + LimitsEventCategory + RW + Multiple + Mandatory + Integer + + + + + LimitsRequestExpTime + RW + Multiple + Mandatory + Integer + 2 Instances + ms + + + LimitsResultExpTime + RW + Multiple + Mandatory + Integer + 2 Instances + ms + + + LimitsOptExpTime + RW + Multiple + Mandatory + Integer + 2 Instances + ms + + + LimitsRespPersistence + RW + Multiple + Mandatory + Integer + 2 Instances + ms + + + LimitsDelAggregation + RW + Multiple + Mandatory + String + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2054.xml b/application/src/main/data/lwm2m-registry/2054.xml new file mode 100644 index 0000000000..3935fb7253 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2054.xml @@ -0,0 +1,69 @@ + + + + + + + CmdhNetworkAccessRules + + 2054 + urn:oma:lwm2m:ext:20541.0 + 1.0Multiple + Optional + + ApplicableEventCategories + RW + Multiple + Mandatory + Integer + + + + + NetworkAccessRule + RW + Multiple + Optional + Objlnk + + + + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2055.xml b/application/src/main/data/lwm2m-registry/2055.xml new file mode 100644 index 0000000000..cbd0dc5796 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2055.xml @@ -0,0 +1,100 @@ + + + + + + + CmdhNwAccessRule + + 2055 + urn:oma:lwm2m:ext:20551.0 + 1.0Multiple + Optional + + TargetNetwork + RW + Multiple + Mandatory + String + + + + + SpreadingWaitTime + RW + Single + Mandatory + Integer + + ms + + + MinReqVolume + RW + Single + Mandatory + Integer + + B + + + BackOffParameters + RW + Single + Mandatory + Objlnk + + + + + OtherConditions + RW + Single + Mandatory + String + + + + + AllowedSchedule + RW + Multiple + Mandatory + String + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2056.xml b/application/src/main/data/lwm2m-registry/2056.xml new file mode 100644 index 0000000000..ddd50e269a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2056.xml @@ -0,0 +1,77 @@ + + + + + + + CmdhBuffer + + 2056 + urn:oma:lwm2m:ext:20561.0 + 1.0Multiple + Optional + + ApplicableEventCategory + RW + Multiple + Mandatory + Integer + + + + + MaxBufferSize + RW + Single + Mandatory + Integer + + B + + + StoragePriority + RW + Single + Mandatory + Integer + 1..10 + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/2057.xml b/application/src/main/data/lwm2m-registry/2057.xml new file mode 100644 index 0000000000..68d4bcd135 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/2057.xml @@ -0,0 +1,92 @@ + + + + + + + CmdhBackOffParametersSet + + 2057 + urn:oma:lwm2m:ext:2057 + 1.0 + 1.0 + Multiple + Optional + + NetworkAction + RW + Single + Optional + Integer + 1..5 + + + + InitialBackoffTime + RW + Single + Mandatory + Integer + + ms + + + AdditionalBackoffTime + RW + Single + Mandatory + Integer + + ms + + + MaximumBackoffTime + RW + Single + Mandatory + Integer + + ms + + + OptionalRandomBackoffTime + RW + Multiple + Optional + Integer + + ms + + + + + diff --git a/application/src/main/data/lwm2m-registry/21.xml b/application/src/main/data/lwm2m-registry/21.xml new file mode 100644 index 0000000000..5685648bad --- /dev/null +++ b/application/src/main/data/lwm2m-registry/21.xml @@ -0,0 +1,132 @@ + + + + + + + LWM2M OSCORE + + 21 + urn:oma:lwm2m:oma:21:2.0 + 1.1 + 2.0 + Multiple + Optional + + OSCORE Master Secret + + Single + Mandatory + Opaque + + + + + OSCORE Sender ID + + Single + Mandatory + Opaque + + + + + OSCORE Recipient ID + + Single + Mandatory + Opaque + + + + + OSCORE AEAD Algorithm + + Single + Optional + Integer + + + + + OSCORE HMAC Algorithm + + Single + Optional + Integer + + + + + OSCORE Master Salt + + Single + Optional + Opaque + + + + + OSCORE ID Context + + Single + Optional + Opaque + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/22.xml b/application/src/main/data/lwm2m-registry/22.xml new file mode 100644 index 0000000000..02fed409ae --- /dev/null +++ b/application/src/main/data/lwm2m-registry/22.xml @@ -0,0 +1,157 @@ + + + + + + + Virtual Observe Notify + + 22 + urn:oma:lwm2m:oma:22:1.1 + 1.1 + 1.1 + Multiple + Optional + + + ObserveLinks + RW + Multiple + Mandatory + Corelnk + + + + + + Report + R + Single + Mandatory + String + + + + + + ResourceFilter + RW + Single + Optional + Boolean + + + + + + ReportLinks + RW + Multiple + Optional + Corelnk + + + + + + ObserveRelation + RW + Single + Optional + Integer + + + + + + Virtual Observe Report Format + RW + Single + Optional + Integer + + + + + + Virtual Observe Report + R + Single + Optional + Opaque + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/23.xml b/application/src/main/data/lwm2m-registry/23.xml new file mode 100644 index 0000000000..55db80cc77 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/23.xml @@ -0,0 +1,100 @@ + + + + + + + LwM2M COSE + + 23 + urn:oma:lwm2m:oma:23 + 1.2 + 1.0 + Multiple + Optional + + KID + + Single + Mandatory + String + + + + + AEAD Algorithm + + Single + Mandatory + Integer + + + + + Key + + Single + Mandatory + String + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/24.xml b/application/src/main/data/lwm2m-registry/24.xml new file mode 100644 index 0000000000..7dd6942646 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/24.xml @@ -0,0 +1,163 @@ + + + + + + MQTT Server + + 24 + urn:oma:lwm2m:oma:24 + 1.2 + 1.0 + Multiple + Optional + + + Will Retain + RW + Single + Optional + Boolean + + + + + + Will Topic + RW + Single + Optional + String + + + + + + Will Message + RW + Single + Optional + String + + + + + + Clean Session + RW + Single + Optional + Boolean + + + + + + Will QoS + RW + Single + Optional + Unsigned Integer + 0..3 + + + + + Keep Alive + RW + Single + Optional + Unsigned Integer + 0..65535 + + + + + Client Identifier + RW + Single + Mandatory + String + + + + + + User Name + RW + Single + Optional + String + + + + + + Password + RW + Single + Optional + Opaque + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/25.xml b/application/src/main/data/lwm2m-registry/25.xml new file mode 100644 index 0000000000..5ab62b3c36 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/25.xml @@ -0,0 +1,104 @@ + + + + + + LwM2M Gateway + + 25 + urn:oma:lwm2m:oma:25:2.0 + 1.1 + 2.0 + Multiple + Optional + + + Device ID + R + Single + Mandatory + String + + + + + + Prefix + R + Single + Mandatory + String + + + + + + IoT Device Objects + R + Single + Mandatory + Corelnk + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/26.xml b/application/src/main/data/lwm2m-registry/26.xml new file mode 100644 index 0000000000..0f77aad973 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/26.xml @@ -0,0 +1,93 @@ + + + + + + LwM2M Gateway Routing + + 26 + urn:oma:lwm2m:oma:26 + 1.2 + 1.0 + Multiple + Optional + + + LwM2M Object ID + R + Single + Mandatory + Unsigned Integer + + + + + + Mapping Info + R + Single + Mandatory + Corelnk + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/27.xml b/application/src/main/data/lwm2m-registry/27.xml new file mode 100644 index 0000000000..9f30947997 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/27.xml @@ -0,0 +1,322 @@ + + + + + + 5GNR Connectivity + + 27 + urn:oma:lwm2m:oma:27 + 1.2 + 1.0 + Multiple + Optional + + + Connectivity Option + R + Single + Mandatory + Integer + + + + + + NR Band Support available + R + Multiple + Optional + Integer + + + + + + NR Band attached + R + Multiple + Optional + Integer + + + + + + S-NSSAI + R + Multiple + Optional + Integer + + + + + + DNN Name + R + Single + Optional + String + + + + + + PDU Session Id + R + Single + Optional + Integer + 1...15 + + + + + SSC Mode + R + Single + Optional + Integer + 1...3 + + + + + PDU Session Type + R + Single + Optional + Integer + + + + + + 5QI + R + Single + Optional + Integer + + + + + + SDAP Enablement + R + Single + Optional + Integer + + + + + + QFI + R + Single + Optional + Integer + 1...63 + + + + + Session AMBR + R + Single + Optional + Integer + 1...25 + + + + + APN-AMBR + R + Single + Optional + Integer + + dB + + + + NAS Reflective QOS + R + Single + Optional + Boolean + + + + + + Access Stratum Reflective QoS + R + Single + Optional + Boolean + + + + + + P-CSCF Address Index + R + Single + Optional + Integer + + + + + + PDU session Authentication + R + Single + Optional + Integer + + + + + + PLMN ID + R + Single + Optional + String + + + + + + LADN Support + R + Single + Optional + Boolean + + + + + + Integrity Protection on DRB + R + Single + Optional + Integer + + + + + + Access type Preference + R + Single + Optional + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/28.xml b/application/src/main/data/lwm2m-registry/28.xml new file mode 100644 index 0000000000..8edd51bcc5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/28.xml @@ -0,0 +1,150 @@ + + + + + + + Device RF Capabilities + + 28 + urn:oma:lwm2m:oma:28 + 1.1 + 1.0 + Single + Optional + + + Supported LTE Band Information + R + Single + Optional + Integer + + + + + + Supported EGPRS Band Information + R + Single + Optional + Integer + + + + + + Supported TD-SCDMA Band Information + R + Single + Optional + Integer + + + + + + Supported WCDMA Band Information + R + Single + Optional + Integer + + + + + + Supported CDMA-2000 Band Information + R + Single + Optional + Integer + + + + + + Supported WiMAX Band Information + R + Single + Optional + Integer + + + + + + Supported NB-IoT Band Information + R + Single + Optional + Integer + + + + + + Supported 5G Band Information + R + Single + Optional + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3.xml b/application/src/main/data/lwm2m-registry/3.xml new file mode 100644 index 0000000000..cb360acd10 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3.xml @@ -0,0 +1,331 @@ + + + + + + + Device + + 3 + urn:oma:lwm2m:oma:3:1.2 + 1.1 + 1.2 + Single + Mandatory + + + Manufacturer + R + Single + Optional + String + + + + + + Model Number + R + Single + Optional + String + + + + + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0..7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + + + + + Power Source Current + R + Multiple + Optional + Integer + + + + + + Battery Level + R + Single + Optional + Integer + 0..100 + /100 + + + + Memory Free + R + Single + Optional + Integer + + + + + + Error Code + R + Multiple + Mandatory + Integer + 0..32 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0..6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3200.xml b/application/src/main/data/lwm2m-registry/3200.xml new file mode 100644 index 0000000000..6a6310b5bb --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3200.xml @@ -0,0 +1,148 @@ + + + + + + + Digital Input + Generic digital input for non-specific sensors + 3200 + urn:oma:lwm2m:ext:3200:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Digital Input State + R + Single + Mandatory + Boolean + + + The current state of a digital input. + + + Digital Input Counter + R + Single + Optional + Integer + + + The cumulative value of active state detected. + + + Digital Input Polarity + RW + Single + Optional + Boolean + + + The polarity of the digital input as a Boolean (False = Normal, True = Reversed). + + + Digital Input Debounce + RW + Single + Optional + Integer + + ms + The debounce period in ms. + + + Digital Input Edge Selection + RW + Single + Optional + Integer + 1..3 + + The edge selection as an integer (1 = Falling edge, 2 = Rising edge, 3 = Both Rising and Falling edge). + + + Digital Input Counter Reset + E + Single + Optional + + + + Reset the Counter value. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Sensor Type + R + Single + Optional + String + + + The type of the sensor (for instance PIR type). + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3201.xml b/application/src/main/data/lwm2m-registry/3201.xml new file mode 100644 index 0000000000..b950077387 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3201.xml @@ -0,0 +1,98 @@ + + + + + + + Digital Output + Generic digital output for non-specific actuators + 3201 + urn:oma:lwm2m:ext:3201:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Digital Output State + RW + Single + Mandatory + Boolean + + + The current state of a digital output. + + + Digital Output Polarity + RW + Single + Optional + Boolean + + + The polarity of the digital output as a Boolean (False = Normal, True = Reversed). + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3202.xml b/application/src/main/data/lwm2m-registry/3202.xml new file mode 100644 index 0000000000..7814f6ccf9 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3202.xml @@ -0,0 +1,168 @@ + + + + + + + Analog Input + Generic analog input for non-specific sensors + 3202 + urn:oma:lwm2m:ext:3202:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Analog Input Current Value + R + Single + Mandatory + Float + + + The current value of the analog input. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Sensor Type + R + Single + Optional + String + + + The type of the sensor (for instance PIR type). + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3203.xml b/application/src/main/data/lwm2m-registry/3203.xml new file mode 100644 index 0000000000..0316e920a5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3203.xml @@ -0,0 +1,108 @@ + + + + + + + Analog Output + This IPSO object is a generic object that can be used with any kind of analog output interface. + 3203 + urn:oma:lwm2m:ext:3203:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Analog Output Current Value + RW + Single + Mandatory + Float + 0..1 + + The current value of the analog output. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3300.xml b/application/src/main/data/lwm2m-registry/3300.xml new file mode 100644 index 0000000000..f7a5b8a3e2 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3300.xml @@ -0,0 +1,178 @@ + + + + + + + Generic Sensor + This IPSO object allows the description of a generic sensor. It is based on the description of a value and a unit according to the SenML specification. Thus, any type of value defined within this specification can be reported using this object. This object may be used as a generic object if a dedicated one does not exist. + 3300 + urn:oma:lwm2m:ext:3300:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Sensor Type + R + Single + Optional + String + + + The type of the sensor (for instance PIR type). + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3301.xml b/application/src/main/data/lwm2m-registry/3301.xml new file mode 100644 index 0000000000..6604464f20 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3301.xml @@ -0,0 +1,168 @@ + + + + + + + Illuminance + Illuminance sensor, example units = lx + 3301 + urn:oma:lwm2m:ext:3301:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3302.xml b/application/src/main/data/lwm2m-registry/3302.xml new file mode 100644 index 0000000000..fe9e886408 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3302.xml @@ -0,0 +1,158 @@ + + + + + + + Presence + Presence sensor with digital sensing, optional delay parameters + 3302 + urn:oma:lwm2m:ext:3302:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Digital Input State + R + Single + Mandatory + Boolean + + + The current state of a digital input. + + + Digital Input Counter + R + Single + Optional + Integer + + + The cumulative value of active state detected. + + + Digital Input Counter Reset + E + Single + Optional + + + + Reset the Counter value. + + + Sensor Type + R + Single + Optional + String + + + The type of the sensor (for instance PIR type). + + + Busy to Clear delay + RW + Single + Optional + Integer + + ms + Delay from the detection state to the clear state in ms. + + + Clear to Busy delay + RW + Single + Optional + Integer + + ms + Delay from the clear state to the busy state in ms. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3303.xml b/application/src/main/data/lwm2m-registry/3303.xml new file mode 100644 index 0000000000..2db5da74c8 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3303.xml @@ -0,0 +1,168 @@ + + + + + + + Temperature + This IPSO object should be used with a temperature sensor to report a temperature measurement. It also provides resources for minimum/maximum measured values and the minimum/maximum range that can be measured by the temperature sensor. An example measurement unit is degrees Celsius. + 3303 + urn:oma:lwm2m:ext:3303:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3304.xml b/application/src/main/data/lwm2m-registry/3304.xml new file mode 100644 index 0000000000..7331c5f83a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3304.xml @@ -0,0 +1,168 @@ + + + + + + + Humidity + This IPSO object should be used with a humidity sensor to report a humidity measurement. It also provides resources for minimum/maximum measured values and the minimum/maximum range that can be measured by the humidity sensor. An example measurement unit is relative humidity as a percentage. + 3304 + urn:oma:lwm2m:ext:3304:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3305.xml b/application/src/main/data/lwm2m-registry/3305.xml new file mode 100644 index 0000000000..f927a44a59 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3305.xml @@ -0,0 +1,278 @@ + + + + + + + Power Measurement + This IPSO object should be used over a power measurement sensor to report a remote power measurement. It also provides resources for minimum/maximum measured values and the minimum/maximum range for both active and reactive power. It also provides resources for cumulative energy, calibration, and the power factor. + 3305 + urn:oma:lwm2m:ext:3305:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Instantaneous active power + R + Single + Mandatory + Float + + W + The current active power. + + + Min Measured active power + R + Single + Optional + Float + + W + The minimum active power measured by the sensor since it is ON. + + + Max Measured active power + R + Single + Optional + Float + + W + The maximum active power measured by the sensor since it is ON. + + + Min Range active power + R + Single + Optional + Float + + W + The minimum active power that can be measured by the sensor. + + + Max Range active power + R + Single + Optional + Float + + W + The maximum active power that can be measured by the sensor. + + + Cumulative active power + R + Single + Optional + Float + + Wh + The cumulative active power since the last cumulative energy reset or device start. + + + Active Power Calibration + W + Single + Optional + Float + + W + Request an active power calibration by writing the value of a calibrated load. + + + Instantaneous reactive power + R + Single + Optional + Float + + var + The current reactive power. + + + Min Measured reactive power + R + Single + Optional + Float + + var + The minimum reactive power measured by the sensor since it is ON. + + + Max Measured reactive power + R + Single + Optional + Float + + var + The maximum reactive power measured by the sensor since it is ON. + + + Min Range reactive power + R + Single + Optional + Float + + var + The minimum active power that can be measured by the sensor. + + + Max Range reactive power + R + Single + Optional + Float + + var + The maximum reactive power that can be measured by the sensor. + + + Cumulative reactive power + R + Single + Optional + Float + + varh + The cumulative reactive power since the last cumulative energy reset or device start. + + + Reactive Power Calibration + W + Single + Optional + Float + + var + Request a reactive power calibration by writing the value of a calibrated load. + + + Power factor + R + Single + Optional + Float + + + If applicable, the power factor of the current consumption. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Reset Cumulative energy + E + Single + Optional + + + + Reset both cumulative active/reactive power. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3306.xml b/application/src/main/data/lwm2m-registry/3306.xml new file mode 100644 index 0000000000..748bc0f005 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3306.xml @@ -0,0 +1,118 @@ + + + + + + + Actuation + This IPSO object is dedicated to remote actuation such as ON/OFF action or dimming. A multi-state output can also be described as a string. This is useful to send pilot wire orders for instance. It also provides a resource to reflect the time that the device has been switched on. + 3306 + urn:oma:lwm2m:ext:3306:1.1 + 1.0 + 1.1 + Multiple + Optional + + + On/Off + RW + Single + Mandatory + Boolean + + + On/off control. Boolean value where True is On and False is Off. + + + Dimmer + RW + Single + Optional + Integer + 0..100 + /100 + This resource represents a dimmer setting, which has an Integer value between 0 and 100 as a percentage. + + + On time + RW + Single + Optional + Integer + + s + The time in seconds that the device has been on. Writing a value of 0 resets the counter. + + + Multi-state Output + RW + Single + Optional + String + + + A string describing a state for multiple level output such as Pilot Wire. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3308.xml b/application/src/main/data/lwm2m-registry/3308.xml new file mode 100644 index 0000000000..f1d5c18f12 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3308.xml @@ -0,0 +1,108 @@ + + + + + + + Set Point + This IPSO object should be used to set a desired value to a controller, such as a thermostat. A special resource is added to set the colour of an object. + 3308 + urn:oma:lwm2m:ext:3308:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Set Point Value + RW + Single + Mandatory + Float + + + The setpoint value. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Colour + RW + Single + Optional + String + + + A string representing a value in some color space. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3310.xml b/application/src/main/data/lwm2m-registry/3310.xml new file mode 100644 index 0000000000..6b4e04d6b6 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3310.xml @@ -0,0 +1,138 @@ + + + + + + + Load Control + This Object is used for demand-response load control and other load control in automation application (not limited to power). + 3310 + urn:oma:lwm2m:ext:3310:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Event Identifier + RW + Single + Mandatory + String + + + The event identifier as a string. + + + Start Time + RW + Single + Mandatory + Time + + + Time when the event started. + + + Duration In Min + RW + Single + Mandatory + Integer + + min + The duration of the event in minutes. + + + Criticality Level + RW + Single + Optional + Integer + 0..3 + + The criticality of the event. The device receiving the event will react in an appropriate fashion for the device. + + + Avg Load AdjPct + RW + Single + Optional + Integer + 0..100 + /100 + Defines the maximum energy usage of the receiving device, as a percentage of the device's normal maximum energy usage. + + + Duty Cycle + RW + Single + Optional + Integer + 0..100 + /100 + Defines the duty cycle for the load control event, i.e, what percentage of time the receiving device is allowed to be on. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3311.xml b/application/src/main/data/lwm2m-registry/3311.xml new file mode 100644 index 0000000000..4c98be2522 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3311.xml @@ -0,0 +1,127 @@ + + + + + + Light Control + This Object is used to control a light source, such as a LED or other light. It allows a light to be turned on or off and its dimmer setting to be control as a % between 0 and 100. An optional colour setting enables a string to be used to indicate the desired colour. + 3311 + urn:oma:lwm2m:ext:3311 + 1.0 + 1.0 + Multiple + Optional + + + On/Off + RW + Single + Mandatory + Boolean + + + On/off control. Boolean value where True is On and False is Off. + + + Dimmer + RW + Single + Optional + Integer + 0..100 + /100 + This resource represents a dimmer setting, which has an Integer value between 0 and 100 as a percentage. + + + On time + RW + Single + Optional + Integer + + s + The time in seconds that the device has been on. Writing a value of 0 resets the counter. + + + Cumulative active power + R + Single + Optional + Float + + Wh + The cumulative active power since the last cumulative energy reset or device start. + + + Power factor + R + Single + Optional + Float + + + If applicable, the power factor of the current consumption. + + + Colour + RW + Single + Optional + String + + + A string representing a value in some color space. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3312.xml b/application/src/main/data/lwm2m-registry/3312.xml new file mode 100644 index 0000000000..17bd0312db --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3312.xml @@ -0,0 +1,127 @@ + + + + + + Power Control + This Object is used to control a power source, such as a Smart Plug. It allows a power relay to be turned on or off and its dimmer setting to be control as a % between 0 and 100. + 3312 + urn:oma:lwm2m:ext:3312:1.1 + 1.0 + 1.1 + Multiple + Optional + + + On/Off + RW + Single + Mandatory + Boolean + + + On/off control. Boolean value where True is On and False is Off. + + + Dimmer + RW + Single + Optional + Integer + 0..100 + /100 + This resource represents a dimmer setting, which has an Integer value between 0 and 100 as a percentage. + + + On time + RW + Single + Optional + Integer + + s + The time in seconds that the device has been on. Writing a value of 0 resets the counter. + + + Cumulative active power + R + Single + Optional + Float + + Wh + The cumulative active power since the last cumulative energy reset or device start. + + + Power factor + R + Single + Optional + Float + + + If applicable, the power factor of the current consumption. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3313.xml b/application/src/main/data/lwm2m-registry/3313.xml new file mode 100644 index 0000000000..43d0d63b88 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3313.xml @@ -0,0 +1,157 @@ + + + + + + Accelerometer + This IPSO object can be used to represent a 1-3 axis accelerometer. + 3313 + urn:oma:lwm2m:ext:3313:1.1 + 1.0 + 1.1 + Multiple + Optional + + + X Value + R + Single + Mandatory + Float + + + The measured value along the X axis. + + + Y Value + R + Single + Optional + Float + + + The measured value along the Y axis. + + + Z Value + R + Single + Optional + Float + + + The measured value along the Z axis. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3314.xml b/application/src/main/data/lwm2m-registry/3314.xml new file mode 100644 index 0000000000..96ebc5a537 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3314.xml @@ -0,0 +1,147 @@ + + + + + + Magnetometer + This IPSO object can be used to represent a 1-3 axis magnetometer with optional compass direction. + 3314 + urn:oma:lwm2m:ext:3314:1.1 + 1.0 + 1.1 + Multiple + Optional + + + X Value + R + Single + Mandatory + Float + + + The measured value along the X axis. + + + Y Value + R + Single + Optional + Float + + + The measured value along the Y axis. + + + Z Value + R + Single + Optional + Float + + + The measured value along the Z axis. + + + Compass Direction + R + Single + Optional + Float + 0..360 + deg + The measured compass direction. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3315.xml b/application/src/main/data/lwm2m-registry/3315.xml new file mode 100644 index 0000000000..d54a09edd8 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3315.xml @@ -0,0 +1,168 @@ + + + + + + + Barometer + This IPSO object should be used with an air pressure sensor to report a barometer measurement. It also provides resources for minimum/maximum measured values and the minimum/maximum range that can be measured by the barometer sensor. An example measurement unit is pascals. + 3315 + urn:oma:lwm2m:ext:3315:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3316.xml b/application/src/main/data/lwm2m-registry/3316.xml new file mode 100644 index 0000000000..16a20a219a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3316.xml @@ -0,0 +1,179 @@ + + + + + + + Voltage + This IPSO object should be used with voltmeter sensor to report measured voltage between two points. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is volts. + + 3316 + urn:oma:lwm2m:ext:3316:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3317.xml b/application/src/main/data/lwm2m-registry/3317.xml new file mode 100644 index 0000000000..9d41da70de --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3317.xml @@ -0,0 +1,179 @@ + + + + + + + Current + This IPSO object should be used with an ammeter to report measured electric current in amperes. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is ampere. + + 3317 + urn:oma:lwm2m:ext:3317:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3318.xml b/application/src/main/data/lwm2m-registry/3318.xml new file mode 100644 index 0000000000..957b593c89 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3318.xml @@ -0,0 +1,179 @@ + + + + + + + Frequency + This IPSO object should be used to report frequency measurements. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is hertz. + + 3318 + urn:oma:lwm2m:ext:3318:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3319.xml b/application/src/main/data/lwm2m-registry/3319.xml new file mode 100644 index 0000000000..e3a13ea3ef --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3319.xml @@ -0,0 +1,179 @@ + + + + + + + Depth + This IPSO object should be used to report depth measurements. It can, for example, be used to describe a generic rain gauge that measures the accumulated rainfall in millimetres (mm). + + 3319 + urn:oma:lwm2m:ext:3319:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3320.xml b/application/src/main/data/lwm2m-registry/3320.xml new file mode 100644 index 0000000000..9ec16fb671 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3320.xml @@ -0,0 +1,179 @@ + + + + + + + Percentage + This IPSO object should can be used to report measurements relative to a 0-100% scale. For example it could be used to measure the level of a liquid in a vessel or container in units of %. + + 3320 + urn:oma:lwm2m:ext:3320:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3321.xml b/application/src/main/data/lwm2m-registry/3321.xml new file mode 100644 index 0000000000..e4a1f80bf1 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3321.xml @@ -0,0 +1,179 @@ + + + + + + + Altitude + This IPSO object should be used with an altitude sensor to report altitude above sea level in meters. Note that Altitude can be calculated from the measured pressure given the local sea level pressure. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is meters. + + 3321 + urn:oma:lwm2m:ext:3321:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3322.xml b/application/src/main/data/lwm2m-registry/3322.xml new file mode 100644 index 0000000000..838ed44995 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3322.xml @@ -0,0 +1,179 @@ + + + + + + + Load + This IPSO object should be used with a load sensor (as in a scale) to report the applied weight or force. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is kilograms. + + 3322 + urn:oma:lwm2m:ext:3322:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3323.xml b/application/src/main/data/lwm2m-registry/3323.xml new file mode 100644 index 0000000000..e65c11c123 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3323.xml @@ -0,0 +1,179 @@ + + + + + + + Pressure + This IPSO object should be used to report pressure measurements. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is pascals. + + 3323 + urn:oma:lwm2m:ext:3323:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3324.xml b/application/src/main/data/lwm2m-registry/3324.xml new file mode 100644 index 0000000000..b80d427b91 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3324.xml @@ -0,0 +1,179 @@ + + + + + + + Loudness + This IPSO object should be used to report loudness or noise level measurements. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is decibels. + + 3324 + urn:oma:lwm2m:ext:3324:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3325.xml b/application/src/main/data/lwm2m-registry/3325.xml new file mode 100644 index 0000000000..c0577d64cd --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3325.xml @@ -0,0 +1,179 @@ + + + + + + + Concentration + This IPSO object should be used to the particle concentration measurement of a medium. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is parts per million. + + 3325 + urn:oma:lwm2m:ext:3325:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3326.xml b/application/src/main/data/lwm2m-registry/3326.xml new file mode 100644 index 0000000000..e247637bcf --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3326.xml @@ -0,0 +1,179 @@ + + + + + + + Acidity + This IPSO object should be used to report an acidity measurement of a liquid. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is pH. + + 3326 + urn:oma:lwm2m:ext:3326:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3327.xml b/application/src/main/data/lwm2m-registry/3327.xml new file mode 100644 index 0000000000..9cc689459f --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3327.xml @@ -0,0 +1,179 @@ + + + + + + + Conductivity + This IPSO object should be used to report a measurement of the electric conductivity of a medium or sample. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is Siemens. + + 3327 + urn:oma:lwm2m:ext:3327:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3328.xml b/application/src/main/data/lwm2m-registry/3328.xml new file mode 100644 index 0000000000..752ebe0c53 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3328.xml @@ -0,0 +1,179 @@ + + + + + + + Power + This IPSO object should be used to report power measurements. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is Watts. This object may be used for either real power or apparent power measurements. + + 3328 + urn:oma:lwm2m:ext:3328:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3329.xml b/application/src/main/data/lwm2m-registry/3329.xml new file mode 100644 index 0000000000..fe221a1564 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3329.xml @@ -0,0 +1,179 @@ + + + + + + + Power Factor + This IPSO object should be used to report a measurement or calculation of the power factor of a reactive electrical load. Power Factor is normally the ratio of non-reactive power to total power. This object also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. + + 3329 + urn:oma:lwm2m:ext:3329:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3330.xml b/application/src/main/data/lwm2m-registry/3330.xml new file mode 100644 index 0000000000..baec2dab14 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3330.xml @@ -0,0 +1,179 @@ + + + + + + + Distance + This IPSO object should be used to report a distance measurement. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is Meters. + + 3330 + urn:oma:lwm2m:ext:3330:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3331.xml b/application/src/main/data/lwm2m-registry/3331.xml new file mode 100644 index 0000000000..1094d5bf31 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3331.xml @@ -0,0 +1,129 @@ + + + + + + + Energy + This IPSO object should be used to report energy consumption (Cumulative Power) of an electrical load. An example measurement unit is Watt Hours. + + 3331 + urn:oma:lwm2m:ext:3331:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Reset Cumulative energy + E + Single + Optional + + + + Reset both cumulative active/reactive power. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3332.xml b/application/src/main/data/lwm2m-registry/3332.xml new file mode 100644 index 0000000000..d4026e9558 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3332.xml @@ -0,0 +1,139 @@ + + + + + + + Direction + This IPSO object is used to report the direction indicated by a compass, wind vane, or other directional indicator. The units of measure is plane angle degrees. + + 3332 + urn:oma:lwm2m:ext:3332:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Compass Direction + R + Single + Mandatory + Float + 0..360 + deg + The measured compass direction. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3333.xml b/application/src/main/data/lwm2m-registry/3333.xml new file mode 100644 index 0000000000..1fc7117482 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3333.xml @@ -0,0 +1,98 @@ + + + + + + Time + This IPSO object is used to report the current time in seconds since January 1, 1970 UTC. There is also a fractional time counter that has a range of less than one second. + + 3333 + urn:oma:lwm2m:ext:3333:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Current Time + RW + Single + Mandatory + Time + + + Unix Time. A signed integer representing the number of seconds since Jan 1st, 1970 in the UTC time zone. + + + Fractional Time + RW + Single + Optional + Float + 0..1 + s + Fractional part of the time when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3334.xml b/application/src/main/data/lwm2m-registry/3334.xml new file mode 100644 index 0000000000..4ff3e6c6a6 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3334.xml @@ -0,0 +1,229 @@ + + + + + + + Gyrometer + This IPSO Object is used to report the current reading of a gyrometer sensor in 3 axes. It provides tracking of the minimum and maximum angular rate in all 3 axes. An example unit of measure is radians per second. + + 3334 + urn:oma:lwm2m:ext:3334:1.1 + 1.0 + 1.1 + Multiple + Optional + + + X Value + R + Single + Mandatory + Float + + + The measured value along the X axis. + + + Y Value + R + Single + Optional + Float + + + The measured value along the Y axis. + + + Z Value + R + Single + Optional + Float + + + The measured value along the Z axis. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min X Value + R + Single + Optional + Float + + + The minimum measured value along the X axis. + + + Max X Value + R + Single + Optional + Float + + + The maximum measured value along the X axis. + + + Min Y Value + R + Single + Optional + Float + + + The minimum measured value along the Y axis. + + + Max Y Value + R + Single + Optional + Float + + + The maximum measured value along the Y axis. + + + Min Z Value + R + Single + Optional + Float + + + The minimum measured value along the Z axis. + + + Max Z Value + R + Single + Optional + Float + + + The maximum measured value along the Z axis. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3335.xml b/application/src/main/data/lwm2m-registry/3335.xml new file mode 100644 index 0000000000..9f608653f5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3335.xml @@ -0,0 +1,119 @@ + + + + + + + Colour + This IPSO object should be used to report the measured value of a colour sensor in some colour space described by the units resource. + + 3335 + urn:oma:lwm2m:ext:3335:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Colour + RW + Single + Mandatory + String + + + A string representing a value in some color space. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3336.xml b/application/src/main/data/lwm2m-registry/3336.xml new file mode 100644 index 0000000000..68fea3b90e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3336.xml @@ -0,0 +1,149 @@ + + + + + + + Location + This IPSO object represents GPS coordinates. This object is compatible with the LwM2M management object for location, but uses reusable resources. + + 3336 + urn:oma:lwm2m:ext:3336:2.0 + 1.0 + 2.0 + Multiple + Optional + + + Numeric Latitude + R + Single + Mandatory + Float + + lat + The decimal notation of latitude, e.g. -43.5723 (World Geodetic System 1984). + + + Numeric Longitude + R + Single + Mandatory + Float + + lon + The decimal notation of longitude, e.g. 153.21760 (World Geodetic System 1984). + + + Numeric Uncertainty + R + Single + Optional + Float + + m + The accuracy of the position in meters. + + + Compass Direction + R + Single + Optional + Float + 0..360 + deg + The measured compass direction. + + + Velocity + R + Single + Optional + Opaque + + + The velocity of the device as defined in 3GPP 23.032 GAD specification. This set of values may not be available if the device is static. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3337.xml b/application/src/main/data/lwm2m-registry/3337.xml new file mode 100644 index 0000000000..fa4eefbe81 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3337.xml @@ -0,0 +1,159 @@ + + + + + + + Positioner + This IPSO object should be used with a generic position actuator with range from 0 to 100%. This object optionally allows setting the transition time for an operation that changes the position of the actuator, and for reading the remaining time of the currently active transition. + + 3337 + urn:oma:lwm2m:ext:3337:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Current Position + RW + Single + Mandatory + Float + 0..100 + /100 + Current position or desired position of a positioner actuator. + + + Transition Time + RW + Single + Optional + Float + + s + The time expected to move the actuator to the new position. + + + Remaining Time + R + Single + Optional + Float + + s + The time remaining in an operation. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Min Limit + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Limit + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3338.xml b/application/src/main/data/lwm2m-registry/3338.xml new file mode 100644 index 0000000000..fd1fba3332 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3338.xml @@ -0,0 +1,119 @@ + + + + + + + Buzzer + This IPSO object should be used to actuate an audible alarm such as a buzzer, beeper, or vibration alarm. There is a dimmer control for setting the relative level of the alarm, and an optional duration control to limit the length of time the alarm sounds when turned on. Each time "true" is written to the On/Off resource, the alarm will sound again for the configured duration. If no duration is programmed or the setting is "false", writing a "true" to the On/Off resource will result in the alarm sounding continuously until a "false" is written to the On/Off resource. + + 3338 + urn:oma:lwm2m:ext:3338:1.1 + 1.0 + 1.1 + Multiple + Optional + + + On/Off + RW + Single + Mandatory + Boolean + + + On/off control. Boolean value where True is On and False is Off. + + + Level + RW + Single + Optional + Float + 0..100 + /100 + Used to represent a level control such as audio volume. + + + Delay Duration + RW + Single + Optional + Float + + s + The duration of the time delay. + + + Minimum Off-time + RW + Single + Mandatory + Float + + s + The duration of the rearm delay (i.e. the delay from the end of one cycle until the beginning of the next, the inhibit time). + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3339.xml b/application/src/main/data/lwm2m-registry/3339.xml new file mode 100644 index 0000000000..194685474e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3339.xml @@ -0,0 +1,98 @@ + + + + + + + Audio Clip + This IPSO object should be used for a speaker that plays a pre-recorded audio clip or an audio output that is sent elsewhere. For example, an elevator which announces the floor of the building. A resource is provided to store the clip, a dimmer resource controls the relative sound level of the playback, and a duration resource limits the maximum playback time. After the duration time is reached, any remaining samples in the clip are ignored, and the clip player will be ready to play another clip. + 3339 + urn:oma:lwm2m:ext:3339 + 1.0 + 1.0 + Multiple + Optional + + + Clip + RW + Single + Mandatory + Opaque + + + Audio clip that is playable (e.g., a short audio recording indicating the floor in an elevator). + + + Trigger + E + Single + Optional + + + + Trigger initiating actuation. + + + Level + RW + Single + Optional + Float + 0..100 + /100 + Used to represent a level control such as audio volume. + + + Duration + RW + Single + Optional + Float + + s + The duration of the sound once trigger. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3340.xml b/application/src/main/data/lwm2m-registry/3340.xml new file mode 100644 index 0000000000..35de944f3a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3340.xml @@ -0,0 +1,159 @@ + + + + + + + Timer + This IPSO object is used to time events and actions, using patterns common to industrial timers. A write to the trigger resource or On/Off input state change starts the timing operation, and the timer remaining time shows zero when the operation is complete. The patterns supported are One-Shot (mode 1), On-Time or Interval (mode 2), Time delay on pick-up or TDPU (mode 3), and Time Delay on Drop-Out or TDDO (mode 4). Mode 0 disables the timer, so the output follows the input with no delay. A counter is provided to count occurrences of the timer output changing from 0 to 1. Writing a value of zero resets the counter. The Digital Input State resource reports the state of the timer output. + + 3340 + urn:oma:lwm2m:ext:3340 + 1.0 + 1.0 + Multiple + Optional + + + Delay Duration + RW + Single + Mandatory + Float + + s + The duration of the time delay. + + + Remaining Time + R + Single + Optional + Float + + s + The time remaining in an operation. + + + Minimum Off-time + RW + Single + Optional + Float + + s + The duration of the rearm delay (i.e. the delay from the end of one cycle until the beginning of the next, the inhibit time). + + + Trigger + E + Single + Optional + + + + Trigger initiating actuation. + + + On/Off + RW + Single + Optional + Boolean + + + On/off control. Boolean value where True is On and False is Off. + + + Digital Input Counter + R + Single + Optional + Integer + + + The cumulative value of active state detected. + + + Cumulative Time + RW + Single + Optional + Float + + s + The total time in seconds that the timer input is true. Writing a 0 resets the time. + + + Digital State + R + Single + Optional + Boolean + + + The current state of the timer output. + + + Counter + RW + Single + Optional + Integer + + + Counts the number of times the timer output transitions from 0 to 1. + + + Timer Mode + RW + Single + Optional + Integer + 0..4 + + Type of timer pattern used by the timer. 1: One-shot, 2: On-Time or Interval, 3: Time delay on pick-up, 4: Time Delay on Drop-Out, 0: disables the timer. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3341.xml b/application/src/main/data/lwm2m-registry/3341.xml new file mode 100644 index 0000000000..38e8fb8d9d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3341.xml @@ -0,0 +1,139 @@ + + + + + + + Addressable Text Display + This IPSO object is used to send text to a text-only or text mode graphics display. Writing a string of text to the text resource causes it to be displayed at the selected X and Y locations on the display. If X or Y are set to a value greater than the size of the display, the position "wraps around" to the modulus of the setting and the display size. Likewise, if the text string overflows the display size, the text "wraps around" and displays on the next line down or, if the last line has been written, wraps around to the top of the display. Brightness and Contrast controls are provided to allow control of various display types including STN and DSTN type LCD character displays. Writing an empty payload to the Clear Display resource causes the display to be erased. + + 3341 + urn:oma:lwm2m:ext:3341 + 1.0 + 1.0 + Multiple + Optional + + + Text + RW + Single + Mandatory + String + + + A string of text. + + + X Coordinate + RW + Single + Optional + Integer + + + X Coordinate. + + + Y Coordinate + RW + Single + Optional + Integer + + + Y Coordinate. + + + Max X Coordinate + R + Single + Optional + Integer + + + The highest X coordinate the display supports before wrapping to the next line. + + + Max Y Coordinate + R + Single + Optional + Integer + + + The highest Y coordinate the display supports before wrapping to the next line. + + + Clear Display + E + Single + Optional + + + + Command to clear the display. + + + Level + RW + Single + Optional + Float + 0..100 + /100 + Used to represent a level control such as audio volume. + + + Contrast + RW + Single + Optional + Float + 0..100 + /100 + Proportional control, integer value between 0 and 100 as a percentage. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3342.xml b/application/src/main/data/lwm2m-registry/3342.xml new file mode 100644 index 0000000000..cd27717ed4 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3342.xml @@ -0,0 +1,118 @@ + + + + + + + On/Off switch + This IPSO object should be used with an On/Off switch to report the state of the switch. + 3342 + urn:oma:lwm2m:ext:3342:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Digital Input State + R + Single + Mandatory + Boolean + + + The current state of a digital input. + + + Digital Input Counter + R + Single + Optional + Integer + + + The cumulative value of active state detected. + + + On time + RW + Single + Optional + Integer + + s + The time in seconds that the device has been on. Writing a value of 0 resets the counter. + + + Off Time + RW + Single + Optional + Integer + + s + The time in seconds in the off state. Writing a value of 0 resets the counter. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3343.xml b/application/src/main/data/lwm2m-registry/3343.xml new file mode 100644 index 0000000000..3aa109923a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3343.xml @@ -0,0 +1,89 @@ + + + + + + + Dimmer + This IPSO object should be used with a dimmer or level control to report the state of the control. + + 3343 + urn:oma:lwm2m:ext:3343 + 1.0 + 1.0 + Multiple + Optional + + + Level + RW + Single + Mandatory + Float + 0..100 + /100 + Used to represent a level control such as audio volume. + + + On time + RW + Single + Optional + Integer + + s + The time in seconds that the device has been on. Writing a value of 0 resets the counter. + + + Off Time + RW + Single + Optional + Integer + + s + The time in seconds in the off state. Writing a value of 0 resets the counter. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3344.xml b/application/src/main/data/lwm2m-registry/3344.xml new file mode 100644 index 0000000000..2dd8648a19 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3344.xml @@ -0,0 +1,99 @@ + + + + + + + Up/Down Control + This IPSO object is used to report the state of an up/down control element like a pair of push buttons or a rotary encoder. Counters for increase and decrease operations are provided for counting pulses from a quadrature encoder. + + 3344 + urn:oma:lwm2m:ext:3344 + 1.0 + 1.0 + Multiple + Optional + + + Increase Input State + R + Single + Mandatory + Boolean + + + Indicates an increase control action. + + + Decrease Input State + R + Single + Mandatory + Boolean + + + Indicates a decrease control action. + + + Up Counter + RW + Single + Optional + Integer + + + Counts the number of times the increase control has been operated. Writing a 0 resets the counter. + + + Down Counter + RW + Single + Optional + Integer + + + Counts the times the decrease control has been operated. Writing a 0 resets the counter. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3345.xml b/application/src/main/data/lwm2m-registry/3345.xml new file mode 100644 index 0000000000..bb234638a9 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3345.xml @@ -0,0 +1,109 @@ + + + + + + + Multiple Axis Joystick + This IPSO object can be used to report the position of a shuttle or joystick control. A digital input is provided to report the state of an associated push button. + + 3345 + urn:oma:lwm2m:ext:3345 + 1.0 + 1.0 + Multiple + Optional + + + Digital Input State + R + Single + Optional + Boolean + + + The current state of a digital input. + + + Digital Input Counter + R + Single + Optional + Integer + + + The cumulative value of active state detected. + + + X Value + R + Single + Optional + Float + + + The measured value along the X axis. + + + Y Value + R + Single + Optional + Float + + + The measured value along the Y axis. + + + Z Value + R + Single + Optional + Float + + + The measured value along the Z axis. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3346.xml b/application/src/main/data/lwm2m-registry/3346.xml new file mode 100644 index 0000000000..d49322f32a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3346.xml @@ -0,0 +1,179 @@ + + + + + + + Rate + This object type should be used to report a rate measurement, for example the speed of a vehicle, or the rotational speed of a drive shaft. It also provides resources for minimum and maximum measured values, as well as the minimum and maximum range that can be measured by the sensor. An example measurement unit is meters per second (m/s). + + 3346 + urn:oma:lwm2m:ext:3346:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Sensor Value + R + Single + Mandatory + Float + + + Last or Current Measured Value from the Sensor. + + + Sensor Units + R + Single + Optional + String + + + Measurement Units Definition. + + + Min Measured Value + R + Single + Optional + Float + + + The minimum value measured by the sensor since power ON or reset. + + + Max Measured Value + R + Single + Optional + Float + + + The maximum value measured by the sensor since power ON or reset. + + + Min Range Value + R + Single + Optional + Float + + + The minimum value that can be measured by the sensor. + + + Max Range Value + R + Single + Optional + Float + + + The maximum value that can be measured by the sensor. + + + Reset Min and Max Measured Values + E + Single + Optional + + + + Reset the Min and Max Measured Values to Current Value. + + + Current Calibration + RW + Single + Optional + Float + + + Read or Write the current calibration coefficient. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3347.xml b/application/src/main/data/lwm2m-registry/3347.xml new file mode 100644 index 0000000000..136e3445b9 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3347.xml @@ -0,0 +1,99 @@ + + + + + + + Push button + This IPSO object is used to report the state of a momentary action push button control and to count the number of times the control has been operated since the last observation. + + 3347 + urn:oma:lwm2m:ext:3347:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Digital Input State + R + Single + Mandatory + Boolean + + + The current state of a digital input. + + + Digital Input Counter + R + Single + Optional + Integer + + + The cumulative value of active state detected. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3348.xml b/application/src/main/data/lwm2m-registry/3348.xml new file mode 100644 index 0000000000..9309b4b3dd --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3348.xml @@ -0,0 +1,89 @@ + + + + + + + Multi-state Selector + This IPSO object is used to represent the state of a Multi-state selector switch with a number of fixed positions. + + 3348 + urn:oma:lwm2m:ext:3348:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Multi-state Input + R + Single + Mandatory + Integer + + + The current state of a Multi-state input or selector. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3349.xml b/application/src/main/data/lwm2m-registry/3349.xml new file mode 100644 index 0000000000..28029e7a6a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3349.xml @@ -0,0 +1,108 @@ + + + + + + + Bitmap + Summarize several digital inputs to one value by mapping each bit to a digital input. + 3349 + urn:oma:lwm2m:ext:3349:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Bitmap Input + R + Single + Mandatory + Integer + + + Integer in which each of the bits are associated with specific digital input value. Represented as a binary signed integer in network byte order, and in two's complement representation. Using values in range 0-127 is recommended to avoid ambiguities with byte order and negative values. + + + Bitmap Input Reset + E + Single + Optional + + + + Reset the Bitmap Input value. + + + Element Description + RW + Multiple + Optional + String + + + The description of each bit as a string. First instance describes the least significant bit, second instance the second least significant bit. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3350.xml b/application/src/main/data/lwm2m-registry/3350.xml new file mode 100644 index 0000000000..f4f8477fdc --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3350.xml @@ -0,0 +1,128 @@ + + + + + + + Stopwatch + An ascending timer that counts how long time has passed since the timer was started after reset. + 3350 + urn:oma:lwm2m:ext:3350:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Cumulative Time + RW + Single + Mandatory + Float + + s + The total time in seconds that the timer input is true. Writing a 0 resets the time. + + + On/Off + RW + Single + Optional + Boolean + + + On/off control. Boolean value where True is On and False is Off. + + + Digital Input Counter + R + Single + Optional + Integer + + + The cumulative value of active state detected. + + + Application Type + RW + Single + Optional + String + + + The application type of the sensor or actuator as a string depending on the use case. + + + Timestamp + R + Single + Optional + Time + + + The timestamp of when the measurement was performed. + + + Fractional Timestamp + R + Single + Optional + Float + 0..1 + s + Fractional part of the timestamp when sub-second precision is used (e.g., 0.23 for 230 ms). + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. 0: UNCHECKED No quality checks were done because they do not exist or can not be applied. 1: REJECTED WITH CERTAINTY The measured value is invalid. 2: REJECTED WITH PROBABILITY The measured value is likely invalid. 3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. 4: ACCEPTED The measured value is OK. 5-15: Reserved for future extensions. 16-23: Vendor specific measurement quality. + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3351.xml b/application/src/main/data/lwm2m-registry/3351.xml new file mode 100644 index 0000000000..9219a765df --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3351.xml @@ -0,0 +1,131 @@ + + + powerupLog + + 3351 + urn:oma:lwm2m:ext:3351 + 1.0 + 1.0 + Single + Optional + + deviceName + R + Single + Mandatory + String + + + + + toolVersion + R + Single + Mandatory + String + + + + + IMEI + R + Single + Mandatory + String + + + + + IMSI + R + Single + Mandatory + String + + + + + MSISDN + R + Single + Mandatory + String + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3352.xml b/application/src/main/data/lwm2m-registry/3352.xml new file mode 100644 index 0000000000..0e519c4951 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3352.xml @@ -0,0 +1,124 @@ + + + plmnSearchEvent + + 3352 + urn:oma:lwm2m:ext:3352 + 1.0 + 1.0 + Multiple + Optional + + timeScanStart + R + Single + Mandatory + Integer + + + + + plmnID + R + Single + Mandatory + Integer + + + + + BandIndicator + R + Single + Mandatory + Integer + + + + + dlEarfcn + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3353.xml b/application/src/main/data/lwm2m-registry/3353.xml new file mode 100644 index 0000000000..1881a3b3bb --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3353.xml @@ -0,0 +1,123 @@ + + + scellID + + 3353 + urn:oma:lwm2m:ext:3353 + 1.0 + 1.0 + Single + Optional + + plmnID + R + Single + Mandatory + Integer + + + + + BandIndicator + R + Single + Mandatory + Integer + + + + + TrackingAreaCode + R + Single + Mandatory + Integer + + + + + CellID + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3354.xml b/application/src/main/data/lwm2m-registry/3354.xml new file mode 100644 index 0000000000..702be4cabc --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3354.xml @@ -0,0 +1,134 @@ + + + cellReselectionEvent + + 3354 + urn:oma:lwm2m:ext:3354 + 1.0 + 1.0 + Single + Optional + + timeReselectionStart + R + Single + Mandatory + Integer + + + + + dlEarfcn + R + Single + Mandatory + Integer + + + + + CellID + R + Single + Mandatory + Integer + + + + + failureType + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3355.xml b/application/src/main/data/lwm2m-registry/3355.xml new file mode 100644 index 0000000000..fb56230fdf --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3355.xml @@ -0,0 +1,156 @@ + + + handoverEvent + + 3355 + urn:oma:lwm2m:ext:3355 + 1.0 + 1.0 + Single + Optional + + timeHandoverStart + R + Single + Mandatory + Integer + + + + + dlEarfcn + R + Single + Mandatory + Integer + + + + + CellID + R + Single + Mandatory + Integer + + + + + handoverResult + R + Single + Mandatory + Integer + + + + + + TargetEarfcn + R + Single + Mandatory + Integer + + + + + TargetPhysicalCellID + R + Single + Mandatory + Integer + + + + + targetCellRsrp + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3356.xml b/application/src/main/data/lwm2m-registry/3356.xml new file mode 100644 index 0000000000..95c7fc7835 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3356.xml @@ -0,0 +1,120 @@ + + + radioLinkFailureEvent + + 3356 + urn:oma:lwm2m:ext:3356 + 1.0 + 1.0 + Single + Optional + + timeRLF + R + Single + Mandatory + Integer + + + + + rlfCause + R + Single + Mandatory + Integer + + + + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3357.xml b/application/src/main/data/lwm2m-registry/3357.xml new file mode 100644 index 0000000000..935b7c4ba7 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3357.xml @@ -0,0 +1,131 @@ + + + rrcStateChangeEvent + + 3357 + urn:oma:lwm2m:ext:3357 + 1.0 + 1.0 + Single + Optional + + rrcState + R + Single + Mandatory + Integer + + + + + rrcStateChangeCause + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3358.xml b/application/src/main/data/lwm2m-registry/3358.xml new file mode 100644 index 0000000000..2784d6a990 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3358.xml @@ -0,0 +1,106 @@ + + + rrcTimerExpiryEvent + + 3358 + urn:oma:lwm2m:ext:3358 + 1.0 + 1.0 + Single + Optional + + RrcTimerExpiryEvent + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3359.xml b/application/src/main/data/lwm2m-registry/3359.xml new file mode 100644 index 0000000000..91cb845e7a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3359.xml @@ -0,0 +1,105 @@ + + + cellBlacklistEvent + + 3359 + urn:oma:lwm2m:ext:3359 + 1.0 + 1.0 + Single + Optional + + dlEarfcn + R + Single + Mandatory + Integer + + + + + CellID + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3360.xml b/application/src/main/data/lwm2m-registry/3360.xml new file mode 100644 index 0000000000..12b2302854 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3360.xml @@ -0,0 +1,128 @@ + + + esmContextInfo + + 3360 + urn:oma:lwm2m:ext:3360 + 1.0 + 1.0 + Single + Optional + + contextType + R + Single + Mandatory + Integer + + + + + bearerState + R + Single + Mandatory + Integer + + + + + radioBearerId + R + Single + Mandatory + Integer + + + + + qci + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3361.xml b/application/src/main/data/lwm2m-registry/3361.xml new file mode 100644 index 0000000000..3d354bffac --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3361.xml @@ -0,0 +1,130 @@ + + + emmStateValue + + 3361 + urn:oma:lwm2m:ext:3361 + 1.0 + 1.0 + Single + Optional + + EmmState + R + Single + Mandatory + Integer + + + + + emmSubstate + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3362.xml b/application/src/main/data/lwm2m-registry/3362.xml new file mode 100644 index 0000000000..3cb73eb79a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3362.xml @@ -0,0 +1,97 @@ + + + nasEmmTimerExpiryEvent + + 3362 + urn:oma:lwm2m:ext:3362 + 1.0 + 1.0 + Single + Optional + + NasEmmTimerExpiryEvent + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3363.xml b/application/src/main/data/lwm2m-registry/3363.xml new file mode 100644 index 0000000000..3b44b91f26 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3363.xml @@ -0,0 +1,100 @@ + + + nasEsmExpiryEvent + + 3363 + urn:oma:lwm2m:ext:3363 + 1.0 + 1.0 + Single + Optional + + NasEsmExpiryEvent + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3364.xml b/application/src/main/data/lwm2m-registry/3364.xml new file mode 100644 index 0000000000..0a042d12d2 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3364.xml @@ -0,0 +1,97 @@ + + + emmFailureCauseEvent + + 3364 + urn:oma:lwm2m:ext:3364 + 1.0 + 1.0 + Single + Optional + + EMMCause + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3365.xml b/application/src/main/data/lwm2m-registry/3365.xml new file mode 100644 index 0000000000..83dbf0c807 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3365.xml @@ -0,0 +1,123 @@ + + + rachLatency_delay + + 3365 + urn:oma:lwm2m:ext:3365 + 1.0 + 1.0 + Single + Optional + + sysFrameNumber + R + Single + Mandatory + Integer + + + + + subFrameNumber + R + Single + Mandatory + Integer + + + + + rachLatencyVal + R + Single + Mandatory + Integer + + + + + delay + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3366.xml b/application/src/main/data/lwm2m-registry/3366.xml new file mode 100644 index 0000000000..5498af2a3d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3366.xml @@ -0,0 +1,176 @@ + + + macRachAttemptEvent + + 3366 + urn:oma:lwm2m:ext:3366 + 1.0 + 1.0 + Single + Optional + + rachAttemptCounter + R + Single + Mandatory + Integer + + + + + MacRachAttemptEventType + R + Single + Mandatory + Integer + + + + + contentionBased + R + Single + Mandatory + Boolean + + + + + rachMessage + R + Single + Mandatory + Integer + + + + + preambleIndex + R + Single + Mandatory + Integer + + + + + preamblePowerOffset + R + Single + Mandatory + Integer + + + + + backoffTime + R + Single + Mandatory + Integer + + + + + msg2Result + R + Single + Mandatory + Boolean + + + + + timingAdjustmentValue + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3367.xml b/application/src/main/data/lwm2m-registry/3367.xml new file mode 100644 index 0000000000..1f35cf4cc5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3367.xml @@ -0,0 +1,138 @@ + + + macRachAttemptReasonEvent + + 3367 + urn:oma:lwm2m:ext:3367 + 1.0 + 1.0 + Single + Optional + + MacRachAttemptReasonType + R + Single + Mandatory + Integer + + + + + ueID + R + Single + Mandatory + Integer + + + + + contentionBased + R + Single + Mandatory + Boolean + + + + + preamble + R + Single + Mandatory + Integer + + + + + preambleGroupChosen + R + Single + Mandatory + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3368.xml b/application/src/main/data/lwm2m-registry/3368.xml new file mode 100644 index 0000000000..40c17bfd37 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3368.xml @@ -0,0 +1,100 @@ + + + macTimerStatusEvent + + 3368 + urn:oma:lwm2m:ext:3368 + 1.0 + 1.0 + Single + Optional + + macTimerName + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3369.xml b/application/src/main/data/lwm2m-registry/3369.xml new file mode 100644 index 0000000000..b93bb86644 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3369.xml @@ -0,0 +1,105 @@ + + + macTimingAdvanceEvent + + 3369 + urn:oma:lwm2m:ext:3369 + 1.0 + 1.0 + Single + Optional + + timerValue + R + Single + Mandatory + Integer + + + + + timingAdvance + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3370.xml b/application/src/main/data/lwm2m-registry/3370.xml new file mode 100644 index 0000000000..667ab0a281 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3370.xml @@ -0,0 +1,141 @@ + + + ServingCellMeasurement + + 3370 + urn:oma:lwm2m:ext:3370 + 1.0 + 1.0 + Single + Optional + + sysFrameNumber + R + Single + Mandatory + Integer + + + + + subFrameNumber + R + Single + Mandatory + Integer + + + + + pci + R + Single + Mandatory + Integer + + + + + rsrp + R + Single + Mandatory + Integer + + + + + rsrq + R + Single + Mandatory + Integer + + + + + dlEarfcn + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3371.xml b/application/src/main/data/lwm2m-registry/3371.xml new file mode 100644 index 0000000000..fbc0860870 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3371.xml @@ -0,0 +1,141 @@ + + + NeighborCellMeasurements + + 3371 + urn:oma:lwm2m:ext:3371 + 1.0 + 1.0 + Multiple + Optional + + sysFrameNumber + R + Single + Mandatory + Integer + + + + + subFrameNumber + R + Single + Mandatory + Integer + + + + + pci + R + Single + Mandatory + Integer + + + + + rsrp + R + Single + Mandatory + Integer + + + + + rsrq + R + Single + Mandatory + Integer + + + + + dlEarfcn + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3372.xml b/application/src/main/data/lwm2m-registry/3372.xml new file mode 100644 index 0000000000..49d585a973 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3372.xml @@ -0,0 +1,115 @@ + + + TimingAdvance + + 3372 + urn:oma:lwm2m:ext:3372 + 1.0 + 1.0 + Single + Optional + + sysFrameNumber + R + Single + Mandatory + Integer + + + + + subFrameNumber + R + Single + Mandatory + Integer + + + + + timingAdvance + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3373.xml b/application/src/main/data/lwm2m-registry/3373.xml new file mode 100644 index 0000000000..fb2af328b3 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3373.xml @@ -0,0 +1,115 @@ + + + txPowerHeadroomEvent + + 3373 + urn:oma:lwm2m:ext:3373 + 1.0 + 1.0 + Single + Optional + + sysFrameNumber + R + Single + Mandatory + Integer + + + + + subFrameNumber + R + Single + Mandatory + Integer + + + + + headroom-value + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3374.xml b/application/src/main/data/lwm2m-registry/3374.xml new file mode 100644 index 0000000000..be7ecdc34e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3374.xml @@ -0,0 +1,132 @@ + + + radioLinkMonitoring + + 3374 + urn:oma:lwm2m:ext:3374 + 1.0 + 1.0 + Single + Optional + + sysFrameNumber + R + Single + Mandatory + Integer + + + + + subFrameNumber + R + Single + Mandatory + Integer + + + + + outOfSyncCount + R + Single + Mandatory + Integer + + + + + inSyncCount + R + Single + Mandatory + Integer + + + + + t310Timer + R + Single + Mandatory + Boolean + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3375.xml b/application/src/main/data/lwm2m-registry/3375.xml new file mode 100644 index 0000000000..75fb2a5a42 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3375.xml @@ -0,0 +1,150 @@ + + + PagingDRX + + 3375 + urn:oma:lwm2m:ext:3375 + 1.0 + 1.0 + Single + Optional + + dlEarfcn + R + Single + Mandatory + Integer + + + + + pci + R + Single + Mandatory + Integer + + + + + pagingCycle + R + Single + Mandatory + Integer + + + + + DrxNb + R + Single + Mandatory + Integer + + + + + ueID + R + Single + Mandatory + Integer + + + + + drxSysFrameNumOffset + R + Single + Mandatory + Integer + + + + + drxSubFrameNumOffset + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3376.xml b/application/src/main/data/lwm2m-registry/3376.xml new file mode 100644 index 0000000000..542f816da0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3376.xml @@ -0,0 +1,97 @@ + + + txPowerBackOffEvent + + 3376 + urn:oma:lwm2m:ext:3376 + 1.0 + 1.0 + Single + Optional + + TxPowerBackoff + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3377.xml b/application/src/main/data/lwm2m-registry/3377.xml new file mode 100644 index 0000000000..515eb36439 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3377.xml @@ -0,0 +1,168 @@ + + + Message3Report + + 3377 + urn:oma:lwm2m:ext:3377 + 1.0 + 1.0 + Single + Optional + + tpc + R + Single + Mandatory + Integer + + + + + resourceIndicatorValue + R + Single + Mandatory + Integer + + + + + cqi + R + Single + Mandatory + Integer + + + + + uplinkDelay + R + Single + Mandatory + Boolean + + + + + hoppingEnabled + R + Single + Mandatory + Boolean + + + + + numRb + R + Single + Mandatory + Integer + + + + + transportBlockSizeIndex + R + Single + Mandatory + Integer + + + + + ModulationType + R + Single + Mandatory + Integer + + + + + redundancyVersionIndex + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3378.xml b/application/src/main/data/lwm2m-registry/3378.xml new file mode 100644 index 0000000000..970d245a90 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3378.xml @@ -0,0 +1,115 @@ + + + PbchDecodingResults + + 3378 + urn:oma:lwm2m:ext:3378 + 1.0 + 1.0 + Single + Optional + + servingCellID + R + Single + Mandatory + Integer + + + + + crcResult + R + Single + Mandatory + Boolean + + + + + sysFrameNumber + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3379.xml b/application/src/main/data/lwm2m-registry/3379.xml new file mode 100644 index 0000000000..87137da530 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3379.xml @@ -0,0 +1,123 @@ + + + pucchPowerControl + + 3379 + urn:oma:lwm2m:ext:3379 + 1.0 + 1.0 + Single + Optional + + sysFrameNumber + R + Single + Mandatory + Integer + + + + + subFrameNumber + R + Single + Mandatory + Integer + + + + + pucchTxPowerValue + R + Single + Mandatory + Integer + + + + + dlPathLoss + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3380.xml b/application/src/main/data/lwm2m-registry/3380.xml new file mode 100644 index 0000000000..8290651569 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3380.xml @@ -0,0 +1,204 @@ + + + PrachReport + + 3380 + urn:oma:lwm2m:ext:3380:2.0 + 1.0 + 2.0 + Single + Optional + + sysFrameNumber + R + Single + Mandatory + Integer + + + + + subFrameNumber + R + Single + Mandatory + Integer + + + + + rachTxPower + R + Single + Mandatory + Integer + + + + + zadOffSeqNum + R + Single + Mandatory + Integer + + + + + prachConfig + R + Single + Mandatory + Integer + + + + + preambleFormat + R + Single + Mandatory + Integer + + + + + maxTransmissionMsg3 + R + Single + Mandatory + Integer + + + + + raResponseWindowSize + R + Single + Mandatory + Integer + + + + + RachRequestResult + R + Single + Mandatory + Boolean + + + + + ce_mode + R + Single + Mandatory + Integer + + + + + ce_level + R + Single + Mandatory + Integer + + + + + num_prach_repetition + R + Single + Mandatory + Integer + + + + + prach_repetition_seq + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3381.xml b/application/src/main/data/lwm2m-registry/3381.xml new file mode 100644 index 0000000000..db2e4d52e8 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3381.xml @@ -0,0 +1,110 @@ + + + VolteCallEvent + + 3381 + urn:oma:lwm2m:ext:3381 + 1.0 + 1.0 + Single + Optional + + callStatus + R + Single + Mandatory + Integer + + + + + callType + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3382.xml b/application/src/main/data/lwm2m-registry/3382.xml new file mode 100644 index 0000000000..8f066837c8 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3382.xml @@ -0,0 +1,108 @@ + + + SipRegistrationEvent + + 3382 + urn:oma:lwm2m:ext:3382 + 1.0 + 1.0 + Single + Optional + + registrationType + R + Single + Mandatory + Integer + + + + + registrationResult + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3383.xml b/application/src/main/data/lwm2m-registry/3383.xml new file mode 100644 index 0000000000..663ac45faa --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3383.xml @@ -0,0 +1,99 @@ + + + sipPublishEvent + + 3383 + urn:oma:lwm2m:ext:3383 + 1.0 + 1.0 + Single + Optional + + publishResult + R + Single + Mandatory + Integer + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3384.xml b/application/src/main/data/lwm2m-registry/3384.xml new file mode 100644 index 0000000000..a18c6b910b --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3384.xml @@ -0,0 +1,112 @@ + + + sipSubscriptionEvent + + 3384 + urn:oma:lwm2m:ext:3384 + 1.0 + 1.0 + Single + Optional + + eventType + R + Single + Mandatory + Integer + + + + + subscriptionResult + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3385.xml b/application/src/main/data/lwm2m-registry/3385.xml new file mode 100644 index 0000000000..cc0a99182a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3385.xml @@ -0,0 +1,110 @@ + + + volteCallStateChangeEvent + + 3385 + urn:oma:lwm2m:ext:3385 + 1.0 + 1.0 + Single + Optional + + callStatus + R + Single + Mandatory + Integer + + + + + VolteCallStateChangeCause + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3386.xml b/application/src/main/data/lwm2m-registry/3386.xml new file mode 100644 index 0000000000..43937efadb --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3386.xml @@ -0,0 +1,105 @@ + + + VoLTErtpPacketLoss + 1]]> + 3386 + urn:oma:lwm2m:ext:3386 + 1.0 + 1.0 + Single + Optional + + ssrc + R + Single + Mandatory + Integer + + + + + packetsLost + R + Single + Mandatory + Integer + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3387.xml b/application/src/main/data/lwm2m-registry/3387.xml new file mode 100644 index 0000000000..c50ecb47bc --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3387.xml @@ -0,0 +1,224 @@ + + + + + + + oA Basic Control + + 3387 + urn:oma:lwm2m:ext:3387 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Control Function Status + R + Single + Mandatory + Integer + 0: disabled + 1: normal + 2: remote + 3: local + 255: non_responsive + + + + + Ghost Status + R + Single + Mandatory + Boolean + + + + + + Control Object Mode + RW + Single + Mandatory + Integer + 1: normal + 2: remote + 3: local + + + + + Ghost Configuration + RW + Single + Optional + Boolean + 0..65535 + + + + + Supersede Heartbeat Time + RW + Single + Mandatory + Integer + + s + + + + Controlled Application Group ID + RW + Single + Mandatory + Integer + 0..65535 + + + + + Push-Button Event Configuration + RW + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Presence Sensor Event Configuration + RW + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Executing Object + RW + Single + Mandatory + Corelnk + + + + + + Supersede Heartbeat + W + Single + Optional + Unsigned Integer + 0..65535 + s + + + + Debug Mode Enabled + RW + Single + Optional + Boolean + + + + + + Inject Test Event + RW + Single + Optional + Unsigned Integer + 0..65535 + + + + + Execute Inject + E + Single + Optional + + + + + + + Object Enabled + RW + Single + Optional + Boolean + + + + + + Active-Controller-List + R + Multiple + Optional + Opaque + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3388.xml b/application/src/main/data/lwm2m-registry/3388.xml new file mode 100644 index 0000000000..c1b5e54206 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3388.xml @@ -0,0 +1,178 @@ + + + + + + + oA Device + + 3388 + urn:oma:lwm2m:ext:3388 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Error Status + R + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + OEM ID + R + Single + Optional + String + 0..64 bytes + + + + + OEM String + R + Single + Optional + String + 0..64 bytes + + + + + BMS ID + RW + Single + Optional + Unsigned Integer + 0..65535 + + + + + CPU Temperature + R + Single + Mandatory + Integer + -128..127 + C + + + + Executing Objects + R + Multiple + Mandatory + Corelnk + + + + + + Total Energy Usage + R + Single + Mandatory + Unsigned Integer + 0..18446744073709551615 + Ws + + + + Actual Power Usage + R + Single + Mandatory + Unsigned Integer + 0..4294967295 + W + + + + Accuracy Class + R + Single + Optional + String + 0..64 bytes + + + + + Mounting Location + RW + Single + Mandatory + String + 1..64 bytes + + + + + System Failure Time + RW + Single + Optional + Unsigned Integer + 0..65535 + ms + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3389.xml b/application/src/main/data/lwm2m-registry/3389.xml new file mode 100644 index 0000000000..7e3d0c7cdb --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3389.xml @@ -0,0 +1,108 @@ + + + + + + + oA Group + + 3389 + urn:oma:lwm2m:ext:3389 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Application Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Security Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + IP Addresses + RW + Multiple + Mandatory + Opaque + + + + + + Members + RW + Multiple + Mandatory + Corelnk + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3390.xml b/application/src/main/data/lwm2m-registry/3390.xml new file mode 100644 index 0000000000..0e56d0a19a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3390.xml @@ -0,0 +1,298 @@ + + + + + + + oA Logical Colour Light-Point Actuator + + 3390 + urn:oma:lwm2m:ext:3390 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Target Colour X + R + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Target Colour Y + R + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Executing Object + RW + Single + Mandatory + Corelnk + + + + + + Priority + RW + Single + Mandatory + Unsigned Integer + 0..5 + + + + + Colour Changing Time + RW + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Colour Transition Time + RW + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Hue Step Size + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Saturation Step Size + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Application Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Status Resend Time + RW + Single + Mandatory + Unsigned Integer + 1..600 + s + + + + Status Report Structure ID + RW + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Scene Cache + RW + Multiple + Optional + Opaque + + + + + + Set Colour + W + Single + Mandatory + Opaque + + + + + + Relative Change Saturation + W + Single + Mandatory + Boolean + + + ////118 0` \ndecreases saturation.\n- `POST coap://////118 1` \nincreases saturation.]]> + + + Relative Change Hue + W + Single + Mandatory + Boolean + + + ////118 0` \nincreases hue.\n- `POST coap://////118 1` \ndecreases hue.]]> + + + Stop Transition + E + Single + Mandatory + + + + + + + Step Saturation + W + Single + Mandatory + Opaque + + + + + + Step Hue + W + Single + Mandatory + Opaque + + + + + + Recall Scene + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Debug Mode Enabled + RW + Single + Optional + Boolean + + + + + + Inject Test Event + RW + Single + Optional + Unsigned Integer + 0..65535 + + + + + Execute Inject + E + Single + Optional + + + + + + + Object Enabled + RW + Single + Optional + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3391.xml b/application/src/main/data/lwm2m-registry/3391.xml new file mode 100644 index 0000000000..0b4ce12cd0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3391.xml @@ -0,0 +1,258 @@ + + + + + + + oA Logical Colour Temperature Light-Point Actuator + + 3391 + urn:oma:lwm2m:ext:3391 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Target Colour Temperature + R + Single + Mandatory + Unsigned Integer + 0..65535 + K + + + + Executing Object + RW + Single + Mandatory + Corelnk + + + + + + Priority + RW + Single + Mandatory + Unsigned Integer + 0..5 + + + + + Colour Temperature Changing Time + RW + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Colour Temperature Transition Time + RW + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Colour Temperature Step Size + RW + Single + Mandatory + Unsigned Integer + 0..65535 + K + + + + Application Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Status Resend Time + RW + Single + Mandatory + Unsigned Integer + 1..600 + s + + + + Status Report Structure ID + RW + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Scene Cache + RW + Multiple + Optional + Opaque + + + + + + Set Colour Temperature + W + Single + Mandatory + Opaque + + + + + + Relative Change Colour Temperature + W + Single + Mandatory + Boolean + + + ////118 0` \nreduces the colour temperature.\n- `POST coap://////118 1` \nincreases the colour temperature.]]> + + + Stop Transition + E + Single + Mandatory + + + + + + + Step Colour Temperature + W + Single + Mandatory + Opaque + + + + + + Recall Scene + W + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Debug Mode Enabled + RW + Single + Optional + Boolean + + + + + + Inject Test Event + W + Single + Optional + Unsigned Integer + 0..65535 + + + + + Execute Test Injection + E + Single + Optional + + + + + + + Object Enabled + RW + Single + Optional + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3392.xml b/application/src/main/data/lwm2m-registry/3392.xml new file mode 100644 index 0000000000..07b1b154e4 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3392.xml @@ -0,0 +1,198 @@ + + + + + + + oA Logical Illuminance Sensor + + 3392 + urn:oma:lwm2m:ext:3392 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Sensor Value + R + Single + Mandatory + Unsigned Integer + 0..65535 + lx + + + + Executing Object + RW + Single + Mandatory + Corelnk + + + + + + Less Than + RW + Single + Optional + Unsigned Integer + 0..65535 + lx + + + + Greater Than + RW + Single + Optional + Unsigned Integer + 0..65535 + lx + + + + Step + RW + Single + Optional + Unsigned Integer + 0..65535 + lx + + + + Minimum Update Interval + RW + Single + Mandatory + Unsigned Integer + 0..65535 + s + + + + Application Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Status Resend Time + RW + Single + Mandatory + Unsigned Integer + 1..600 + s + + + + Status Report Structure ID + RW + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Debug Mode Enabled + RW + Single + Optional + Boolean + + + + + + Inject Test Event + W + Single + Optional + Unsigned Integer + 0..65535 + + + + + Object Enabled + RW + Single + Optional + Boolean + + + + + + Object Enabled + E + Single + Optional + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3393.xml b/application/src/main/data/lwm2m-registry/3393.xml new file mode 100644 index 0000000000..e3c883ac70 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3393.xml @@ -0,0 +1,268 @@ + + + + + + + oA Logical Light-Point Actuator + + 3393 + urn:oma:lwm2m:ext:3393 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Target OnOff + R + Single + Mandatory + Boolean + + + + + + Target Intensity + R + Single + Mandatory + Float + 0.0..1.0 + + + + + Executing Object + RW + Single + Mandatory + Corelnk + + + + + + Priority + RW + Single + Mandatory + Unsigned Integer + 0..5 + + + + + Dimming Time + RW + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Transition Time + RW + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Step Size + RW + Single + Mandatory + Float + 0.000001..1.0 + + + + + Application Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Status Resend Time + RW + Single + Mandatory + Unsigned Integer + 1..600 + s + + + + Status Report Structure ID + RW + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Scene Cache + RW + Multiple + Optional + Opaque + + + + + + Switch + W + Single + Mandatory + Opaque + + + ////117 {0:false,2:2}` \nor\n`POST coap://////117 {1:0,2:2}` \nsets the light point to Off and takes 200ms to set the intensity to '0'\n- `POST coap://////117 {0:true,2:2}` \nsets the light point to On and takes 200ms to set the intensity to the intensity that was active before the lightpoint was switched off.\n- `POST coap://////117 {1:1,2:2}` \nsets the light point to On and takes 200ms to set the intensity to '1'\n\t\t\t\n**Note:** When the optional argument time is not given, or if its value is empty the default configuration value is used.\nA request on this Resource will immediately impact the value of 'On/Off Status' and a continuous update of 'Remaining Transition Time' and 'Current Intensity' until the Switch operation is cloncluded.]]> + + + Dim + W + Single + Mandatory + Boolean + + + ////118 0` \ndims the light points down.\n- `POST coap://////118 1` \ndims the light point up.\n\t\t\t\t\nDimming is also possible during off. This effects the intensity that 'On' requests without requested intensity will execute.]]> + + + Stop Transition + E + Single + Mandatory + + + + + + + Step + W + Single + Mandatory + Opaque + + + + + + Recall Scene + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Debug Mode Enabled + RW + Single + Optional + Boolean + + + + + + Inject Test Event + W + Single + Optional + Unsigned Integer + 0..65535 + + + + + Object Enabled + E + Single + Optional + + + + + + + Object Enabled + RW + Single + Optional + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3394.xml b/application/src/main/data/lwm2m-registry/3394.xml new file mode 100644 index 0000000000..767100e145 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3394.xml @@ -0,0 +1,177 @@ + + + + + + oA Logical Presence Sensor + + 3394 + urn:oma:lwm2m:ext:3394 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Digital Input State + R + Single + Mandatory + Boolean + + + + + + Executing Object + RW + Single + Mandatory + Corelnk + + + + + + Busy to Clear delay + RW + Single + Optional + Integer + + ms + + + + Clear to Busy delay + RW + Single + Optional + Integer + + ms + + + + Application Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Status Resend Time + RW + Single + Mandatory + Unsigned Integer + 1..600 + s + + + + Status Report Structure ID + RW + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Debug Mode Enabled + RW + Single + Optional + Boolean + + + + + + Inject Test Event + W + Single + Optional + Boolean + + + + + + Object Enabled + E + Single + Optional + + + + + + + Object Enabled + RW + Single + Optional + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3395.xml b/application/src/main/data/lwm2m-registry/3395.xml new file mode 100644 index 0000000000..29b4ce6b67 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3395.xml @@ -0,0 +1,182 @@ + + + + + + + oA Logical Push-Button Sensor + + 3395 + urn:oma:lwm2m:ext:3395 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Push-Button Event Value + R + Single + Mandatory + Unsigned Integer + 0: RELEASE + 1: CLICK + 2: HOLD + 3: DOUBLE-CLICK + 255: STUCK + + + + + Executing Object + RW + Single + Mandatory + Corelnk + + + + + + Single Click Time + RW + Single + Mandatory + Unsigned Integer + 100..60000 + ms + + + + Hold Repeat Time + RW + Single + Optional + Unsigned Integer + 300..60000 + ms + + + + Application Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Status Resend Time + RW + Single + Mandatory + Unsigned Integer + 1..600 + s + + + + Status Report Structure ID + RW + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Debug Mode Enabled + RW + Single + Optional + Boolean + + + + + + Inject Test Event + W + Single + Optional + Integer + 0..65535 + + + + + Object Enabled + E + Single + Optional + + + + + + + Object Enabled + RW + Single + Optional + Boolean + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3396.xml b/application/src/main/data/lwm2m-registry/3396.xml new file mode 100644 index 0000000000..c3183f8e29 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3396.xml @@ -0,0 +1,270 @@ + + + + + + + oA Physical Colour Light-Point Actuator + + 3396 + urn:oma:lwm2m:ext:3396 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Error Status + R + Single + Mandatory + Opaque + + + + + + Mounting Location + RW + Single + Mandatory + String + 1..64 Bytes + B + + + + On/Off + RW + Single + Mandatory + Boolean + + + + + + Current Intensity + R + Single + Mandatory + Float + 0.0..1.0 + + + + + Remaining Transition Time + R + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Current Colour Y + R + Single + Mandatory + Unsigned Integer + 0..10000 + K + + + + Current Colour X + R + Single + Mandatory + Unsigned Integer + 0..10000 + K + + + + Remaining Colour Transition Time + R + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Total Energy Usage + R + Single + Mandatory + Unsigned Integer + 0..18446744073709551615 + Ws + + + + Actual Power Usage + R + Single + Mandatory + Unsigned Integer + 0..4294967295 + W + + + + Accuracy Class + R + Single + Optional + String + 0..64 bytes + + + + + Operating Hours + R + Single + Mandatory + Unsigned Integer + 0..4294967295 + h + + + + Adjusted Operating Hours + R + Single + Mandatory + Unsigned Integer + 0..4294967295 + + + + + Physical Minimum Intensity + R + Single + Optional + Float + 0.000001..1.0 + + + + + Minimum Intensity + RW + Single + Mandatory + Float + 0.000001..1.0 + + + + + Maximum Intensity + RW + Single + Mandatory + Float + 0.000001..1.0 + + + + + Power On Behaviour + RW + Single + Optional + Unsigned Integer + 0: CONFIGURED_ON + 1: FULL_ON + 2: RECALL + + + + + Power On Intensity + RW + Single + Optional + Float + 0.000001..1.0 + + + + + Stored Intensity + R + Single + Optional + Float + 0.000001..1.0 + + + + + System Failure Intensity + RW + Single + Optional + Float + 0.000001..1.0 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3397.xml b/application/src/main/data/lwm2m-registry/3397.xml new file mode 100644 index 0000000000..b8a3bed8d0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3397.xml @@ -0,0 +1,302 @@ + + + + + + + oA Physical Colour Temperature Light-Point Actuator + + 3397 + urn:oma:lwm2m:ext:3397 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Error Status + R + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Mounting Location + RW + Single + Mandatory + String + 1..64 bytes + + + + + On/Off + RW + Single + Mandatory + Boolean + + + + + + Current Intensity + R + Single + Mandatory + Float + 0.0..1.0 + + + + + Remaining Transition Time + R + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + + + Current Colour Temperature + R + Single + Mandatory + Unsigned Integer + 0..65535 + K + + + + Remaining Colour Temperature Transition Time + R + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Total Energy Usage + R + Single + Mandatory + Unsigned Integer + 0..18446744073709551615 + Ws + + + + Actual Power Usage + R + Single + Mandatory + Integer + 0..4294967295 + W + + + + Accuracy Class + R + Single + Optional + String + 0..64 bytes + + + + + Operating Hours + R + Single + Mandatory + Unsigned Integer + 0..4294967295 + h + + + + Adjusted Operating Hours + R + Single + Mandatory + Unsigned Integer + 0..4294967295 + + + + + Physical Minimum Intensity + R + Single + Optional + Float + 0.000001..1.0 + + + + + Minimum Intensity + RW + Single + Mandatory + Float + 0.000001..1.0 + + + + + Maximum Intensity + RW + Single + Mandatory + Float + 0.000001..1.0 + + + + + Physical Minimum Colour Temperature + R + Single + Optional + Unsigned Integer + 0..65535 + K + + + + Physical Maximum Colour Temperature + R + Single + Optional + Unsigned Integer + 0..65535 + K + + + + Minimum Colour Temperature + R + Single + Optional + Unsigned Integer + 0..65535 + K + + + + Maximum Colour Temperature + R + Single + Optional + Unsigned Integer + 0..65535 + K + + + + Power On Behaviour + RW + Single + Optional + Unsigned Integer + 0: CONFIGURED_ON + 1: FULL_ON + 2: RECALL + + + + + Power On Intensity + RW + Single + Optional + Float + 0.000001..1.0 + + + + + Stored Intensity + R + Single + Optional + Float + 0.000001..1.0 + + + + + System Failure Intensity + RW + Single + Optional + Float + 0.000001..1.0 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3398.xml b/application/src/main/data/lwm2m-registry/3398.xml new file mode 100644 index 0000000000..de3e24113d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3398.xml @@ -0,0 +1,107 @@ + + + + + + oA Physical Illuminance Sensor + + 3398 + urn:oma:lwm2m:ext:3398 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Min Range Value + R + Single + Optional + Unsigned Integer + 0..65535 + lx + + + + Max Range Value + R + Single + Optional + Unsigned Integer + 0..65535 + lx + + + + Error Status + R + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Mounting Location + RW + Single + Mandatory + String + 1..64 bytes + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3399.xml b/application/src/main/data/lwm2m-registry/3399.xml new file mode 100644 index 0000000000..8c29c4af82 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3399.xml @@ -0,0 +1,238 @@ + + + + + oA Physical Light-Point Actuator + + 3399 + urn:oma:lwm2m:ext:3399 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Error Status + R + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Mounting Location + RW + Single + Mandatory + String + 1..64 bytes + + + + + On/Off + RW + Single + Mandatory + Boolean + + + + + + Current Intensity + R + Single + Mandatory + Float + 0.0..1.0 + + + + + Remaining Transition Time + R + Single + Mandatory + Unsigned Integer + 0..65535 + ms + + + + Total Energy Usage + R + Single + Mandatory + Unsigned Integer + 0..18446744073709551615 + Ws + + + + Actual Power Usage + R + Single + Mandatory + Unsigned Integer + 0..4294967295 + W + + + + Accuracy Class + R + Single + Optional + String + 0..64 bytes + + + + + Operating Hours + R + Single + Mandatory + Unsigned Integer + 0..4294967295 + h + + + + Adjusted Operating Hours + R + Single + Mandatory + Unsigned Integer + 0..4294967295 + + + + + Physical Minimum Intensity + R + Single + Optional + Float + 0.000001..1.0 + + + + + Minimum Intensity + RW + Single + Mandatory + Float + 0.000001..1.0 + + + + + Maximum Intensity + RW + Single + Mandatory + Float + 0.000001..1.0 + + + + + Power On Behaviour + RW + Single + Optional + Unsigned Integer + 0: CONFIGURED_ON + 1: FULL_ON + 2: RECALL + + + + + Power On Intensity + RW + Single + Optional + Float + 0.000001..1.0 + + + + + Stored Intensity + R + Single + Optional + Float + 0.000001..1.0 + + + + + System Failure Intensity + RW + Single + Optional + Float + 0.000001..1.0 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3400.xml b/application/src/main/data/lwm2m-registry/3400.xml new file mode 100644 index 0000000000..aea142daba --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3400.xml @@ -0,0 +1,88 @@ + + + + + + + oA Physical Presence Sensor + + 3400 + urn:oma:lwm2m:ext:3400 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Error Status + R + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Mounting Location + RW + Single + Mandatory + String + 1..64 bytes + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3401.xml b/application/src/main/data/lwm2m-registry/3401.xml new file mode 100644 index 0000000000..1b088f1737 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3401.xml @@ -0,0 +1,120 @@ + + + + + + + oA Physical Push-Button Sensor + + 3401 + urn:oma:lwm2m:ext:3401 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Push-Button Status Value + R + Single + Mandatory + Unsigned Integer + 0: RELEASED + 1: PRESSED + 2: FAULT + + + + + Error Status + R + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Mounting Location + RW + Single + Mandatory + String + 1..64 bytes + + + + + Digital Input Polarity + RW + Single + Mandatory + Boolean + + + + + + Digital Input Debounce + RW + Single + Mandatory + Integer + + ms + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3402.xml b/application/src/main/data/lwm2m-registry/3402.xml new file mode 100644 index 0000000000..5cf405a66a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3402.xml @@ -0,0 +1,88 @@ + + + + + + + oA Receiving Object + + 3402 + urn:oma:lwm2m:ext:3402 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Executing Object + RW + Single + Mandatory + Corelnk + + + + + + Status Report + W + Single + Mandatory + Opaque + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3403.xml b/application/src/main/data/lwm2m-registry/3403.xml new file mode 100644 index 0000000000..f3690e5df5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3403.xml @@ -0,0 +1,118 @@ + + + + + + + oA Reporting Object + + 3403 + urn:oma:lwm2m:ext:3403 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Reporting Object Instances + RW + Multiple + Mandatory + Corelnk + + + + + + Target Resource + RW + Single + Mandatory + Corelnk + + + + + + Status Resend Time + RW + Single + Mandatory + Unsigned Integer + 1..600 + s + + + + Status Report Structure ID + RW + Single + Mandatory + Unsigned Integer + 1..600 + s + + + + Application Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + s + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3404.xml b/application/src/main/data/lwm2m-registry/3404.xml new file mode 100644 index 0000000000..24250ab608 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3404.xml @@ -0,0 +1,118 @@ + + + + + + + oA Scene + + 3404 + urn:oma:lwm2m:ext:3404 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Scene ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Application Group ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Changeable by User API + RW + Single + Mandatory + Boolean + + + + + + Transition Time + RW + Single + Mandatory + Unsigned Integer + 0..60000 + ms + + + + Scene Configuration + RW + Multiple + Mandatory + Opaque + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3405.xml b/application/src/main/data/lwm2m-registry/3405.xml new file mode 100644 index 0000000000..f6939917ae --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3405.xml @@ -0,0 +1,98 @@ + + + + + + + oA OGC-Security + + 3405 + urn:oma:lwm2m:ext:3405 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + OGC Security ID + RW + Single + Mandatory + Unsigned Integer + 0..65535 + + + + + Accepted Senders + RW + Multiple + Mandatory + Opaque + + + + + + Secret Group Key + W + Single + Mandatory + Opaque + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3406.xml b/application/src/main/data/lwm2m-registry/3406.xml new file mode 100644 index 0000000000..ccc7601eeb --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3406.xml @@ -0,0 +1,88 @@ + + + + + + + oA Status Report Structure + + 3406 + urn:oma:lwm2m:ext:3406 + 1.1 + 1.0 + Multiple + Optional + + + ObjectVersion + R + Single + Optional + String + + + + + + Documentary Description + RW + Single + Optional + String + 0..256 bytes + + + + + Status Report Structure ID + RW + Single + Mandatory + Unsigned Integer + 0..255 + + + + + Keys + RW + Multiple + Mandatory + String + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3407.xml b/application/src/main/data/lwm2m-registry/3407.xml new file mode 100644 index 0000000000..7f29324566 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3407.xml @@ -0,0 +1,98 @@ + + + + + + + Smoke Alarm + + 3407 + urn:oma:lwm2m:ext:3407 + 1.1 + 1.0 + Multiple + Optional + + + Battery Percentage + R + Single + Optional + Float + 0.00..100.00 + + + + + RSSI + R + Single + Optional + Float + + dBW + + + + Smoke Sensor State + R + Single + Mandatory + Boolean + + + + + + Detected CO2 percentage + R + Single + Optional + Float + 0.00..100.00 + /100 + + + + Alarm loudness + R + Single + Optional + Float + + dB + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3408.xml b/application/src/main/data/lwm2m-registry/3408.xml new file mode 100644 index 0000000000..4d5ff67eef --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3408.xml @@ -0,0 +1,79 @@ + + + + Manhole Cover + + 3408 + urn:oma:lwm2m:ext:3408 + 1.1 + 1.0 + Multiple + Optional + + + Safety Angle Threshold + R + Single + Mandatory + Integer + 0..180 + rad + + + + Manhole Cover Tilt + R + Single + Mandatory + Integer + 0..180 + rad + + + + Cover Open Status + R + Single + Mandatory + Boolean + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/3410.xml b/application/src/main/data/lwm2m-registry/3410.xml new file mode 100644 index 0000000000..63449e590b --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3410.xml @@ -0,0 +1,148 @@ + + + + + + + Device Extension + The uCIFI device extension is an extension to the standard Lightweight M2M device (object ID 3) using the resource called "ExtDevInfo" (resource ID 22). + 3410 + urn:oma:lwm2m:ext:3410 + 1.0 + 1.0 + Single + Optional + + + GTIN model number + R + Single + Optional + String + + + Standard global trade-in international number for the control device. + + + Manufacturer identifier + R + Single + Mandatory + String + + + Unique identifier of the manufacturer of the control device, equivalent to IEEE's OUI (Organizational Unique Identifier). This value should be a Hexadecimal value. This value may be used by an online uCIFI product catalog server, for software modules to retrieve the data model of a device using this attribute associated with the model number on the "Device" (Id=3) object. + + + User-given name + RW + Single + Optional + String + + + User-readable name of the device set by the user. + + + Asset identifier + RW + Single + Optional + String + + + Identifier of the asset (e.g. meter, luminaire, container) that is controlled by the device. + + + Installation date + RW + Single + Optional + Time + + + Installation date of the device. + + + Software update + R + Single + Optional + Boolean + + + Set to True while software within the device is being updated. + + + Maintenance + RW + Single + Optional + Boolean + + + Set to True when the device is in maintenance mode. + + + Configuration reset + E + Single + Optional + + + + Reset the configuration parameters (and only the configuration parameters) of the device. It does not clear any counter (e.g. kWh counter or operating hour counter). + + + Device operating hours + R + Single + Optional + Integer + + h + Cumulated number of hours during which the device has been powered on. + + + Additional firmware information + R + Single + Optional + String + + + Additional information about peripheral firmware versions. The format is left to the vendor. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3411.xml b/application/src/main/data/lwm2m-registry/3411.xml new file mode 100644 index 0000000000..6339b847bd --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3411.xml @@ -0,0 +1,168 @@ + + + + + + + Battery + The uCIFI battery object provides attributes to monitor battery level and activity. + 3411 + urn:oma:lwm2m:ext:3411 + 1.0 + 1.0 + Multiple + Optional + + + Battery level + R + Single + Mandatory + Integer + + /100 + Level of charge of the battery in % of total possible charge. + + + Battery capacity + R + Single + Optional + Float + + Ah + Nominal capacity of the battery in Ampere hour. + + + Battery voltage + R + Single + Optional + Float + + V + Level of charge of the battery in Volts. + + + Type of battery + RW + Single + Optional + String + + + Describes the type of battery (e.g. Li-Ion rechargeable). + + + Low battery threshold + RW + Single + Optional + Integer + + /100 + Threshold below which an event is generated by the device. + + + Battery level too low + R + Single + Optional + Boolean + + + Set to True if battery level is below or equal the low battery threshold. Set to False otherwise. + + + Battery shutdown + RW + Single + Optional + Boolean + + + Indicates that the device has shut down due to battery discharge. Can be reset by central application. + + + Number of cycles + R + Single + Optional + Integer + + + Number of times the battery has discharged and recharged. + + + Supply loss + R + Single + Optional + Boolean + + + Applies for battery devices that are also connected to mains supply. Set to True when mains power is lost. Set to False when mains power is back. + + + Supply loss counter + R + Single + Optional + Integer + + + Number of supply losses since last reset. + + + Supply loss counter reset + E + Single + Optional + + + + Reset the supply loss counter. + + + Supply loss reason + R + Single + Optional + String + + + Reason identified by the device why the device has lost mains supply (e.g. lightning if the device measured a high voltage, accident if the device identified a move with an accelerometer). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3412.xml b/application/src/main/data/lwm2m-registry/3412.xml new file mode 100644 index 0000000000..c3675639da --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3412.xml @@ -0,0 +1,288 @@ + + + + + + + LPWAN Communication + The uCIFI communication object provides attributes related to the communication on the LPWAN network, including multicast grouping. + 3412 + urn:oma:lwm2m:ext:3412 + 1.0 + 1.0 + Multiple + Optional + + + Type of network + R + Single + Optional + String + + + Type of LPWAN communication network (e.g. LoRaWAN, NB-IoT, wireless mesh, power line). + + + IPv4 address + RW + Multiple + Optional + String + + + Device’s IPv4 address. + + + IPv6 address + RW + Multiple + Optional + String + + + Device’s IPv6 address. + + + Network address + RW + Multiple + Optional + String + + + Address of the device on the LPWAN network. + + + Secondary network address + RW + Multiple + Optional + String + + + Secondary address used to communicate with the device on the LPWAN network. + + + MAC address + RW + Single + Mandatory + String + + + IEEE MAC address of the device. + + + Peer address + R + Multiple + Optional + String + + + Address of a peer (e.g. a router, a mesh node, a gateway). + + + Multicast group address + RW + Multiple + Optional + String + + + Group address from which the device should accept incoming messages and/or commands. + + + Multicast group key + RW + Multiple + Optional + String + + + Security key (e.g. AES128) to be shared with other members to be part of a multicast group. + + + Data rate + RW + Single + Optional + Integer + + B/s + Data rate of the communication used on the LPWAN network. + + + Transmit power + R + Single + Optional + Float + + dBm + Transmit power (also called TxPower) used by the device on the LPWAN network in decibel per milliwatt. + + + Frequency + RW + Single + Optional + Float + + Hz + Frequency of the wireless communication used on the LPWAN network. + + + Session time + RW + Single + Optional + Time + + + Starting time of the multicast session. The device shall not accept incoming messages before this time or after this time + session duration. + + + Session duration + R + Single + Optional + Time + + s + Duration of the multicast session. The device shall accept incoming messages only during this session duration time, starting at "Session time". + + + Mesh node + RW + Single + Optional + Boolean + + + Set to True if the device is a mesh node that should repeat incoming messages on a mesh network. + + + Maximum repeat time + RW + Single + Optional + Integer + + + Maximum number of times a message should be repeated within a mesh network. + + + Number of repeats + R + Single + Optional + Integer + + + Number of mesh nodes between the device and the destination device (e.g. another device or a router) including the destination node, on a mesh network. + + + Signal to noise ratio + R + Single + Optional + Float + + dB + Ratio of signal power to the noise power. + + + Communication failure + R + Single + Optional + Boolean + + + Set to True when the device can't communicate properly. + + + Received Signal Strength Indication + R + Single + Optional + Float + + dBm + Signal strength of the communication network measured by the device (a.k.a. RSSI or signal level). + + + IMSI + R + Single + Optional + String + + + Device’s International Mobile Subscriber Identity. + + + IMEI + R + Single + Optional + String + + + Device’s International Mobile Equipment Identity. + + + Current Communication Operator + R + Single + Optional + String + + + Device’s current communication operator. Communication operators could be private operators. Thus this resource is free text format and does not use Mobile Network Code or equivalent. + + + Integrated Circuit Card Identifier + R + Single + Optional + String + + + Unique identifier used to identify a communication SIM card. The ICCID is defined by the ITU-T recommendation E.118 as the Primary Account Number, composed of 19 or 20 characters containing the ISO Industry Identifier, country code, issuer identity, account ID, and other data which allows the network operator to identify the card. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3413.xml b/application/src/main/data/lwm2m-registry/3413.xml new file mode 100644 index 0000000000..e699cb67dd --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3413.xml @@ -0,0 +1,108 @@ + + + + + + + uCIFI Generic Actuator + The uCIFI generic actuator may be used for any actuator but not to replace Outdoor Lamp Controllers nor for an Irrigation Valves which have a specific device type in the uCIFI data model. + 3413 + urn:oma:lwm2m:ext:3413 + 1.0 + 1.0 + Multiple + Optional + + + Default dimming level + RW + Single + Optional + Integer + 0..100 + /100 + Default dimming level that the generic actuator applies when the device is powered ON. + + + Dimming level + R + Single + Optional + Integer + 0..100 + /100 + Actual dimming level (0 for OFF and 100% for ON) measured on the actuator. + + + Command + RW + Single + Optional + Integer + 0..100 + /100 + Command (e.g. manual override, scheduler) sent to the actuator. + + + Command in action + R + Single + Optional + Integer + 0..100 + /100 + For devices connected on slow LPWAN networks, the command in action (this resource) may differ from a command that was sent (resource ID: 3). The command in action is the actual value of the command in action in the actuator. + + + Scheduler ID + RW + Multiple + Optional + Integer + + + The identifier of the schedulers that are assigned to the Command of this actuator. + + + Invalid scheduler + R + Single + Optional + Boolean + + + Set to True when one of the schedulers can’t be executed or is not supported by the actuator. Otherwise set to False. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3414.xml b/application/src/main/data/lwm2m-registry/3414.xml new file mode 100644 index 0000000000..b3f7ad4567 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3414.xml @@ -0,0 +1,108 @@ + + + + + + + uCIFI data bridge + The uCIFI data bridge object may be used to transport data from a 3rd party device, operating a proprietary network or protocol, over a LwM2M compliant network. + 3414 + urn:oma:lwm2m:ext:3414 + 1.0 + 1.0 + Multiple + Optional + + + Payload + RW + Single + Mandatory + String + + + Content of the message that is to be transported. + + + Hash + RW + Single + Optional + String + + + Hash to check the consistency of the data payload. Hash mechanism and calculation is vendor-specific. + + + Cumulated daily data volume up + R + Single + Optional + Integer + + B + Cumulated volume of data received by the device since beginning of the day. This information could be used for data invoicing. + + + Cumulated daily data volume down + R + Single + Optional + Integer + + B + Cumulated volume of data sent by the device since beginning of the day. This information could be used for data invoicing. + + + Cumulated daily data volume total + R + Single + Optional + Integer + + B + Cumulated volume of data sent and received by/from the device since beginning of the day. This information could be used for data invoicing. + + + Communication error + R + Single + Optional + Boolean + + + Set to True if the device detects a difference in a received payload and the hash. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3415.xml b/application/src/main/data/lwm2m-registry/3415.xml new file mode 100644 index 0000000000..5a76ca93a3 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3415.xml @@ -0,0 +1,98 @@ + + + + + + + Time synchronization + The uCIFI time synchronization object enables a device to sync-up its internal clock with a remote NTP server. + 3415 + urn:oma:lwm2m:ext:3415 + 1.0 + 1.0 + Single + Optional + + + NTP server address + RW + Single + Mandatory + String + + + Hostname or IP address of the Network Time Protocol server. + + + Backup NTP server address + RW + Single + Optional + String + + + Hostname or IP address of a backup NTP server to be used in case the main NTP server does not respond. + + + NTP period + RW + Single + Optional + String + + + Number of hours before which the device tries to reach the NTP server for time synchronization. + + + Last time sync + R + Single + Optional + Integer + + h + Last time at which a successful time synchronization occurred. + + + Time sync error + R + Single + Optional + Boolean + + + Set to True in case the latest time synchronization operation failed. Set to False in case the last operation succeeded. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3416.xml b/application/src/main/data/lwm2m-registry/3416.xml new file mode 100644 index 0000000000..bf9f23b6ac --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3416.xml @@ -0,0 +1,518 @@ + + + + + + + Outdoor lamp controller + The uCIFI outdoor lamp controller object provides attributes to control, command and monitor outdoor luminaires in streets, in tunnels, on roads and for illumination of park and gardens. + 3416 + urn:oma:lwm2m:ext:3416 + 1.0 + 1.0 + Multiple + Optional + + + Command + RW + Single + Mandatory + Integer + 0..100 + /100 + Command (e.g. manual override, scheduler) sent to the outdoor lamp controller. + + + Command in action + R + Single + Optional + Integer + 0..100 + /100 + For outdoor lighting applications, the command in action (this resource) may differ from a command that was sent (resource ID: 1), due to LPWAN network constraints and/or light adjustments within the lamp’s control gear (e.g. virtual power settings). The command in action is the actual value of the command in action in the outdoor lamp controller. + + + Dimming level + R + Single + Mandatory + Integer + 0..100 + /100 + Dimming level (0 for OFF and 100% for ON) measured on the outdoor lamp controller. + + + Default dimming level + RW + Single + Optional + Integer + 0..100 + /100 + The default dimming level that the outdoor lamp controller applies when the device is powered ON. + + + Lamp failure + R + Single + Optional + Boolean + + + Set to True if the outdoor lamp controller detects that the lamp is not producing light while it is expected to (e.g. the lamp is broken). + + + Lamp failure reason + R + Single + Optional + Integer + + + Description of the reason why the lamp failed (e.g. low power on a LED engine, no consumption detected while relay closed). + + + Control gear failure + R + Single + Optional + Boolean + + + Set to True in case the control gear has a failure. Outdoor lamp controllers may read the control gear failure from a DALI bus or by analyzing a 0-10 volts interface to the control gear. + + + Control gear failure reason + R + Single + Optional + Integer + + + Description of the reason why the control gear failed. You may refer to the DiiA list of possible control gear failures. + + + Relay failure + R + Single + Optional + Boolean + + + Set to True if the outdoor lamp controller detects that its relay is not operating as it is expected to. + + + Day burner + R + Single + Optional + Boolean + + + Set to True in case the lamp is ON while it should be OFF (e.g. day burner). + + + Cycling failure + R + Single + Optional + Boolean + + + Set to True if the outdoor lamp controller detects that the lamp is switching ON and OFF too often. + + + Control gear communication failure + R + Single + Optional + Boolean + + + Set to True in case the control gear (e.g. LED driver) does not answer on the DALI bus. + + + Scheduler ID + RW + Multiple + Optional + Integer + + + Identifier(s) of the scheduler(s) that are assigned to the Command of this outdoor lamp controller. + + + Invalid scheduler + R + Single + Optional + Boolean + + + Set to True when one of the schedulers can’t be executed or is not supported by the outdoor lamp controller. Otherwise set to False. + + + Lamp operating hours + R + Single + Optional + Integer + + h + Cumulated number of hours during which the lamp has been ON with a strictly positive dimming level. + + + Lamp operating hours reset + E + Single + Optional + + + + Reset of the lamp operating hours counter. + + + Lamp ON timestamp + R + Single + Optional + Time + + + Last date and time at which the lamp switched ON, i.e. from no light to light (e.g. power off to power on and/or dim level = 0 to dim level > 0). + + + Lamp switch counter + R + Single + Optional + Integer + + + Number of times the lamp was switched from ON to OFF since the last lamp switch counter reset. + + + Lamp switch counter reset + E + Single + Optional + + + + Reset the lamp switch counter. + + + Control gear start counter + R + Single + Optional + Integer + + + Number of times the control gear was switched from ON to OFF. + + + Control gear temperature + R + Single + Optional + Float + + Cel + Temperature measured by the control gear and transmitted to the device through DALI, Zhaga D4i or equivalent. + + + Control gear thermal derating + R + Single + Optional + Boolean + + + Set to True if the control gear has derated the light source due to high temperature. + + + Control gear thermal derating counter + R + Single + Optional + Integer + + + Number of time the control gear has derated the light source due to a high temperature, since last counter reset. + + + Control gear thermal derating counter reset + E + Single + Optional + + + + Reset of the control gear thermal derating counter. + + + Control gear thermal shutdown + R + Single + Optional + Boolean + + + Set to True if the control gear has shut the light source down due to high temperature. + + + Control gear thermal shutdown counter + R + Single + Optional + Integer + + + Number of times the control gear has shutdown the light source since last counter reset. + + + Control gear thermal derating counter reset + E + Single + Optional + + + + Reset of the control gear shutdown counter. + + + Output port + RW + Single + Optional + Integer + + + Address or reference of the output port (e.g. DALI port address or 1-10 volt output) in case of multiple control gears. + + + Standby mode + RW + Single + Optional + Boolean + + + Set to True if the outdoor lamp controller should keep its relay closed (control gear is powered ON) when command and/or dimming level is equal to 0. Set to False if the outdoor lamp controller should open its relay (control gear is not powered ON) when command and/or dimming level is equal to 0. + + + Constant light output + RW + Single + Optional + Boolean + + + Set to True to activate Constant Light Output dimming correction on the outdoor lamp controller. Set to False to deactivate Constant Light Output. + + + Cleaning factor enabled + RW + Single + Optional + Boolean + + + Light output of a luminaire may depend on the lamp cleaning factor. Cleaning factor may be used as a light output compensation. Set to True to activate lamp cleaning correction on the outdoor lamp controller. Set to False to deactivate the lamp cleaning correction. + + + Cleaning period + RW + Single + Optional + Integer + + + Number of days after which cleaning factor is back to 100%. + + + Initial lamp cleaning factor + RW + Single + Optional + Integer + 0..100 + /100 + Initial lamp cleaning correction factor to multiply to the command when the luminaire is cleaned, at the lamp cleaning date. + + + Lamp cleaning date + RW + Single + Optional + Time + + + Date at which the luminaire was last (or will be next) cleaned and the lamp cleaning factor should be set to the initial lamp cleaning factor. + + + Control type + RW + Single + Optional + Integer + 0..13 + + Type of control system with whioch the outdoor lamp controller switches, dims and monitors the lamp. The possible control types are: 0: No dimming control, 1 : DALI part 201 - Device Type 0, 2 : DALI part 202 - Device Type 1, 3 : DALI part 203 - Device Type 2, 4 : DALI part 204 - Device Type 3, 5 : DALI part 205 - Device Type 4, 6 : DALI part 206 - Device Type 5, 7 : DALI part 207 - Device Type 6, 8 : DALI part 208 - Device Type 7, 9 : DALI part 209 - Device Type 8, 10 : 0-10V, 11 : PWM, 12 : PWM_N, 13 : Other + + + Nominal Lamp wattage + RW + Single + Optional + Integer + + W + Active power of the light source at 100% dimming level. + + + Minimum dimming level + RW + Single + Optional + Integer + + /100 + Minimum dimming level under which the lamp will most probably not operate. + + + Minimum lamp wattage + RW + Single + Optional + Integer + + W + Expected active power of the light source at its minimum dimming level. This value may be used to detect failure at low dimming level. + + + Light color temperature command + RW + Single + Optional + String + + K + Latest light color temperature command sent to the lamp actuator. + + + Actual light color temperature + R + Single + Optional + String + + K + The actual light color temperature of the light source. + + + Virtual power output + RW + Single + Optional + Integer + 0..100 + /100 + Percentage of nominal power at which the light source should be set when the Command is set to 100%. + + + Voltage at max dim level + RW + Single + Optional + Float + + V + Voltage level on the control port that corresponds to maximum dimming level. This applies only if Control Type = 0-10V. + + + Voltage at min dim level + RW + Single + Optional + Float + + V + Voltage level on the control port that corresponds to minimum dimming level. This applies only if Control Type = 0-10V. + + + Light source voltage + R + Single + Optional + Float + + V + Voltage (usually DC voltage) to the light source or generic load, measured at the output of the control gear. + + + Light source current + R + Single + Optional + Float + + A + Current (usually DC current) delivered by the control gear to the light source or generic load, measured at the output of the control gear. + + + Light source active power + R + Single + Optional + Float + + W + Active power of the light source or generic load, measured at the output of the control gear. + + + Light source active energy + R + Single + Optional + Float + + kWh + Cumulated active energy consumption of the light source or generic load, measured at the output of the control gear. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3417.xml b/application/src/main/data/lwm2m-registry/3417.xml new file mode 100644 index 0000000000..d22a318d84 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3417.xml @@ -0,0 +1,188 @@ + + + + + + + Luminaire asset + The uCIFI luminaire asset is an object that enables outdoor lighting control software to configure outdoor lamp controllers. It also enables outdoor lamp controllers to send Zhaga D4i or DALI attributes read from the control gears, to centrally managed outdoor lighting control software or asset management systems. + 3417 + urn:oma:lwm2m:ext:3417 + 1.0 + 1.0 + Multiple + Optional + + + Asset GTIN + R + Single + Mandatory + String + + + Global trade-in international number used in the Luminaire Asset controlled by the device. + + + Year of manufacture + RW + Single + Optional + Integer + + + Year of manufacture of the luminaire. + + + Week of manufacture + RW + Single + Optional + Integer + + + Week of manufacture of the luminaire. + + + Nominal light output + RW + Single + Optional + Integer + + lm + Nominal light output of the luminaire. + + + Light distribution type + RW + Single + Optional + Integer + + + Enumeration of possible light distribution type, using the Zhaga D4i enumeration. Please refer to ZD4i standard for more details. + + + Luminaire color + RW + Single + Optional + String + + + Painting color of the luminaire. + + + Nominal input power + R + Single + Optional + Float + + W + Nominal input power of the luminaire. + + + Power at minimum dim level + R + Single + Optional + Float + + W + Power at minimum dim level for the luminaire. + + + Nominal max AC mains voltage + R + Single + Optional + Integer + + V + Nominal max AC mains voltage for the luminaire to operate. + + + Nominal min AC mains voltage + R + Single + Optional + Integer + + V + Nominal min AC mains voltage for the luminaire to operate. + + + CRI + R + Single + Optional + Integer + 0..100 + + Color rendering index (0 to 100) of the luminaire. + + + CCT value + R + Single + Optional + Integer + + K + Color temperature of the luminaire. + + + Luminaire identification + R + Single + Optional + String + + + Luminaire identification as per DiiA/D4i specification part 251 (MB1 extension): 60 ascii character string. + + + Luminaire identification number + R + Single + Optional + String + + + Luminaire identification number as per DiiA/D4i specification part 251 (MB1 extension): 20 digit number. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3418.xml b/application/src/main/data/lwm2m-registry/3418.xml new file mode 100644 index 0000000000..6ff07dfa94 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3418.xml @@ -0,0 +1,348 @@ + + + + + + + Electrical monitor + The uCIFI electrical monitor object provides attributes related to the analysis of electrical consumption in an outdoor luminaire or in a streetlight cabinet. It also identifies electrical anomalies. + 3418 + urn:oma:lwm2m:ext:3418 + 1.0 + 1.0 + Multiple + Optional + + + Supply voltage + R + Single + Optional + Float + + V + Electrical voltage supplied to the device. + + + Supply current + R + Single + Optional + Float + + A + Electrical current supplied to the device. + + + Frequency + R + Single + Optional + Float + + Hz + Frequency of the supply current to the device. + + + Active power + R + Single + Optional + Float + + W + Active power consumed by the device and its electrical load. + + + Power factor + R + Single + Optional + Float + -1..1 + + Power factor is equal to active power divided by apparent power. The value is between -1 and +1. + + + Cumulated active energy + R + Single + Optional + Float + + kWh + Cumulated number of kWh measured by the device and its load since last energy counter reset. + + + Energy reset + E + Single + Optional + + + + Reset both cumulated active and reactive energy counter. + + + Low power factor threshold + RW + Single + Optional + Float + + + Threshold below which the device should trigger a low power factor event. + + + Low power factor + R + Single + Optional + Boolean + + + Set to True if the power factor is below threshold. This is an absolute value threshold. + + + Low power threshold + RW + Single + Optional + Float + + W + Threshold below which the device should trigger a low power event. + + + Low power threshold at low dim level + RW + Single + Optional + Float + + W + Threshold below which the device should trigger a low power event. This is an addition to the low power threshold to allow a separated threshold when dim level is low. + + + Low power + R + Single + Optional + Boolean + + + Set to True if the power is below threshold. Vendor may consider the Low power threshold at low dim level to set this resource to True, taking into account the lamp dim level. + + + High power threshold + RW + Single + Optional + Float + + W + Threshold above which the device should trigger a high power event. + + + High power threshold at low dim level + RW + Single + Optional + Float + + W + Threshold above which the device should trigger a high power event. This is an addition to the high power threshold to allow a separated threshold when dim level is low. + + + High power + R + Single + Optional + Boolean + + + Set to True if the power is above threshold. Vendor may consider the High power threshold at low dim level to set this resource to True, taking into account the lamp dim level. + + + Low current threshold + RW + Single + Optional + Float + + A + Threshold below which the device should trigger a low current event. + + + Low current + R + Single + Optional + Boolean + + + Set to True if the current is below threshold. + + + High current threshold + RW + Single + Optional + Float + + A + Threshold above which the device should trigger a high current event. + + + High current + R + Single + Optional + Boolean + + + Set to True if the current is above threshold. + + + Low voltage threshold + RW + Single + Optional + Float + + V + Threshold below which the device should trigger a low voltage event. + + + Low voltage + R + Single + Optional + Boolean + + + Set to True if the voltage is below threshold. + + + High voltage threshold + RW + Single + Optional + Float + ON;OFF + V + Threshold above which the device should trigger a high voltage event. + + + High voltage + R + Single + Optional + Boolean + + + Set to True if the voltage is above threshold. + + + Critical inrush threshold + RW + Single + Optional + Float + + A + Threshold above which the device should trigger a critical inrush event. + + + Critical inrush current + R + Single + Optional + Boolean + + + Set to True if the inrush current is above threshold. + + + Minimum inrush Current + R + Single + Optional + Float + + A + Minimum inrush current since last configuration. + + + Maximum inrush Current + R + Single + Optional + Float + + A + Maximum inrush current since last configuration. + + + Latest inrush Current + R + Single + Optional + Float + + A + Latest inrush current measured since last time the relay switched ON. + + + Reactive power + R + Single + Optional + Float + + var + Instantaneous reactive power measured by the device and its electrical load. + + + Reactive energy + R + Single + Optional + Float + + varh + Cumulative reactive power measured by the device and its electrical load since last energy counter reset. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3419.xml b/application/src/main/data/lwm2m-registry/3419.xml new file mode 100644 index 0000000000..88b66119a7 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3419.xml @@ -0,0 +1,78 @@ + + + + + + + Photocell + A uCIFI photocell object is used for lighting control, particularly to identify when light should be switched ON/OFF. + 3419 + urn:oma:lwm2m:ext:3419 + 1.0 + 1.0 + Multiple + Optional + + + ON lux level + RW + Single + Optional + Float + + lx + Lux level below which the photocell switches its relay ON or sends a switch ON message on the network. + + + OFF lux level + RW + Single + Optional + Float + + lx + Lux level above which the photocell switches its relay OFF or sends a switch OFF message on the network. + + + Photocell status + R + Single + Optional + Boolean + + + Set to True if lux level is below ON lux level. Set to False if lux level is above OFF lux. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3420.xml b/application/src/main/data/lwm2m-registry/3420.xml new file mode 100644 index 0000000000..835f4ec8a5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3420.xml @@ -0,0 +1,58 @@ + + + + + + + LED color light + The uCIFI LED color light enables to control light colors of a LED luminaire. + 3420 + urn:oma:lwm2m:ext:3420 + 1.0 + 1.0 + Multiple + Optional + + + RGB value + RW + Single + Mandatory + String + + + Text string according to the RBG hexadecimal format with # (e.g. #FF0000 for 100% red). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3421.xml b/application/src/main/data/lwm2m-registry/3421.xml new file mode 100644 index 0000000000..dadf8d8f47 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3421.xml @@ -0,0 +1,618 @@ + + + + + + + Single-phase electrical meter + The uCIFI single-phase electrical meter measures the electrical consumption of loads on one electrical line, such as electrical cabinets in city infrastructures, street lighting networks, homes and buildings. + 3421 + urn:oma:lwm2m:ext:3421 + 1.0 + 1.0 + Multiple + Optional + + + Voltage + R + Single + Mandatory + Float + + V + Instantaneous voltage measured between line and neutral. + + + Low voltage threshold + R + Single + Optional + Float + + V + Set to True when voltage is below low voltage threshold. + + + Low voltage + R + Single + Optional + Boolean + + + Set to True when voltage is below low voltage threshold. + + + High voltage threshold + R + Single + Optional + Float + + V + Threshold above which the high voltage resource is set to True. + + + High voltage + R + Single + Optional + Boolean + + + Set to True when voltage is above high voltage threshold. + + + Current + R + Single + Mandatory + Float + + A + Instantaneous current measured. + + + Low current threshold + R + Single + Optional + Float + + A + Threshold below which the low current resource is set to True. + + + Low current + R + Single + Optional + Boolean + + + Set to True when current is below low current threshold. + + + High current threshold + R + Single + Optional + Float + + V + Threshold above which the high current resource is set to True. + + + High current + R + Single + Optional + Boolean + + + Set to True when current is above high current threshold. + + + Frequency + R + Single + Optional + Float + + Hz + Instantaneous frequency measured. + + + Angle of I-U + R + Single + Optional + Float + + deg + Instantaneous phase angle measured. + + + Instantaneous Power Factor + R + Single + Mandatory + Float + + + Instantaneous power factor overall. + + + Low power factor threshold + R + Single + Optional + Float + + + Threshold below which the low power factor resource is set to True. + + + Low power factor + R + Single + Optional + Boolean + + + Set to True when power factor is below low power factor threshold. + + + Active power+ (QI+QIV) + R + Single + Mandatory + Float + + W + Active power import + + + Active power- (QII+QIII) + R + Single + Optional + Float + + W + Active power export. + + + Reactive power+ (QI+QII) + R + Single + Optional + Float + + var + Reactive power import. + + + Reactive power- (QIII+QIV) + R + Single + Optional + Float + + var + Reactive power export + + + Instantaneous Apparent Power import (+VA) + R + Single + Optional + Float + + VA + Instantaneous apparent power import. + + + Instantaneous Apparent Power export (-VA) + R + Single + Optional + Float + + VA + Instantaneous apparent power export. + + + Instantaneous Active Power (|+A|+|-A|) + R + Single + Optional + Float + + W + Instantaneous active power. + + + Instantaneous Net Active Power (|+A|-|-A|) + R + Single + Optional + Float + + W + Instantaneous net active power. + + + Measurement period of Instantaneous value + R + Single + Optional + Float + + s + Measurement period of instantaneous value. + + + Active energy import (+A) Un + R + Single + Mandatory + Float + + Wh + Active energy import (unified rate). + + + Active energy export (-A) Un + R + Single + Optional + Float + + Wh + Active energy export (unified rate). + + + Active energy (|+A|+|-A|) Combined total + R + Single + Optional + Float + + Wh + Active energy (|+A|+|-A|) Combined total. + + + Active energy (|+A|-|-A|) Combined total + R + Single + Optional + Float + + Wh + Active energy (|+A|-|-A|) Combined total. + + + Reactive energy import (+R) Un + R + Single + Optional + Float + + varh + Reactive energy import (unified rate) + + + Reactive energy export (-R) Un + R + Single + Optional + Float + + varh + Reactive energy export (unified rate). + + + Reactive energy QI (+Ri) Un + R + Single + Optional + Float + + varh + Reactive energy QI (+Ri) (unified rate). + + + Reactive energy QII (+Rc) Un + R + Single + Optional + Float + + varh + Reactive energy QII (+Rc) (unified rate). + + + Reactive energy QIII (-Ri) Un + R + Single + Optional + Float + + varh + Reactive energy QIII (-Ri) (unified rate). + + + Reactive energy QIV (-Rc) Un + R + Single + Optional + Float + + varh + Reactive energy QIV (-Rc) (unified rate). + + + Apparent energy import Un + R + Single + Optional + Float + + VAh + Apparent energy import (unified rate). + + + Apparent energy export Un + R + Single + Optional + Float + + VAh + Apparent energy export (unified rate). + + + Number of power failures + R + Single + Optional + Float + + + Number of power failures. + + + Number of long power failures + R + Single + Optional + Float + + + Number of long power failures. + + + Time threshold for long power failure + RW + Single + Optional + Float + + s + Time threshold for long power failure. + + + Duration of last long power failure + R + Single + Optional + Float + + s + Duration of last long power failure. + + + Threshold for voltage sag + RW + Single + Optional + Float + + V + Threshold for voltage sag. + + + Time threshold for voltage sag + RW + Single + Optional + Float + + s + Time threshold for voltage sag. + + + Number of voltage sags + R + Single + Optional + Float + + + Number of voltage sags. + + + Duration of last voltage sag + R + Single + Optional + Float + + s + Duration of last voltage sag. + + + Magnitude of last voltage sag + R + Single + Optional + Float + + V + Magnitude of last voltage sag. + + + Threshold for voltage swell + RW + Single + Optional + Float + + V + Threshold for voltage sag. + + + Time threshold for voltage swell + RW + Single + Optional + Float + + s + Time threshold for voltage sag. + + + Number of voltage swells + R + Single + Optional + Float + + + Number of voltage sags. + + + Duration of last voltage swell + R + Single + Optional + Float + + s + Duration of last voltage sag. + + + Magnitude of last voltage swell + R + Single + Optional + Float + + V + Magnitude of last voltage swell. + + + Threshold for missing voltage (voltage cut) + RW + Single + Optional + Float + + V + Threshold for missing voltage (voltage cut). + + + Time threshold for voltage cut + RW + Single + Optional + Float + + s + Time threshold for voltage cut. + + + Voltage cut + R + Single + Optional + Boolean + + + Set to True if a voltage cut for a time above the time threshold and for a voltage below the voltage threshold is detected. + + + CT Numerator Parameter + RW + Single + Optional + Float + + + Transformer ratio - current (numerator). + + + CT Denominator Parameter + RW + Single + Optional + Float + + + Transformer ratio - voltage (denominator). + + + VT Numerator Parameter + RW + Single + Optional + Float + + + Transformer ratio - current (numerator). + + + VT Denominator Parameter + RW + Single + Optional + Float + + + Transformer ratio - voltage (denominator). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3423.xml b/application/src/main/data/lwm2m-registry/3423.xml new file mode 100644 index 0000000000..6e0aaf3b21 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3423.xml @@ -0,0 +1,158 @@ + + + + + + + Gas meter + The uCIFI gas meter measures the volume of gas that was distributed through the gas meter, in pulse count and/or in m3. It also detects anomalies in the gas meter. + 3423 + urn:oma:lwm2m:ext:3423 + 1.0 + 1.0 + Multiple + Optional + + + Cumulated gas volume + R + Single + Mandatory + Float + + m3 + Number of cubic meters of gas distributed through the meter since last reset. + + + Cumulated gas meter value reset + E + Single + Optional + + + + Reset the cumulated meter value. + + + Type of meter + RW + Single + Optional + String + + + Type of gas meter. + + + Cumulated pulse value + R + Single + Optional + Integer + + + Cumulated number of pulses detected on the gas meter. + + + Cumulated pulse value reset + E + Single + Optional + + + + Reset the cumulated pulse value. + + + Pulse ratio + RW + Single + Optional + Integer + + + Ratio to calculate gas volume from pulse value. + + + Minimum flow rate + R + Single + Optional + Float + + m3/s + Minimum flow rate since last metering value. + + + Maximum flow rate + R + Single + Optional + Float + + m3/s + Maximum flow rate since last metering value. + + + Leak suspected + R + Single + Optional + Boolean + + + Set to True if gas leak is suspected. + + + Leak detected + R + Single + Optional + Boolean + + + Set to True if gas leak is detected. + + + High temperature + R + Single + Optional + Boolean + + + Set to True if high temperature is detected around the gas meter. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3424.xml b/application/src/main/data/lwm2m-registry/3424.xml new file mode 100644 index 0000000000..01df43cbaf --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3424.xml @@ -0,0 +1,178 @@ + + + + + + + Water meter + The uCIFI water meter measures water volume that was distributed through a water meter, in pulse count as well as in m3. It also detects anomalies in the water meter. + 3424 + urn:oma:lwm2m:ext:3424 + 1.0 + 1.0 + Multiple + Optional + + + Cumulated water volume + R + Single + Mandatory + Float + + m3 + Number of cubic meters of water distributed through the meter since last reset. + + + Cumulated water meter value reset + E + Single + Optional + + + + Reset the cumulated meter value. + + + Type of meter + RW + Single + Optional + String + + + Type of water meter. + + + Cumulated pulse value + R + Single + Optional + Integer + + + Cumulated number of pulses detected on the meter. + + + Cumulated pulse value reset + E + Single + Optional + + + + Reset the cumulated pulse value. + + + Pulse ratio + RW + Single + Optional + Integer + + + Ratio to calculate water volume from pulse value. + + + Minimum flow rate + R + Single + Optional + Float + + m3/s + Minimum flow rate since last metering value. + + + Maximum flow rate + R + Single + Optional + Float + + m3/s + Maximum flow rate since last metering value. + + + Leak suspected + R + Single + Optional + Boolean + + + Set to True if water leak is suspected. + + + Leak detected + R + Single + Optional + Boolean + + + Set to True if leak is detected. + + + Back flow detected + R + Single + Optional + Boolean + + + Set to True if water back flow is detected. + + + Blocked meter + R + Single + Optional + Boolean + + + Set to True if water meter is blocked. + + + Fraud detected + R + Single + Optional + Boolean + + + Set to True if fraud is detected. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3425.xml b/application/src/main/data/lwm2m-registry/3425.xml new file mode 100644 index 0000000000..dd94f14dfd --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3425.xml @@ -0,0 +1,108 @@ + + + + + + + Irrigation valve + The uCIFI irrigation valve provides a way to control and monitor an irrigation valve. + 3425 + urn:oma:lwm2m:ext:3425 + 1.0 + 1.0 + Multiple + Optional + + + Default status + RW + Single + Optional + Integer + 0..100 + /100 + Default status (level of opening of the valve from 0 to 100%) that is applied when the device is powered ON. + + + Status + R + Single + Optional + Integer + 0..100 + /100 + Actual level of opening of the valve (from 0 to 100%). + + + Command + RW + Single + Optional + Integer + 0..100 + /100 + Command (e.g. manual override, scheduler) sent to the irrigation valve. + + + Command in action + R + Single + Optional + Integer + 0..100 + /100 + For devices connected on slow LPWAN networks, the command in action (this resource) may differ from a command that was sent (resource ID: 3). The command in action is the actual value of the command in action on the irrigation valve. + + + Scheduler ID + RW + Multiple + Optional + Integer + + + Identifiers of the schedulers that are assigned to the Command of this irrigation valve. + + + Invalid scheduler + R + Single + Optional + Boolean + + + Set to True when one of the schedulers can’t be executed or is not supported by this irrigation valve. Otherwise set to False. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3426.xml b/application/src/main/data/lwm2m-registry/3426.xml new file mode 100644 index 0000000000..7f33c1bc1a --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3426.xml @@ -0,0 +1,188 @@ + + + + + + + Water quality sensor + The uCIFI water quality sensor measures the quality of the water in the drinkable water distribution network, in water tanks or in lakes and rivers. + 3426 + urn:oma:lwm2m:ext:3426 + 1.0 + 1.0 + Multiple + Optional + + + pH + R + Single + Optional + Float + + + Current or last value of the pH measured by the sensor. + + + Chlorine + R + Single + Optional + Float + + ppm + Current or last value of the chlorine measured by the sensor. + + + Redox or ORP + R + Single + Optional + Float + + V + Current or last value of the oxidation reduction potential measured by the sensor. + + + Total dissolved gas or TDG + R + Single + Optional + Float + + ppm + Current or last value of the dissolved gas measured by the sensor. + + + Dissolved oxygen + R + Single + Optional + Float + + ppm + Current or last value of the dissolved oxygen measured by the sensor. + + + Turbidity + R + Single + Optional + Float + + NTU + Current or last value of the turbidity measured by the sensor using the Nephelometric Turbidity Unit (NTU). + + + Conductivity + R + Single + Optional + Float + + S/m + Current or last value of the conductivity measured by the sensor. + + + Conductance + R + Single + Optional + Float + + S/m + Current or last value of the conductance measured by the sensor. + + + Total suspended solids + R + Single + Optional + Float + + mg/l + Current or last value of the TSS measured by the sensor. + + + Total dissolved solids + R + Single + Optional + Float + + mg/l + Current or last value of the TDS measured by the sensor. + + + Salinity + R + Single + Optional + Float + + ppt + Current or last value of the salinity measured by the sensor. + + + NO3 + R + Single + Optional + Float + + mg/l + Current or last value of NO3 measured by the sensor. + + + NH3 + R + Single + Optional + Float + + mg/l + Current or last value of NH3 measured by the sensor. + + + NH4 + R + Single + Optional + Float + + mg/l + Current or last value of NH4 measured by the sensor. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3427.xml b/application/src/main/data/lwm2m-registry/3427.xml new file mode 100644 index 0000000000..4ffe60c2bc --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3427.xml @@ -0,0 +1,138 @@ + + + + + + + Pressure monitoring sensor + The uCIFI water pressure monitoring sensor measures water pressure and help identify water leaks in water distribution pipelines. + 3427 + urn:oma:lwm2m:ext:3427 + 1.0 + 1.0 + Multiple + Optional + + + Pressure + R + Single + Mandatory + Float + + Pa + Last or current pressure value measured by the sensor. + + + Sampling period + RW + Single + Optional + Integer + + s + Number of seconds between two measurement. + + + Leak detected + R + Single + Optional + Boolean + + + Set to True if a water leak is detected by the sensor. + + + Hammer effect detected + R + Single + Optional + Boolean + + + Set to True if hammer effect is detected in the water pipeline. + + + Minimum measured pressure value + R + Single + Optional + Float + + Pa + Minimum value measured by the sensor since power ON or reset. + + + Maximum measured pressure value + R + Single + Optional + Float + + Pa + Maximum value measured by the sensor since power ON or reset. + + + Minimum range pressure value + R + Single + Optional + Float + + Pa + Minimum value that can be measured by the sensor. + + + Maximum range pressure value + R + Single + Optional + Float + + Pa + Maximum value that can be measured by the sensor. + + + Reset min and max measured pressure values + E + Single + Optional + + + + Set the minimum and maximum measured values to current value. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3428.xml b/application/src/main/data/lwm2m-registry/3428.xml new file mode 100644 index 0000000000..68dab08269 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3428.xml @@ -0,0 +1,268 @@ + + + + + + + Air quality + The uCIFI air quality sensor reports measurement required to calculate Air Quality Index (AIQ. It also provides resources for average calculation as per the IQ index calculation. + 3428 + urn:oma:lwm2m:ext:3428 + 1.0 + 1.0 + Multiple + Optional + + + PM10 + R + Single + Optional + Float + + ug/m3 + Level of PM10 measured by the air quality sensor. + + + PM10 24 hour average + R + Single + Optional + Float + + ug/m3 + Average level of PM10 measured by the sensor during the last 24 hours. + + + PM2.5 + R + Single + Optional + Float + + ug/m3 + Level of PM2.5 measured by the air quality sensor. + + + PM2.5 24 hour average + R + Single + Optional + Float + + ug/m3 + Average level of PM2.5 measured by the sensor during the last 24 hours. + + + PM1 + R + Single + Optional + Float + + ug/m3 + Level of PM1 measured by the air quality sensor. + + + PM1 24 hour average + R + Single + Optional + Float + + ug/m3 + Average level of PM1 measured by the sensor during the last 24 hours. + + + CO + R + Single + Optional + Float + + ppm + Level of carbon monoxide measured by the air quality sensor. + + + CO 8 hour average + R + Single + Optional + Float + + ppm + Average level of carbon monoxide measured by the sensor during the last 8 hours. + + + SO2 + R + Single + Optional + Float + + ppm + Level of sulfur dioxide measured by the air quality sensor. + + + SO2 1 hour average + R + Single + Optional + Float + + ppm + Average level of sulfur dioxide measured by the sensor during the last 1 hour. + + + SO2 24 hour average + R + Single + Optional + Float + + ppm + Average level of sulfur dioxide measured by the sensor during the last 24 hours. + + + O3 + R + Single + Optional + Float + + ppm + Level of ozone measured by the air quality sensor. + + + O3 1 hour average + R + Single + Optional + Float + + ppm + Average level of ozone measured by the sensor during the last 1 hour. + + + O3 8 hour average + R + Single + Optional + Float + + ppm + Average level of ozone measured by the sensor during the last 8 hour. + + + NO2 + R + Single + Optional + Float + + ppm + Level of nitrogen dioxide measured by the air quality sensor. + + + NO2 1 hour average + R + Single + Optional + Float + + ppm + Average level of nitrogen dioxide measured by the sensor during the last 1 hour. + + + CO2 + R + Single + Optional + Float + + ppm + Level of carbon dioxide measured by the air quality sensor. + + + CO2 1 hour average + R + Single + Optional + Float + + ppm + Average level of carbon dioxide measured by the sensor during the last 1 hour. + + + NO + R + Single + Optional + Float + + ppm + Level of nitric oxide measured by the air quality sensor. + + + NO 1 hour average + R + Single + Optional + Float + + ppm + Average level of nitric oxide measured by the sensor during the last 1 hour. + + + H2S + R + Single + Optional + Float + + ppm + Level of hydrogen sulfid measured by the air quality sensor. + + + H2S 1 hour average + R + Single + Optional + Float + + ppm + Average level of hydrogen sulfid measured by the sensor during the last 1 hour. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3429.xml b/application/src/main/data/lwm2m-registry/3429.xml new file mode 100644 index 0000000000..e5f6f46b75 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3429.xml @@ -0,0 +1,108 @@ + + + + + + + Tilt sensor + The uCIFI tilt sensor provides the angle between the object's internal vertical and the perpendicular to the earth. + 3429 + urn:oma:lwm2m:ext:3429 + 1.0 + 1.0 + Multiple + Optional + + + Angle + R + Single + Mandatory + Float + + rad + Angle between the internal vertical line and the perpendicular to earth. + + + Minimum measured Angle + R + Single + Optional + Float + + rad + Minimum angle measured by the tilt sensor since last reset or since power ON if no reset. + + + Maximum measured angle + R + Single + Optional + Float + + rad + Maximum angle measured by the tilt sensor since last reset or since power ON if no reset. + + + Reset min max angles + E + Single + Optional + + + + Set the minimum and maximum measured angles to the current angle value. + + + Out of position threshold + RW + Single + Optional + Float + + rad + The angle above which the device triggers an out of position event. + + + Out of position + R + Single + Optional + Boolean + + + Set to True if the angle is above the out of position threshold. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3430.xml b/application/src/main/data/lwm2m-registry/3430.xml new file mode 100644 index 0000000000..76a14c0656 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3430.xml @@ -0,0 +1,371 @@ + + + + + + + Global Navigation Satellite System + More than a positioning object, the uCIFI global navigation satellite system object provides all the information required to calculate a position/location. + 3430 + urn:oma:lwm2m:ext:3430 + 1.0 + 1.0 + Single + Optional + + + Fix timestamp + R + Single + Mandatory + Time + + s + Timestamp of when the location measurement was performed. + + + Latitude + R + Single + Mandatory + Float + + lat + Decimal notation of latitude, e.g. -43.5723 [World Geodetic System 1984]. This value ranges from [-90, 90]. + + + Longitude + R + Single + Mandatory + Float + + lon + Decimal notation of longitude, e.g. 153.21760 [World Geodetic System 1984]. This value ranges from [-180, 180]. + + + Altitude + R + Single + Optional + Float + + m + Altitude above mean sea level in meters. + + + Speed + R + Single + Optional + Integer + + m/s + Horizontal speed calculated by the device. + + + Heading + R + Single + Optional + Float + + deg + Direction that the device is following + + + Radius + R + Single + Optional + Float + + m + Radius of a circular area corresponding to the location’s uncertainty (GPS data precision). Negative values indicate that the radius is not available. + + + HDOP + R + Single + Optional + Float + + + Horizontal dilution of precision. + + + VDOP + R + Single + Optional + Float + + + Vertical dilution of precision. + + + Estimated horizontal accuracy + R + Single + Optional + Float + + m + Estimated horizontal accuracy. + + + Estimated vertical accuracy + R + Single + Optional + Float + + m + Estimated vertical accuracy. + + + Estimated speed accuracy + R + Single + Optional + Float + + m/s + Estimated speed accuracy. + + + Estimated heading accuracy + R + Single + Optional + Float + + deg + Estimated heading accuracy. + + + Fix type + R + Single + Optional + Integer + + + + + + + Fix dimension + R + Single + Optional + Integer + + + + + + + Used satellites + R + Single + Optional + Integer + + + Number of satellites used for the fix. + + + Visible satellites + R + Single + Optional + Integer + + + Number of satellites viewed. Represent all the satellites seen but some of them cannot be in use (e.g. bad signal). + + + Satellite identifiers + R + Multiple + Optional + String + + + Identifier of the satellite. + + + Satellite identifiers + R + Multiple + Optional + Float + + deg + Elevation of the satellite. + + + Satellite azimuth + R + Multiple + Optional + Float + + deg + Azimuth of the satellite. + + + Almanac + R + Single + Optional + Boolean + + + + + + + Ephemeris + R + Multiple + Optional + Boolean + + + + + + + Signal-to-noise ratio + R + Multiple + Optional + Float + + dB + Strength of the signal for each satellite, also called carrier-to-noise. + + + GNSS + R + Multiple + Optional + Integer + + + + + + + Hardware RTC + R + Single + Optional + Time + + s + Time of the internal clock of the GNSS hardware. + + + Assisted GPS + RW + Single + Optional + Boolean + + + Set to True if the almanac is obtained via a cellular connection. Set to False otherwise. + + + Power command + RW + Single + Optional + Boolean + + + Command to switch the hardware ON or OFF and status of the device. + + + PDOP + R + Single + Optional + Float + + + Dilution of precision (NMEA-0183 GSA). + + + Status + R + Single + Mandatory + String + + + Status A=active or V=Void (NMEA-0183 RMC). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3431.xml b/application/src/main/data/lwm2m-registry/3431.xml new file mode 100644 index 0000000000..8f0ec363d1 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3431.xml @@ -0,0 +1,98 @@ + + + + + + + Parking Sensor + The uCIFI parking sensor provides actual and cumulated occupancy duration as well as forbidden parking detection. + 3431 + urn:oma:lwm2m:ext:3431 + 1.0 + 1.0 + Multiple + Optional + + + Occupancy + R + Single + Mandatory + Boolean + + + Set to True if the parking place is occupied. Set to False if the parking place is free. + + + Duration + R + Single + Optional + Integer + + s + Number of seconds since the parking place is occupied. If not occupied, duration shows the duration of the last occupation. + + + Daily Duration + R + Single + Optional + Integer + + s + Cumulated occupation time since beginning of the day. + + + Forbidden parking detected + R + Single + Optional + Boolean + + + Set to True if the vehicle present on the parking place is not authorized. Set to False if parking place is free or if the vehicle is authorized. + + + Type of Sensor + RW + Single + Optional + String + + + Type of sensor (e.g. PIR, camera). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3432.xml b/application/src/main/data/lwm2m-registry/3432.xml new file mode 100644 index 0000000000..4816248320 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3432.xml @@ -0,0 +1,218 @@ + + + + + + + Traffic Counter + The uCIFI traffic counter provides vehicle counting and traffic analysis data. A traffic counting device may implement multiple instances of this traffic counter object, each of them being in charge of counting different categories of vehicles (e.g. bikes, cars, trucks). + 3432 + urn:oma:lwm2m:ext:3432 + 1.0 + 1.0 + Multiple + Optional + + + Cumulated number + R + Single + Optional + Integer + + + Cumulated number of vehicles counted since last reset. + + + Reset cumulated number + E + Single + Optional + + + + Reset the cumulated number of vehicles. + + + Cumulated number today + R + Single + Mandatory + Integer + + + Cumulated number of vehicles counted today. + + + Measuring period 1 + RW + Single + Optional + Integer + + s + Time period 1 during which the counter shall provide number of vehicles (e.g. 1 hour). + + + Measuring period 2 + RW + Single + Optional + Integer + + s + Time period 2 during which the counter shall provide number of vehicles (e.g. 15 minutes). + + + Measuring period 3 + RW + Single + Optional + Integer + + s + Time period 3 during which the counter shall provide number of vehicles (e.g. 5 minutes). + + + Cumulated number during last period 1 + R + Single + Optional + Integer + + + Cumulated number of vehicles counted during the last period 1 (e.g. 1 hour). + + + Cumulated number during last period 2 + R + Single + Optional + Integer + + + Cumulated number of vehicles counted during the last period 2 (e.g. 15 minutes). + + + Cumulated number during last period 3 + R + Single + Optional + Integer + + + Cumulated number of vehicles counted during the last period 3 (e.g. 5 minutes). + + + Average speed during last period 1 + R + Single + Optional + Integer + + + Average speed measured on the vehicles during the last period 1 (e.g. 1 hour). + + + Average speed during last period 2 + R + Single + Optional + Integer + + + Average speed measured on the vehicles during the last period 2 (e.g. 15 minutes). + + + Average speed during last period 3 + R + Single + Optional + Integer + + + Average speed measured on the vehicles during the last period 3 (e.g. 5 minutes). + + + Average distance during last period 1 + R + Single + Optional + Integer + + + Average distance between two vehicles measured during the last period 1 (e.g. 1 hour). + + + Average distance during last period 2 + R + Single + Optional + Integer + + + Average distance between two vehicles measured during the last period 2 (e.g. 15 minutes). + + + Average distance during last period 3 + R + Single + Optional + Integer + + + Average distance between two vehicles measured during the last period 3 (e.g. 5 minutes). + + + Speed limit threshold + RW + Single + Optional + Integer + + + Speed limit threshold used to calculate the percentage of vehicles above speed limit. + + + Percentage above speed limit + R + Single + Optional + Integer + + + Percentage of vehicles driving above speed limit. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3433.xml b/application/src/main/data/lwm2m-registry/3433.xml new file mode 100644 index 0000000000..018f2af954 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3433.xml @@ -0,0 +1,128 @@ + + + + + + + Noise sensor + The uCIFI noise sensor reports a noise measurement in decibel. It also provides resources for minimum/maximum measured values and the minimum/maximum range that can be measured by the noise sensor and some specific alarms resulting from noise analysis by the device. + 3433 + urn:oma:lwm2m:ext:3433 + 1.0 + 1.0 + Multiple + Optional + + + Gunshot detected + R + Single + Optional + Boolean + + + Set to True when a gunshot is detected. + + + Abnormal noise detected + R + Single + Optional + Boolean + + + Set to True when an abnormal noise is detected. + + + Sensor Value + R + Single + Mandatory + Float + + + Last or current measured value from the Sensor. + + + Minimum measured value + R + Single + Optional + Float + + dB + Minimum value measured by the sensor since power ON or reset. + + + Maximum measured value + R + Single + Optional + Float + + dB + Maximum value measured by the sensor since power ON or reset. + + + Minimum range value + R + Single + Optional + Float + + dB + Minimum value that can be measured by the sensor. + + + Maximum range value + R + Single + Optional + Float + + dB + Maximum value that can be measured by the sensor. + + + Reset min and max values + E + Single + Optional + + + + Reset the min and max measured values to current value. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3434.xml b/application/src/main/data/lwm2m-registry/3434.xml new file mode 100644 index 0000000000..b06852cbe1 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3434.xml @@ -0,0 +1,128 @@ + + + + + + + People counter + The uCIFI people counter provides people counting information using Bluetooth beacon or any other method. + 3434 + urn:oma:lwm2m:ext:3434 + 1.0 + 1.0 + Multiple + Optional + + + Actual number of persons + R + Single + Mandatory + Integer + + + Number of persons currently identified by the device. + + + Daily number of persons + R + Single + Optional + Integer + + + Cumulated number of persons detected by the device since beginning of the day. + + + Cumulated number of persons + R + Single + Optional + Integer + + + Cumulated number of persons detected by the device since last reset. + + + Reset of cumulated number + E + Single + Optional + + + + Reset the cumulated number of persons. + + + Daily number of passages + R + Single + Optional + Integer + + + Number of passages (same people could be counted multiple times if identified several times) today. + + + Cumulated number of passages + R + Single + Optional + Integer + + + Cumulated number of passages since last reset. + + + Reset of cumulated number of passages + E + Single + Optional + + + + Reset the cumulated number of passages. + + + Type of sensor + RW + Single + Optional + String + + + Type of sensor (e.g. Bluetooth beacon, WIFI detector). + + + + + diff --git a/application/src/main/data/lwm2m-registry/3435.xml b/application/src/main/data/lwm2m-registry/3435.xml new file mode 100644 index 0000000000..1694afc32b --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3435.xml @@ -0,0 +1,178 @@ + + + + + + + Filling level + The uCIFI filling level sensor measures how full and/or how empty a container (e.g. waste, fuel) is and reports it either in percentage or in centimeters. The filling level sensor may be complemented with a temperature sensor to compose a waste filling sensor that can also identify waste container fire. + 3435 + urn:oma:lwm2m:ext:3435 + 1.0 + 1.0 + Multiple + Optional + + + Container height + RW + Single + Optional + Integer + + cm + Height of the container. + + + Actual filling percentage + R + Single + Mandatory + Float + + /100 + Percentage of container filled with content. + + + Actual filling level + R + Single + Optional + Integer + + cm + Height of content in the container. + + + High threshold + RW + Single + Optional + Float + + /100 + Threshold above which the container is considered full. + + + Container full + R + Single + Mandatory + Boolean + + + Set to True if the actual filling percentage is above the high threshold. + + + Low threshold + RW + Single + Optional + Float + + /100 + Threshold below which the container is considered empty. + + + Container empty + R + Single + Mandatory + Boolean + + + Set to True if the actual filling percentage is below the low threshold. + + + Average filling speed + R + Single + Optional + Float + + /100 + Average percentage filled per day. + + + Average filling speed reset + E + Single + Optional + + + + Reset average filling speed. + + + Forecast full date + R + Single + Optional + Time + + + Next date at which the container should reach the high threshold. + + + Forecast empty date + R + Single + Optional + Time + + + Next date at which the container should reach the low threshold. + + + Container out of location + R + Single + Optional + Boolean + + + Set to True if the container is not at the location where it should be. + + + Container out of position + R + Single + Optional + Boolean + + + Set to True if the container is not in correct upright position. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3436.xml b/application/src/main/data/lwm2m-registry/3436.xml new file mode 100644 index 0000000000..a450edf280 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3436.xml @@ -0,0 +1,98 @@ + + + + + + + Edge computing manager + The uCIFI edge computing manager object defines the capability of a device to run scripts on the device itself. + 3436 + urn:oma:lwm2m:ext:3436 + 1.0 + 1.0 + Single + Optional + + + Max number of scripts + R + Single + Optional + Integer + + + Maximum number of scripts supported by the device. + + + Number of free slots for scripts + R + Single + Optional + Integer + + + Number of free slots to store new scripts. + + + Max number of conditions + R + Single + Optional + Integer + + + Maximum number of conditions supported by the device. + + + Number of free slots for conditions + R + Single + Optional + Integer + + + Number of free slots to store new conditions. + + + Edge-computing capabilities + R + Single + Optional + String + + + Vendor-specific resource to share the capabilities of the devices as regards to edge computing. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3437.xml b/application/src/main/data/lwm2m-registry/3437.xml new file mode 100644 index 0000000000..284911326e --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3437.xml @@ -0,0 +1,118 @@ + + + + + + + Edge computing script + The uCIFI edge computing scripts object defines a particular script to be executed by a device under a time or any other condition set by a "Edge Computing Time Condition" or "Edge Computing Generic Condition" object. + 3437 + urn:oma:lwm2m:ext:3437 + 1.0 + 1.0 + Multiple + Optional + + + Name + RW + Single + Optional + String + + + Name of the script. + + + Signature + RW + Single + Optional + String + + + Unique hash or signature associated to the script to be verified by the end-device before execution. The format of the hash or signature is vendor-specific. + + + Version + RW + Single + Optional + String + + + Version of the script, as defined by the vendor. + + + Script + RW + Single + Mandatory + Opaque + + + Vendor-specific script. + + + Script format + RW + Single + Optional + String + + + Script format to enable the device to select the script interpreter in case it supports multiple (e.g. LUA). The format of this resource is free to support vendor-specific formats. + + + Non supported script + R + Single + Optional + Boolean + + + Set to True if the script is not supported or if the device has identified errors in the script. + + + Non authorized script + R + Single + Optional + Boolean + + + Set to True if the hash/signature is not correct. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3438.xml b/application/src/main/data/lwm2m-registry/3438.xml new file mode 100644 index 0000000000..2e2405c161 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3438.xml @@ -0,0 +1,78 @@ + + + + + + + Edge computing time condition + The uCIFI edge computing time condition object defines the day/time (defined as a CRON rule) at which a particular script should be executed by a device. + 3438 + urn:oma:lwm2m:ext:3438 + 1.0 + 1.0 + Multiple + Optional + + + Execution time/day + RW + Single + Mandatory + String + + + Date/time at which the script should be executed, formatted as a CRON rule. + + + Script identifier + RW + Multiple + Mandatory + String + + + Object URN/Resource ID of the script to apply to the times/days defined in the execution time/day resource. + + + Non supported time condition + R + Single + Optional + Boolean + + + Set to True if the device can’t support the condition. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3439.xml b/application/src/main/data/lwm2m-registry/3439.xml new file mode 100644 index 0000000000..b702866ce0 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3439.xml @@ -0,0 +1,78 @@ + + + + + + + Edge computing generic condition + The uCIFI edge computing generic condition object defines a vendor-specific condition (e.g. when a resource or an object is equal to a particular value) at which a particular script should be executed by the device. + 3439 + urn:oma:lwm2m:ext:3439 + 1.0 + 1.0 + Multiple + Optional + + + Vendor condition + RW + Single + Optional + Opaque + + + Condition described in a vendor specific way. + + + Script identifier + RW + Multiple + Mandatory + String + + + Object URN/Resource ID of the script to apply to the time/days defined in the vendor condition resource. + + + Non supported time condition + R + Single + Optional + Boolean + + + Set to True if the device can’t support the condition. + + + + + diff --git a/application/src/main/data/lwm2m-registry/3441.xml b/application/src/main/data/lwm2m-registry/3441.xml new file mode 100644 index 0000000000..bdd5efca6c --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3441.xml @@ -0,0 +1,262 @@ + + + + + LwM2M v1.0 Test Object + + 3441 + urn:oma:lwm2m:ext:3441 + 1.0 + 1.0 + Multiple + Optional + + + Reset values + E + Single + Optional + + + + Reset all resources of this object with their initial value. + + + Randomize values + E + Single + Optional + + + + + + + + + Clear values + E + Single + Optional + + + + + + + + + Exec With Arguments + E + Single + Optional + + + + + + + + + Arguments List + R + Multiple + Optional + String + + + List of Arguments from last execute on "Exec With Arguments"(3) resource. This resource is not affected by the "Randomize values"(1) executable resource. + + + + String Value + RW + Single + Optional + String + + + Initial value must be "initial value". + + + + Integer Value + RW + Single + Optional + Integer + + + Initial value must be "1024". + + + + Float Value + RW + Single + Optional + Float + + + Initial value must be "3.14159". + + + + Boolean Value + RW + Single + Optional + Boolean + + + Initial value must be "true". + + + Opaque Value + RW + Single + Optional + Opaque + + + Initial value must be the bytes sequence "0123456789ABCDEF" (Hexadecimal notation). + + + + Time Value + RW + Single + Optional + Time + + + Initial value must be the time to an 1st, 2000 in the UTC time zone. (Timestamp value : 946684800) + + + ObjLink Value + RW + Single + Optional + Objlnk + + + Initial value must be a link to instance 0 of Device Object 3 (3:0). + + + Multiple String Value + RW + Multiple + Optional + String + + + Initial value must be 1 instance with ID 0 and value "initial value". + + + Multiple Integer Value + RW + Multiple + Optional + Integer + + + Initial value must be 1 instance with ID 0 and value "1024". + + + Multiple Float Value + RW + Multiple + Optional + Float + + + Initial value must be 1 instance with ID 0 and value "3.14159". + + + Multiple Boolean Value + RW + Multiple + Optional + Boolean + + + Initial value must be 1 instance with ID 0 and value "true". + + + Multiple Opaque Value + RW + Multiple + Optional + Opaque + + + Initial value must be 1 instance with ID 0 and value "0123456789ABCDEF"(Hexadecimal notation of the bytes sequence). + + + Multiple Time Value + RW + Multiple + Optional + Time + + + Initial value must be 1 instance with ID 0 and value 1st, 2000 in the UTC time zone (Timestamp value : 946684800). + + + Multiple ObjLink Value + RW + Multiple + Optional + Objlnk + + + Initial value must be 1 instance with ID 0 and value "3:0". + + + + + diff --git a/application/src/main/data/lwm2m-registry/3442.xml b/application/src/main/data/lwm2m-registry/3442.xml new file mode 100644 index 0000000000..b4607cead7 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/3442.xml @@ -0,0 +1,332 @@ + + + + + LwM2M v1.1 Test Object + + 3442 + urn:oma:lwm2m:ext:3442 + 1.1 + 1.0 + Multiple + Optional + + + Reset values + E + Single + Optional + + + + Reset all resources of this object with their initial value. + + + Randomize values + E + Single + Optional + + + + + + + + + Clear values + E + Single + Optional + + + + + + + + + Exec With Arguments + E + Single + Optional + + + + + + + + + Arguments List + R + Multiple + Optional + String + + + List of Arguments from last execute on "Exec With Arguments"(3) resource. This resource is not affected by "Randomize values"(1) executable resource. + + + + Send Data + E + Single + Optional + + + + + + + + + Resources to Send + RW + Multiple + Optional + String + + + List of path to LWM2M node (Object / Object Instance / Resource / Resource Instance) used by "Send Data "(5) resource. This resource is not affected by "Randomize values"(1) executable resource. + + + + String Value + RW + Single + Optional + String + + + Initial value must be "initial value". + + + + Integer Value + RW + Single + Optional + Integer + + + Initial value must be "1024". + + + + Unsigned Integer Value + RW + Single + Optional + Unsigned Integer + + + Initial value must be 2^63 (9223372036854775808). + + + + Float Value + RW + Single + Optional + Float + + + Initial value must be "3.14159". + + + + Boolean Value + RW + Single + Optional + Boolean + + + Initial value must be "true". + + + Opaque Value + RW + Single + Optional + Opaque + + + Initial value must be the bytes sequence "0123456789ABCDEF" (Hexadecimal notation). + + + + Time Value + RW + Single + Optional + Time + + + Initial value must be the time to an 1st, 2000 in the UTC time zone. (Timestamp value : 946684800) + + + ObjLink Value + RW + Single + Optional + Objlnk + + + Initial value must be a link to instance 0 of Device Object 3 (3:0). + + + CoreLnk Value + RW + Single + Optional + Corelnk + + + ".]]> + + + Multiple String Value + RW + Multiple + Optional + String + + + Initial value must be 1 instance with ID 0 and value "initial value". + + + Multiple Integer Value + RW + Multiple + Optional + Integer + + + Initial value must be 1 instance with ID 0 and value "1024". + + + Multiple Unsigned Integer Value + RW + Multiple + Optional + Unsigned Integer + + + Initial value must be 1 instance with ID 0 and value 2^63 (9223372036854775808). + + + + Multiple Float Value + RW + Multiple + Optional + Float + + + Initial value must be 1 instance with ID 0 and value "3.14159". + + + Multiple Boolean Value + RW + Multiple + Optional + Boolean + + + Initial value must be 1 instance with ID 0 and value "true". + + + Multiple Opaque Value + RW + Multiple + Optional + Opaque + + + Initial value must be 1 instance with ID 0 and value "0123456789ABCDEF"(Hexadecimal notation of the bytes sequence). + + + Multiple Time Value + RW + Multiple + Optional + Time + + + Initial value must be 1 instance with ID 0 and value 1st, 2000 in the UTC time zone (Timestamp value : 946684800). + + + Multiple ObjLink Value + RW + Multiple + Optional + Objlnk + + + Initial value must be 1 instance with ID 0 and value "3:0". + + + Multiple CoreLnk Value + RW + Multiple + Optional + Corelnk + + + ".]]> + + + + + diff --git a/application/src/main/data/lwm2m-registry/4.xml b/application/src/main/data/lwm2m-registry/4.xml new file mode 100644 index 0000000000..dc3f100cc4 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/4.xml @@ -0,0 +1,258 @@ + + + + + + + Connectivity Monitoring + + 4 + urn:oma:lwm2m:oma:4:1.3 + 1.1 + 1.3 + Single + Optional + + + Network Bearer + R + Single + Mandatory + Integer + 0..50 + + + + + Available Network Bearer + R + Multiple + Mandatory + Integer + 0..50 + + + + + Radio Signal Strength + R + Single + Mandatory + Integer + + dBm + + + + Link Quality + R + Single + Optional + Integer + + + + + + IP Addresses + R + Multiple + Mandatory + String + + + + + + Router IP Addresses + R + Multiple + Optional + String + + + + + + Link Utilization + R + Single + Optional + Integer + 0..100 + /100 + + + + APN + R + Multiple + Optional + String + + + + + + Cell ID + R + Single + Optional + Integer + + + + + + SMNC + R + Single + Optional + Integer + 0..999 + + + + + SMCC + R + Single + Optional + Integer + 0..999 + + + + SignalSNR + R + Single + Optional + Integer + + dB + + + + LAC + R + Single + Optional + Integer + + + + + + Coverage Enhancement Level + R + Single + Optional + Integer + 0..4 + + + + + + diff --git a/application/src/main/data/lwm2m-registry/5.xml b/application/src/main/data/lwm2m-registry/5.xml new file mode 100644 index 0000000000..02188533b2 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/5.xml @@ -0,0 +1,255 @@ + + + + + + + Firmware Update + + 5 + urn:oma:lwm2m:oma:5:1.1 + 1.1 + 1.1 + Single + Optional + + + Package + W + Single + Mandatory + Opaque + + + + + + Package URI + RW + Single + Mandatory + String + 0..255 + + + + + Update + E + Single + Mandatory + + + + + + + State + R + Single + Mandatory + Integer + 0..3 + + + + + Update Result + R + Single + Mandatory + Integer + 0..11 + + + + + PkgName + R + Single + Optional + String + 0..255 + + + + + PkgVersion + R + Single + Optional + String + 0..255 + + + + + Firmware Update Protocol Support + R + Multiple + Optional + Integer + 0..5 + + + + + Firmware Update Delivery Method + R + Single + Mandatory + Integer + 0..2 + + + + + Cancel + E + Single + Optional + + + + + + + Severity + RW + Single + Optional + Integer + 0..2 + + + + + Last State Change Time + R + Single + Optional + Time + + + + + + Maximum Defer Period + RW + Single + Optional + Unsigned Integer + + s + + + + + + diff --git a/application/src/main/data/lwm2m-registry/500.xml b/application/src/main/data/lwm2m-registry/500.xml new file mode 100644 index 0000000000..df21a5aa80 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/500.xml @@ -0,0 +1,189 @@ + + + + + + + CoAP Config + + + + 500 + urn:oma:lwm2m:oma:500 + 1.0 + 1.0 + Multiple + Optional + + + Network Bearer + RW + Single + Mandatory + Integer + 0..255 + + + + + + + ACK_TIMEOUT + RW + Single + Mandatory + Integer + + s + + + + + + ACK_RANDOM_FACTOR + RW + Single + Mandatory + Float + + + + + + + + MAX_RETRANSMIT + RW + Single + Mandatory + Integer + + + + + + + + NSTART + RW + Single + Mandatory + Integer + + + + + + + + DEFAULT_LEISURE + RW + Single + Optional + Integer + + s + + + + + + PROBING_RATE + RW + Single + Optional + Integer + + B/s + + + + + + DTLS Retransmission Timer + RW + Single + Optional + Integer + + s + + + + + + Max Response Waiting Time + RW + Single + Optional + Integer + + + + + + + + Apply + E + Single + Optional + + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/501.xml b/application/src/main/data/lwm2m-registry/501.xml new file mode 100644 index 0000000000..402cfc0b78 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/501.xml @@ -0,0 +1,163 @@ + + + + + + + LTE-MTC Band Config + + + + 501 + urn:oma:lwm2m:oma:501 + 1.0 + 1.0 + Multiple + Optional + + + EARFCN + RW + Single + Mandatory + Integer + 0..65535 + + + + + + + Current Frequency (Downlink) + R + Single + Mandatory + Float + + MHz + + + + + + Current Frequency (Uplink) + R + Single + Optional + Float + + MHz + + + + + + Prioritized Channels + RW + Multiple + Mandatory + Integer + + + + + + + + Application Type + RW + Single + Optional + String + + + + + + + + Reset Timeout + RW + Single + Mandatory + Float + + s + + + + + + Reset + E + Single + Mandatory + + + + + + + + + Vendor specific extensions + R + Single + Optional + Objlnk + + + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/502.xml b/application/src/main/data/lwm2m-registry/502.xml new file mode 100644 index 0000000000..5b85aa4515 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/502.xml @@ -0,0 +1,214 @@ + + + + + + + CO Detector + + 502 + urn:oma:lwm2m:oma:502 + 1.0 + 1.0 + Multiple + Optional + + + CO Sensor Location Tag + RW + Single + Optional + String + + + + + + CO Detected + R + Single + Mandatory + Boolean + + + + + + Ambient CO Value + RW + Single + Optional + Float + 0..12800 + ppm + + + + CO Value + R + Single + Mandatory + Float + 0..12800 + ppm + + + + Min CO Range Value + R + Single + Optional + Float + 0..12800 + ppm + + + + Max CO Range Value + R + Single + Optional + Float + 0..12800 + ppm + + + + CO Detection Accuracy + R + Single + Optional + Float + 0..12800 + ppm + + + + Minimum Measured CO Value + R + Single + Optional + Float + 0..12800 + ppm + + + + Maximum Measured CO Value + R + Single + Optional + Float + 0..12800 + ppm + + + + Upper CO Threshold + RW + Single + Optional + Float + 0..12800 + ppm + + + + Reset + E + Single + Optional + + + + + + + Alarm loudness + R + Single + Optional + Float + + dB + + + + Latitude + R + Single + Optional + String + + + + + + Longitude + R + Single + Optional + String + + + + + + Altitude + R + Single + Optional + Float + + m + + + + Battery Percentage + R + Single + Optional + Float + 0.00..100.00 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/503.xml b/application/src/main/data/lwm2m-registry/503.xml new file mode 100644 index 0000000000..6c6e627a94 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/503.xml @@ -0,0 +1,434 @@ + + + + + + + Fire Alarm + + 503 + urn:oma:lwm2m:oma:503 + 1.0 + 1.0 + Multiple + Optional + + + Temperature Sensor Location Tag + RW + Single + Optional + String + + + + + + Temperature Unit + RW + Single + Mandatory + Integer + + + + + + Ambient Temperature + RW + Single + Optional + Float + + + + + + Temperature Value + R + Single + Mandatory + Float + + + + + + Minimum Measured Temperature Value + R + Single + Optional + Float + + + + + + Maximum Measured Temperature Value + R + Single + Optional + Float + + + + + + Min Temperature Range Value + RW + Single + Optional + Float + + + + + + Max Temperature Range Value + RW + Single + Optional + Float + + + + + + Lower Temperature Accuracy + RW + Single + Optional + Float + + + + + + Upper Temperature Accuracy + RW + Single + Optional + Float + + + + + + Temperature Threshold + RW + Single + Optional + Float + + + + + + Temperature Sensor Error + R + Single + Optional + Boolean + + + + + + Reset Temperature + E + Single + Optional + + + + + + + Temperature Alarm Loudness + RW + Single + Optional + Float + + dB + + + + Humidity Sensor Location Tag + RW + Single + Optional + String + + + + + + Ambient Relative Humidity + RW + Single + Optional + Float + 0.0..100.0 + %RH + + + + Humidity Value + R + Single + Mandatory + Float + 0.0..100.0 + /100 + + + + Minimum Measured Humidity Value + R + Single + Optional + Float + 0.0..100.0 + /100 + + + + Maximum Measured Humidity Value + R + Single + Optional + Float + 0.0..100.0 + /100 + + + + Humidity Accuracy + RW + Single + Optional + Float + 0.0..100.0 + /100 + + + + Humidity Threshold + RW + Single + Optional + Float + 0.0..100.0 + /100 + + + + Humidity Sensor Error + R + Single + Optional + Boolean + + + + + + Reset Humidity + E + Single + Optional + + + + + + + Humidity Alarm Loudness + RW + Single + Optional + Float + + dB + + + + Smoke Sensor Location Tag + RW + Single + Optional + String + + + + + + CO2 Alarm State + R + Single + Mandatory + Boolean + + + + + + Ambient CO2 Value + R + Single + Optional + Float + + ppm + + + + CO2 Value + R + Single + Mandatory + Float + + ppm + + + + Minimum Measured CO2 Value + R + Single + Optional + Float + + ppm + + + + Maximum Measured CO2 Value + R + Single + Optional + Float + + ppm + + + + CO2 Threshold + RW + Single + Optional + Float + + ppm + + + + Smoke Sensor Error + R + Single + Optional + Boolean + + + + + + Reset Smoke Detection + E + Single + Optional + + + + + + + Smoke Alarm Loudness + R + Single + Optional + Float + + dB + + + + Battery Percentage + R + Single + Optional + Float + 0.00..100.00 + + + + + Latitude + R + Single + Optional + String + + + + + + Longitude + R + Single + Optional + String + + + + + + Altitude + R + Single + Optional + Float + + m + + + + + + diff --git a/application/src/main/data/lwm2m-registry/504.xml b/application/src/main/data/lwm2m-registry/504.xml new file mode 100644 index 0000000000..25c033f59d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/504.xml @@ -0,0 +1,341 @@ + + + + + + + Remote SIM Provisioning + + 504 + urn:oma:lwm2m:oma:504 + 1.0 + 1.0 + Multiple + Optional + + + Current SIM Type + R + Single + Mandatory + Integer + 0..3 + + + + + Supported SIM Type + R + Multiple + Mandatory + Integer + 0..3 + + + + + Service Provider Name + R + Single + Optional + String + + + + + + Download URI/Information + RW + Single + Optional + String + 0..255 + + + + + RSP Update + E + Single + Optional + + + + + + + RSP Update State + R + Single + Optional + Integer + 0..10 + + + + + RSP Update Result + R + Single + Optional + Integer + 0..16 + + + + + Profile Name + R + Single + Optional + String + 0..255 + + + + + Profile Package Version + R + Single + Optional + String + 0..255 + + + + + Free Memory on SIM + R + Single + Optional + Integer + + KiB + + + + Total Memory on SIM + R + Single + Optional + Integer + + KiB + + + + Integrated Circuit Card Identifier (ICCID) + R + Multiple + Mandatory + String + + + + + + eUICC ID + R + Single + Optional + String + + + + + + RSP Type + R + Single + Mandatory + Integer + + + + + + Selected ICCID for RSP operation + RW + Single + Optional + String + + + + + + RSP Consent Reason + R + Single + Optional + String + + + + + + RSP User Consent + W + Single + Optional + Integer + 0..1 + + + + + RSP Confirmation Code + W + Single + Optional + String + + + + + + RSP Data Type + RW + Single + Optional + Integer + 0..17 + + + + + Set RSP Data + W + Single + Optional + Opaque + + + + + + Get RSP Data + R + Single + Optional + Opaque + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/505.xml b/application/src/main/data/lwm2m-registry/505.xml new file mode 100644 index 0000000000..a89c59ca41 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/505.xml @@ -0,0 +1,262 @@ + + + + + + + LwM2M SIM Provisioning + + 505 + urn:oma:lwm2m:oma:505 + 1.0 + 1.0 + Multiple + Optional + + + Current SIM Type + R + Single + Mandatory + Integer + 0..3 + + + + + Supported SIM Type + R + Multiple + Mandatory + Integer + 0..3 + + + + + Service Provider Name + R + Single + Mandatory + String + + + + + + Profile Package + W + Single + Optional + Opaque + + + + + + Profile URI + RW + Single + Optional + String + 0..255 + + + + + Update Profile + E + Single + Optional + + + + + + + Update State + R + Single + Mandatory + Integer + 0..3 + + + + + Update Result + R + Single + Mandatory + Integer + 0..8 + + + + + Profile Name + R + Single + Optional + String + 0..255 + + + + + Profile Package Version + R + Single + Optional + String + 0..255 + + + + + Profile Update Protocol Support + R + Multiple + Optional + Integer + 0..3 + + + + + Profile Update Delivery Method + R + Single + Mandatory + Integer + 0..2 + + + + + Free Memory on SIM + R + Single + Optional + Integer + + KiB + + + + Total Memory on SIM + R + Single + Optional + Integer + + KiB + + + + Integrated Circuit Card Identifier (ICCID) + R + Multiple + Optional + Integer + + + + + + eUICC ID + R + Single + Optional + String + + + + + + Profile Type + R + Single + Optional + String + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/507.xml b/application/src/main/data/lwm2m-registry/507.xml new file mode 100644 index 0000000000..650a0e8904 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/507.xml @@ -0,0 +1,115 @@ + + + + + + nuSIM Profile + This device management object contains the elements of a nuSIM profile package according to nuSIM Technical Specification NTS.01. + 507 + urn:oma:lwm2m:oma:507 + 1.0 + 1.0 + Multiple + Optional + + + encP + RW + Single + Mandatory + String + + + + + mac + RW + Single + Mandatory + String + + + + + eKpubDP + RW + Single + Mandatory + String + + + + + + SigEKpubDP + RW + Single + Mandatory + String + + + + + + KpubDP + RW + Single + Mandatory + String + + + + + + SigKpubDP + RW + Single + Mandatory + String + + + + + + KpubCl + RW + Single + Mandatory + String + + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/508.xml b/application/src/main/data/lwm2m-registry/508.xml new file mode 100644 index 0000000000..7f45059dd5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/508.xml @@ -0,0 +1,115 @@ + + + + + + nuSIM + This device management object represents a nuSIM integrated SIM according to NTS.01 - nuSIM Integrated SIM Specification. + 508 + urn:oma:lwm2m:oma:508 + 1.0 + 1.0 + Single + Optional + + + EID + R + Single + Mandatory + String + + + + + + ICCID + R + Single + Mandatory + String + + + + + + nuSIM Capabilities + R + Single + Mandatory + Opaque + + + + + nuSIM Cert + R + Single + Optional + String + + + + + + nuSIM Profile + RW + Single + Mandatory + Objlnk + + + + + Load Profile + E + Single + Mandatory + + + + + + + Load Profile Result + R + Single + Mandatory + Integer + 0..255 + + + + + + + diff --git a/application/src/main/data/lwm2m-registry/509.xml b/application/src/main/data/lwm2m-registry/509.xml new file mode 100644 index 0000000000..17ff50250f --- /dev/null +++ b/application/src/main/data/lwm2m-registry/509.xml @@ -0,0 +1,138 @@ + + + + + + + Measurement Metadata + This object contains measurement metadata for sensor objects. The corresponding sensor is identified with an object link. If the LwM2M Client provides measurement quality information both inside the sensor objects and in the Measurement Metadata object, it MUST ensure that identical data is reported in corresponding object instances. The LwM2M Server SHOULD use the data from the Measurement Metadata object if both are available. The LwM2M Client also MUST ensure that no two Measurement Metadata object instances link to the same sensor object instance. If the LwM2M Server receives such non-conforming data, the Measurement Metadata object instance with largest Instance ID SHOULD be used. + + 509 + urn:oma:lwm2m:oma:509 + 1.1 + 1.0 + Multiple + Optional + + + Linked Sensor + R + Single + Mandatory + Objlnk + + + Link to the sensor object instance that this metadata relates to. + + + + Measurement Quality Reason Code + R + Multiple + Optional + Integer + 0..1023 + + Measurement quality degradation reason reported by a smart sensor. A degraded measurement has Measurement Quality Indicator value 1-3 or Measurement Quality Level less than 100. +0: UNKNOWN Reason is unknown. +1: UNDER_RANGE Measured value is under measurement range. +2: OVER_RANGE Measured value is over measurement range. +3: NOISY Measured value seems to change too quickly. +4: STUCK_VALUE Measured value seems to change too slowly. +5: STEP_TOO_HIGH Measured value has changed too much. +6: CALIBRATION_EXPIRED Sensor calibration has expired. +7: WARMING_UP Sensor is warming up. +8: SERVICE_MODE Sensor is under service. +9: HUMAN_INTERFERENCE Human presence near the sensor. +10: NATURAL_INTERFERENCE Natural phenomenon or animal presence. +11: CONTAMINATION Contamination of the sensor. +12: HW_FAILURE Sensor hardware has failed. +13: LACK_OF_DATA_SAMPLES Data sampling is lossy. +14: OUT_OF_OPERATING_RANGE Environment conditions exceeded. +15: AUTO_CALIBRATION Auto calibration in progress. +16-127: RESERVED_GENERIC_BLOCK Reserved generic block. +128-511: VENDOR_SPECIFIC_BLOCK Vendor specific block. +512-1023: RESERVED_VENDOR_BLOCK Reserved vendor specific block. + + + + Measurement Quality Indicator + R + Single + Optional + Integer + 0..23 + + Measurement quality indicator reported by a smart sensor. +0: UNCHECKED No quality checks were done because they do not exist or can not be applied. +1: REJECTED WITH CERTAINTY The measured value is invalid. +2: REJECTED WITH PROBABILITY The measured value is likely invalid. +3: ACCEPTED BUT SUSPICIOUS The measured value is likely OK. +4: ACCEPTED The measured value is OK. +5-15: Reserved for future extensions. +16-23: Vendor specific measurement quality. + + + + Measurement Quality Level + R + Single + Optional + Integer + 0..100 + + Measurement quality level reported by a smart sensor. Quality level 100 means that the measurement has fully passed quality check algorithms. Smaller quality levels mean that quality has decreased and the measurement has only partially passed quality check algorithms. The smaller the quality level, the more caution should be used by the application when using the measurement. When the quality level is 0 it means that the measurement should certainly be rejected. + + + + + + diff --git a/application/src/main/data/lwm2m-registry/510.xml b/application/src/main/data/lwm2m-registry/510.xml new file mode 100644 index 0000000000..302377bbff --- /dev/null +++ b/application/src/main/data/lwm2m-registry/510.xml @@ -0,0 +1,103 @@ + + + + + + + Vendor Specific Measurement Quality Reason + This object contains description for vendor specific measurement quality reason codes provided in a Measurement Metadata (509) object. Instances of this object define the mapping of vendor specific reason codes to reason description in the scope of an individual device. + + 510 + urn:oma:lwm2m:oma:510 + 1.1 + 1.0 + Multiple + Optional + + + Vendor Specific Quality Reason Code + R + Single + Mandatory + Integer + 128..1023 + + Measurement quality degradation reason reported by a smart sensor. Reason codes 0-127 are generic reason codes and they are defined in Measurement Metadata object description. +128-511: VENDOR_SPECIFIC_BLOCK Vendor specific block. This block contains vendor specific quality reason codes. +512-1023: RESERVED_VENDOR_BLOCK Reserved vendor specific block. This block is reserved for future use. + + + + Quality Reason Name + R + Single + Mandatory + String + 1..128 + + The name of the quality reason. It is used as a key to find a localized reason description. The name should be concise and unique in the localization scope. + + + + Quality Reason Description + R + Single + Mandatory + String + + + Description of the quality reason. This text can be used if a localized version is not found. + + + + + + diff --git a/application/src/main/data/lwm2m-registry/6.xml b/application/src/main/data/lwm2m-registry/6.xml new file mode 100644 index 0000000000..8b1b3da625 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/6.xml @@ -0,0 +1,143 @@ + + + + + + + Location + + 6 + urn:oma:lwm2m:oma:6 + 1.0 + 1.0 + Single + Optional + + + Latitude + R + Single + Mandatory + Float + + lat + + + + Longitude + R + Single + Mandatory + Float + + lon + + + + Altitude + R + Single + Optional + Float + + m + + + + Radius + R + Single + Optional + Float + + m + + + + Velocity + R + Single + Optional + Opaque + + + + + + Timestamp + R + Single + Mandatory + Time + + + + + Speed + R + Single + Optional + Float + + m/s + + + + + diff --git a/application/src/main/data/lwm2m-registry/7.xml b/application/src/main/data/lwm2m-registry/7.xml new file mode 100644 index 0000000000..451780da1f --- /dev/null +++ b/application/src/main/data/lwm2m-registry/7.xml @@ -0,0 +1,164 @@ + + + + + + + Connectivity Statistics + + 7 + urn:oma:lwm2m:oma:7 + 1.0 + 1.0 + Single + Optional + + + SMS Tx Counter + R + Single + Optional + Integer + + + + + + SMS Rx Counter + R + Single + Optional + Integer + + + + + + Tx Data + R + Single + Optional + Integer + + + + + + Rx Data + R + Single + Optional + Integer + + + + + + Max Message Size + R + Single + Optional + Integer + + B + + + + Average Message Size + R + Single + Optional + Integer + + B + + + + Start + E + Single + Mandatory + + + + + + Stop + E + Single + Mandatory + + + + + + Collection Period + RW + Single + Optional + Integer + + s + + + + + + diff --git a/application/src/main/data/lwm2m-registry/8.xml b/application/src/main/data/lwm2m-registry/8.xml new file mode 100644 index 0000000000..a9296999f5 --- /dev/null +++ b/application/src/main/data/lwm2m-registry/8.xml @@ -0,0 +1,141 @@ + + + + + + + Lock and Wipe + + 8 + urn:oma:lwm2m:oma:8 + 1.0 + 1.0 + Single + Optional + + State + RW + Single + Mandatory + Integer + 0..2 + + + + Lock target + W + Multiple + Mandatory + String + + + + + Wipe item + R + Multiple + Optional + String + + + + + Wipe + E + Single + Mandatory + + + + + + Wipe target + W + Multiple + Mandatory + String + + + + Lock or Wipe Operation Result + R + Single + Mandatory + Integer + 0..8 + + + + + + diff --git a/application/src/main/data/lwm2m-registry/9.xml b/application/src/main/data/lwm2m-registry/9.xml new file mode 100644 index 0000000000..4186b1248d --- /dev/null +++ b/application/src/main/data/lwm2m-registry/9.xml @@ -0,0 +1,321 @@ + + + + + + + LWM2M Software Management + + 9 + urn:oma:lwm2m:oma:9 + 1.0 + 1.0 + Multiple + Optional + + + PkgName + R + Single + Mandatory + String + 0..255 bytes + + + + + PkgVersion + R + Single + Mandatory + String + 0..255 bytes + + + + + Package + W + Single + Optional + Opaque + + + + + + Package URI + W + Single + Optional + String + 0..255 bytes + + + + + + Install + E + Single + Mandatory + + + + + + + Checkpoint + R + Single + Optional + Objlnk + + + + + + Uninstall + E + Single + Mandatory + + + + + + + Update State + R + Single + Mandatory + Integer + 0..4 + + + + + Update Supported Objects + RW + Single + Optional + Boolean + + + + + + Update Result + R + Single + Mandatory + Integer + 0..200 + + + + + Activate + E + Single + Mandatory + + + + + + + Deactivate + E + Single + Mandatory + + + + + + + Activation State + R + Single + Mandatory + Boolean + + + + + + Package Settings + RW + Single + Optional + Objlnk + + + + + + User Name + W + Single + Optional + String + 0..255 bytes + + + + + Password + W + Single + Optional + String + 0..255 bytes + + + + + Status Reason + R + Single + Optional + String + + + + + + Software Component Link + R + Multiple + Optional + Objlnk + + + + + + Software Component tree length + R + Single + Optional + Integer + 0..255 + + + + + + + diff --git a/application/src/main/data/upgrade/1.3.0/schema_update.cql b/application/src/main/data/upgrade/1.3.0/schema_update.cql index 88cb1dce81..da20e2b6d0 100644 --- a/application/src/main/data/upgrade/1.3.0/schema_update.cql +++ b/application/src/main/data/upgrade/1.3.0/schema_update.cql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/1.3.1/schema_update.sql b/application/src/main/data/upgrade/1.3.1/schema_update.sql index 1ca6590835..a0f1ef83b7 100644 --- a/application/src/main/data/upgrade/1.3.1/schema_update.sql +++ b/application/src/main/data/upgrade/1.3.1/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/1.4.0/schema_update.cql b/application/src/main/data/upgrade/1.4.0/schema_update.cql index 138c9f9086..b41f5b82d5 100644 --- a/application/src/main/data/upgrade/1.4.0/schema_update.cql +++ b/application/src/main/data/upgrade/1.4.0/schema_update.cql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/1.4.0/schema_update.sql b/application/src/main/data/upgrade/1.4.0/schema_update.sql index c999c6b3e9..435a3d05d0 100644 --- a/application/src/main/data/upgrade/1.4.0/schema_update.sql +++ b/application/src/main/data/upgrade/1.4.0/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.0.0/schema_update.cql b/application/src/main/data/upgrade/2.0.0/schema_update.cql index 5f7daa0ea8..279b21c489 100644 --- a/application/src/main/data/upgrade/2.0.0/schema_update.cql +++ b/application/src/main/data/upgrade/2.0.0/schema_update.cql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.0.0/schema_update.sql b/application/src/main/data/upgrade/2.0.0/schema_update.sql index 4a07d571e8..d110c2d232 100644 --- a/application/src/main/data/upgrade/2.0.0/schema_update.sql +++ b/application/src/main/data/upgrade/2.0.0/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.1.1/schema_update.cql b/application/src/main/data/upgrade/2.1.1/schema_update.cql index 49aa012d70..8fede09387 100644 --- a/application/src/main/data/upgrade/2.1.1/schema_update.cql +++ b/application/src/main/data/upgrade/2.1.1/schema_update.cql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.1.1/schema_update.sql b/application/src/main/data/upgrade/2.1.1/schema_update.sql index a840af7e4f..4589d405dd 100644 --- a/application/src/main/data/upgrade/2.1.1/schema_update.sql +++ b/application/src/main/data/upgrade/2.1.1/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.1.2/schema_update.cql b/application/src/main/data/upgrade/2.1.2/schema_update.cql index 0a27a912a0..a0b5ca5b90 100644 --- a/application/src/main/data/upgrade/2.1.2/schema_update.cql +++ b/application/src/main/data/upgrade/2.1.2/schema_update.cql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.1.2/schema_update.sql b/application/src/main/data/upgrade/2.1.2/schema_update.sql index d7baf2ec7e..5dcc63dc2f 100644 --- a/application/src/main/data/upgrade/2.1.2/schema_update.sql +++ b/application/src/main/data/upgrade/2.1.2/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.2.0/schema_update.sql b/application/src/main/data/upgrade/2.2.0/schema_update.sql index 9fc8d11189..568d0031fa 100644 --- a/application/src/main/data/upgrade/2.2.0/schema_update.sql +++ b/application/src/main/data/upgrade/2.2.0/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.3.1/schema_update.sql b/application/src/main/data/upgrade/2.3.1/schema_update.sql index 7fd717e253..33c3502ad7 100644 --- a/application/src/main/data/upgrade/2.3.1/schema_update.sql +++ b/application/src/main/data/upgrade/2.3.1/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.4.0/schema_update.sql b/application/src/main/data/upgrade/2.4.0/schema_update.sql index 835b1940ea..daf9f5f2b4 100644 --- a/application/src/main/data/upgrade/2.4.0/schema_update.sql +++ b/application/src/main/data/upgrade/2.4.0/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.4.2/schema_update.sql b/application/src/main/data/upgrade/2.4.2/schema_update.sql index d99aaa4d94..0923883a97 100644 --- a/application/src/main/data/upgrade/2.4.2/schema_update.sql +++ b/application/src/main/data/upgrade/2.4.2/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql b/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql index 936911b6a1..9c05371d3a 100644 --- a/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql +++ b/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_psql_ts.sql b/application/src/main/data/upgrade/2.4.3/schema_update_psql_ts.sql index 5cf51d13ed..0c9e250035 100644 --- a/application/src/main/data/upgrade/2.4.3/schema_update_psql_ts.sql +++ b/application/src/main/data/upgrade/2.4.3/schema_update_psql_ts.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_timescale_ts.sql b/application/src/main/data/upgrade/2.4.3/schema_update_timescale_ts.sql index 58f69d034d..8d5bc061ad 100644 --- a/application/src/main/data/upgrade/2.4.3/schema_update_timescale_ts.sql +++ b/application/src/main/data/upgrade/2.4.3/schema_update_timescale_ts.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql b/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql index 2a5ca58092..46f6e0f675 100644 --- a/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql +++ b/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.0.1/schema_ts_latest.sql b/application/src/main/data/upgrade/3.0.1/schema_ts_latest.sql index a270af1d53..67d5980da2 100644 --- a/application/src/main/data/upgrade/3.0.1/schema_ts_latest.sql +++ b/application/src/main/data/upgrade/3.0.1/schema_ts_latest.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.0.1/schema_update_to_uuid.sql b/application/src/main/data/upgrade/3.0.1/schema_update_to_uuid.sql index 01f443151d..ea382dc829 100644 --- a/application/src/main/data/upgrade/3.0.1/schema_update_to_uuid.sql +++ b/application/src/main/data/upgrade/3.0.1/schema_update_to_uuid.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.1.0/schema_update.sql b/application/src/main/data/upgrade/3.1.0/schema_update.sql index a23150cab9..3e666910c7 100644 --- a/application/src/main/data/upgrade/3.1.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.1.0/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_after.sql b/application/src/main/data/upgrade/3.1.1/schema_update_after.sql index 75cb865b69..97d767cdeb 100644 --- a/application/src/main/data/upgrade/3.1.1/schema_update_after.sql +++ b/application/src/main/data/upgrade/3.1.1/schema_update_after.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql index 0d74063bb1..fc55cb18f3 100644 --- a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql +++ b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.2.1/schema_update.sql b/application/src/main/data/upgrade/3.2.1/schema_update.sql index 04dc38a28d..ea25872753 100644 --- a/application/src/main/data/upgrade/3.2.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.2.1/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.2.1/schema_update_ttl.sql b/application/src/main/data/upgrade/3.2.1/schema_update_ttl.sql index cecbf5aff1..407a2e1727 100644 --- a/application/src/main/data/upgrade/3.2.1/schema_update_ttl.sql +++ b/application/src/main/data/upgrade/3.2.1/schema_update_ttl.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.2.2/schema_update.sql b/application/src/main/data/upgrade/3.2.2/schema_update.sql index 8685bf3ccf..c901dc8783 100644 --- a/application/src/main/data/upgrade/3.2.2/schema_update.sql +++ b/application/src/main/data/upgrade/3.2.2/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.2.2/schema_update_event.sql b/application/src/main/data/upgrade/3.2.2/schema_update_event.sql index 9e2e6cfd33..8f43d7e90e 100644 --- a/application/src/main/data/upgrade/3.2.2/schema_update_event.sql +++ b/application/src/main/data/upgrade/3.2.2/schema_update_event.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql b/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql index 42a7e8b277..a765ce1957 100644 --- a/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql +++ b/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.3.2/schema_update.sql b/application/src/main/data/upgrade/3.3.2/schema_update.sql index 5501c359cb..fde6a83dc2 100644 --- a/application/src/main/data/upgrade/3.3.2/schema_update.sql +++ b/application/src/main/data/upgrade/3.3.2/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql b/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql index dec4f269f3..fe31a67d55 100644 --- a/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql +++ b/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.3.3/schema_event_ttl_procedure.sql b/application/src/main/data/upgrade/3.3.3/schema_event_ttl_procedure.sql index 432fca8786..79862cdee7 100644 --- a/application/src/main/data/upgrade/3.3.3/schema_event_ttl_procedure.sql +++ b/application/src/main/data/upgrade/3.3.3/schema_event_ttl_procedure.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.3.3/schema_update.sql b/application/src/main/data/upgrade/3.3.3/schema_update.sql index eed1cab1ea..5d67f09baf 100644 --- a/application/src/main/data/upgrade/3.3.3/schema_update.sql +++ b/application/src/main/data/upgrade/3.3.3/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.3.4/schema_update.sql b/application/src/main/data/upgrade/3.3.4/schema_update.sql index c86a4fe1f4..566ef25aeb 100644 --- a/application/src/main/data/upgrade/3.3.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.3.4/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.4.0/schema_update.sql b/application/src/main/data/upgrade/3.4.0/schema_update.sql index 71588d88ac..38f4e6d283 100644 --- a/application/src/main/data/upgrade/3.4.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.0/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.4.1/schema_update.sql b/application/src/main/data/upgrade/3.4.1/schema_update.sql index a3d7bfb78f..fe012f8453 100644 --- a/application/src/main/data/upgrade/3.4.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.1/schema_update.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ $$ -- in case of running the upgrade script a second time: IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_audit_log')) THEN ALTER TABLE audit_log RENAME TO old_audit_log; + CREATE INDEX IF NOT EXISTS idx_old_audit_log_created_time ON old_audit_log(created_time); + ALTER INDEX IF EXISTS idx_audit_log_tenant_id_and_created_time RENAME TO idx_old_audit_log_tenant_id_and_created_time; FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts @@ -86,6 +88,7 @@ $$ -- in case of running the upgrade script a second time: IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN ALTER TABLE edge_event RENAME TO old_edge_event; + CREATE INDEX IF NOT EXISTS idx_old_edge_event_created_time_tmp ON old_edge_event(created_time); ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time; FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts diff --git a/application/src/main/data/upgrade/3.4.1/schema_update_after.sql b/application/src/main/data/upgrade/3.4.1/schema_update_after.sql index 78df4d2fd2..d1d8ae4dbe 100644 --- a/application/src/main/data/upgrade/3.4.1/schema_update_after.sql +++ b/application/src/main/data/upgrade/3.4.1/schema_update_after.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.4.1/schema_update_before.sql b/application/src/main/data/upgrade/3.4.1/schema_update_before.sql index 27f772aba3..0820fde769 100644 --- a/application/src/main/data/upgrade/3.4.1/schema_update_before.sql +++ b/application/src/main/data/upgrade/3.4.1/schema_update_before.sql @@ -1,5 +1,5 @@ -- --- Copyright © 2016-2022 The Thingsboard Authors +-- Copyright © 2016-2023 The Thingsboard Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql new file mode 100644 index 0000000000..aa53f98b74 --- /dev/null +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -0,0 +1,379 @@ +-- +-- Copyright © 2016-2023 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +-- USER CREDENTIALS START + +ALTER TABLE user_credentials + ADD COLUMN IF NOT EXISTS additional_info varchar NOT NULL DEFAULT '{}'; + +UPDATE user_credentials + SET additional_info = json_build_object('userPasswordHistory', (u.additional_info::json -> 'userPasswordHistory')) + FROM tb_user u WHERE user_credentials.user_id = u.id AND u.additional_info::jsonb ? 'userPasswordHistory'; + +UPDATE tb_user SET additional_info = tb_user.additional_info::jsonb - 'userPasswordHistory' WHERE additional_info::jsonb ? 'userPasswordHistory'; + +-- USER CREDENTIALS END + +-- ALARM ASSIGN TO USER START + +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS assign_ts BIGINT DEFAULT 0; +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS assignee_id UUID; + +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_assignee_created_time ON alarm(tenant_id, assignee_id, created_time DESC); + +-- ALARM ASSIGN TO USER END + +-- ALARM STATUS REFACTORING START + +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS acknowledged boolean; +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS cleared boolean; + +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS status varchar; -- to avoid failure of the subsequent upgrade. +UPDATE alarm SET acknowledged = true, cleared = true WHERE status = 'CLEARED_ACK'; +UPDATE alarm SET acknowledged = true, cleared = false WHERE status = 'ACTIVE_ACK'; +UPDATE alarm SET acknowledged = false, cleared = true WHERE status = 'CLEARED_UNACK'; +UPDATE alarm SET acknowledged = false, cleared = false WHERE status = 'ACTIVE_UNACK'; + +-- Drop index by 'status' column and replace with new indexes that has only active alarms; +DROP INDEX IF EXISTS idx_alarm_originator_alarm_type_active; +CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active + ON alarm USING btree (originator_id, type) WHERE cleared = false; + +DROP INDEX IF EXISTS idx_alarm_tenant_alarm_type_active; +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_active + ON alarm USING btree (tenant_id, type) WHERE cleared = false; + +-- Cover index by alarm type to optimize propagated alarm queries; +DROP INDEX IF EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id; +CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm +USING btree (tenant_id, entity_id, alarm_type, created_time DESC) INCLUDE(alarm_id); + +DROP INDEX IF EXISTS idx_alarm_tenant_status_created_time; +ALTER TABLE alarm DROP COLUMN IF EXISTS status; + +-- Update old alarms and set their state to clear, if there are newer alarms. +UPDATE alarm a +SET cleared = TRUE +WHERE cleared = FALSE + AND id != (SELECT l.id + FROM alarm l + WHERE l.tenant_id = a.tenant_id + AND l.originator_id = a.originator_id + AND l.type = a.type + ORDER BY l.created_time DESC, l.id + LIMIT 1); + +-- ALARM STATUS REFACTORING END + +-- ALARM COMMENTS START + +CREATE TABLE IF NOT EXISTS alarm_comment ( + id uuid NOT NULL, + created_time bigint NOT NULL, + alarm_id uuid NOT NULL, + user_id uuid, + type varchar(255) NOT NULL, + comment varchar(10000), + CONSTRAINT fk_alarm_comment_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE +) PARTITION BY RANGE (created_time); +CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id); + +-- ALARM COMMENTS END + +-- NOTIFICATIONS START + +CREATE TABLE IF NOT EXISTS notification_target ( + id UUID NOT NULL CONSTRAINT notification_target_pkey PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + name VARCHAR(255) NOT NULL, + configuration VARCHAR(10000) NOT NULL, + CONSTRAINT uq_notification_target_name UNIQUE (tenant_id, name) +); +CREATE INDEX IF NOT EXISTS idx_notification_target_tenant_id_created_time ON notification_target(tenant_id, created_time DESC); + +CREATE TABLE IF NOT EXISTS notification_template ( + id UUID NOT NULL CONSTRAINT notification_template_pkey PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + name VARCHAR(255) NOT NULL, + notification_type VARCHAR(50) NOT NULL, + configuration VARCHAR(10000000) NOT NULL, + CONSTRAINT uq_notification_template_name UNIQUE (tenant_id, name) +); +CREATE INDEX IF NOT EXISTS idx_notification_template_tenant_id_created_time ON notification_template(tenant_id, created_time DESC); + +CREATE TABLE IF NOT EXISTS notification_rule ( + id UUID NOT NULL CONSTRAINT notification_rule_pkey PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + name VARCHAR(255) NOT NULL, + template_id UUID NOT NULL CONSTRAINT fk_notification_rule_template_id REFERENCES notification_template(id), + trigger_type VARCHAR(50) NOT NULL, + trigger_config VARCHAR(1000) NOT NULL, + recipients_config VARCHAR(10000) NOT NULL, + additional_config VARCHAR(255), + CONSTRAINT uq_notification_rule_name UNIQUE (tenant_id, name) +); +CREATE INDEX IF NOT EXISTS idx_notification_rule_tenant_id_trigger_type_created_time ON notification_rule(tenant_id, trigger_type, created_time DESC); + +CREATE TABLE IF NOT EXISTS notification_request ( + id UUID NOT NULL CONSTRAINT notification_request_pkey PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + targets VARCHAR(10000) NOT NULL, + template_id UUID, + template VARCHAR(10000000), + info VARCHAR(1000), + additional_config VARCHAR(1000), + originator_entity_id UUID, + originator_entity_type VARCHAR(32), + rule_id UUID NULL, + status VARCHAR(32), + stats VARCHAR(10000) +); +CREATE INDEX IF NOT EXISTS idx_notification_request_tenant_id_user_created_time ON notification_request(tenant_id, created_time DESC) + WHERE originator_entity_type = 'USER'; +CREATE INDEX IF NOT EXISTS idx_notification_request_rule_id_originator_entity_id ON notification_request(rule_id, originator_entity_id) + WHERE originator_entity_type = 'ALARM'; +CREATE INDEX IF NOT EXISTS idx_notification_request_status ON notification_request(status) + WHERE status = 'SCHEDULED'; + +CREATE TABLE IF NOT EXISTS notification ( + id UUID NOT NULL, + created_time BIGINT NOT NULL, + request_id UUID NULL CONSTRAINT fk_notification_request_id REFERENCES notification_request(id) ON DELETE CASCADE, + recipient_id UUID NOT NULL CONSTRAINT fk_notification_recipient_id REFERENCES tb_user(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + subject VARCHAR(255), + body VARCHAR(1000) NOT NULL, + additional_config VARCHAR(1000), + status VARCHAR(32) +) PARTITION BY RANGE (created_time); +CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id); +CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC); + +-- NOTIFICATIONS END + +ALTER TABLE tb_user ADD COLUMN IF NOT EXISTS phone VARCHAR(255); + +CREATE TABLE IF NOT EXISTS user_settings ( + user_id uuid NOT NULL, + type VARCHAR(50) NOT NULL, + settings varchar(10000), + CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE, + CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type) +); + +-- TTL DROP PARTITIONS FUNCTIONS UPDATE START + +DROP PROCEDURE IF EXISTS drop_partitions_by_max_ttl(character varying, bigint, bigint); +DROP FUNCTION IF EXISTS get_partition_by_max_ttl_date; + +CREATE OR REPLACE FUNCTION get_partition_by_system_ttl_date(IN partition_type varchar, IN date timestamp, OUT partition varchar) AS +$$ +BEGIN + CASE + WHEN partition_type = 'DAYS' THEN + partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM') || '_' || to_char(date, 'dd'); + WHEN partition_type = 'MONTHS' THEN + partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM'); + WHEN partition_type = 'YEARS' THEN + partition := 'ts_kv_' || to_char(date, 'yyyy'); + ELSE + partition := NULL; + END CASE; + IF partition IS NOT NULL THEN + IF NOT EXISTS(SELECT + FROM pg_tables + WHERE schemaname = 'public' + AND tablename = partition) THEN + partition := NULL; + RAISE NOTICE 'Failed to found partition by ttl'; + END IF; + END IF; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE PROCEDURE drop_partitions_by_system_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint) + LANGUAGE plpgsql AS +$$ +DECLARE + date timestamp; + partition_by_max_ttl_date varchar; + partition_by_max_ttl_month varchar; + partition_by_max_ttl_day varchar; + partition_by_max_ttl_year varchar; + partition varchar; + partition_year integer; + partition_month integer; + partition_day integer; + +BEGIN + if system_ttl IS NOT NULL AND system_ttl > 0 THEN + date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - system_ttl); + partition_by_max_ttl_date := get_partition_by_system_ttl_date(partition_type, date); + RAISE NOTICE 'Date by max ttl: %', date; + RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date; + IF partition_by_max_ttl_date IS NOT NULL THEN + CASE + WHEN partition_type = 'DAYS' THEN + partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3); + partition_by_max_ttl_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4); + partition_by_max_ttl_day := SPLIT_PART(partition_by_max_ttl_date, '_', 5); + WHEN partition_type = 'MONTHS' THEN + partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3); + partition_by_max_ttl_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4); + ELSE + partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3); + END CASE; + IF partition_by_max_ttl_year IS NULL THEN + RAISE NOTICE 'Failed to remove partitions by max ttl date due to partition_by_max_ttl_year is null!'; + ELSE + IF partition_type = 'YEARS' THEN + FOR partition IN SELECT tablename + FROM pg_tables + WHERE schemaname = 'public' + AND tablename like 'ts_kv_' || '%' + AND tablename != 'ts_kv_latest' + AND tablename != 'ts_kv_dictionary' + AND tablename != 'ts_kv_indefinite' + AND tablename != partition_by_max_ttl_date + LOOP + partition_year := SPLIT_PART(partition, '_', 3)::integer; + IF partition_year < partition_by_max_ttl_year::integer THEN + RAISE NOTICE 'Partition to delete by max ttl: %', partition; + EXECUTE format('DROP TABLE IF EXISTS %I', partition); + deleted := deleted + 1; + END IF; + END LOOP; + ELSE + IF partition_type = 'MONTHS' THEN + IF partition_by_max_ttl_month IS NULL THEN + RAISE NOTICE 'Failed to remove months partitions by max ttl date due to partition_by_max_ttl_month is null!'; + ELSE + FOR partition IN SELECT tablename + FROM pg_tables + WHERE schemaname = 'public' + AND tablename like 'ts_kv_' || '%' + AND tablename != 'ts_kv_latest' + AND tablename != 'ts_kv_dictionary' + AND tablename != 'ts_kv_indefinite' + AND tablename != partition_by_max_ttl_date + LOOP + partition_year := SPLIT_PART(partition, '_', 3)::integer; + IF partition_year > partition_by_max_ttl_year::integer THEN + RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition; + CONTINUE; + ELSE + IF partition_year < partition_by_max_ttl_year::integer THEN + RAISE NOTICE 'Partition to delete by max ttl: %', partition; + EXECUTE format('DROP TABLE IF EXISTS %I', partition); + deleted := deleted + 1; + ELSE + partition_month := SPLIT_PART(partition, '_', 4)::integer; + IF partition_year = partition_by_max_ttl_year::integer THEN + IF partition_month >= partition_by_max_ttl_month::integer THEN + RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition; + CONTINUE; + ELSE + RAISE NOTICE 'Partition to delete by max ttl: %', partition; + EXECUTE format('DROP TABLE IF EXISTS %I', partition); + deleted := deleted + 1; + END IF; + END IF; + END IF; + END IF; + END LOOP; + END IF; + ELSE + IF partition_type = 'DAYS' THEN + IF partition_by_max_ttl_month IS NULL THEN + RAISE NOTICE 'Failed to remove days partitions by max ttl date due to partition_by_max_ttl_month is null!'; + ELSE + IF partition_by_max_ttl_day IS NULL THEN + RAISE NOTICE 'Failed to remove days partitions by max ttl date due to partition_by_max_ttl_day is null!'; + ELSE + FOR partition IN SELECT tablename + FROM pg_tables + WHERE schemaname = 'public' + AND tablename like 'ts_kv_' || '%' + AND tablename != 'ts_kv_latest' + AND tablename != 'ts_kv_dictionary' + AND tablename != 'ts_kv_indefinite' + AND tablename != partition_by_max_ttl_date + LOOP + partition_year := SPLIT_PART(partition, '_', 3)::integer; + IF partition_year > partition_by_max_ttl_year::integer THEN + RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition; + CONTINUE; + ELSE + IF partition_year < partition_by_max_ttl_year::integer THEN + RAISE NOTICE 'Partition to delete by max ttl: %', partition; + EXECUTE format('DROP TABLE IF EXISTS %I', partition); + deleted := deleted + 1; + ELSE + partition_month := SPLIT_PART(partition, '_', 4)::integer; + IF partition_month > partition_by_max_ttl_month::integer THEN + RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition; + CONTINUE; + ELSE + IF partition_month < partition_by_max_ttl_month::integer THEN + RAISE NOTICE 'Partition to delete by max ttl: %', partition; + EXECUTE format('DROP TABLE IF EXISTS %I', partition); + deleted := deleted + 1; + ELSE + partition_day := SPLIT_PART(partition, '_', 5)::integer; + IF partition_day >= partition_by_max_ttl_day::integer THEN + RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition; + CONTINUE; + ELSE + IF partition_day < partition_by_max_ttl_day::integer THEN + RAISE NOTICE 'Partition to delete by max ttl: %', partition; + EXECUTE format('DROP TABLE IF EXISTS %I', partition); + deleted := deleted + 1; + END IF; + END IF; + END IF; + END IF; + END IF; + END IF; + END LOOP; + END IF; + END IF; + END IF; + END IF; + END IF; + END IF; + END IF; + END IF; +END +$$; + +-- TTL DROP PARTITIONS FUNCTIONS UPDATE END + +-- RULE NODE SINGLETON MODE SUPPORT + +ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS singleton_mode bool DEFAULT false; + +UPDATE rule_node SET singleton_mode = true WHERE type IN ('org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode', 'org.thingsboard.rule.engine.mqtt.TbMqttNode'); + +ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS clustering_mode varchar(255) DEFAULT 'ENABLED'; + +UPDATE component_descriptor SET clustering_mode = 'USER_PREFERENCE' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.TbMqttNode'; + +UPDATE component_descriptor SET clustering_mode = 'SINGLETON' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode'; + diff --git a/application/src/main/data/upgrade/3.5.0/schema_update.sql b/application/src/main/data/upgrade/3.5.0/schema_update.sql new file mode 100644 index 0000000000..321112c4eb --- /dev/null +++ b/application/src/main/data/upgrade/3.5.0/schema_update.sql @@ -0,0 +1,23 @@ +-- +-- Copyright © 2016-2023 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +-- FIX DASHBOARD TEMPLATES AFTER ANGULAR MIGRATION TO VER.15 + +UPDATE dashboard SET configuration = REPLACE(configuration, 'mat-button mat-icon-button', 'mat-icon-button') + WHERE configuration like '%mat-button mat-icon-button%'; + +UPDATE widget_type SET descriptor = REPLACE(descriptor, 'mat-button mat-icon-button', 'mat-icon-button') + WHERE descriptor like '%mat-button mat-icon-button%'; diff --git a/application/src/main/data/upgrade/3.5.1/schema_update.sql b/application/src/main/data/upgrade/3.5.1/schema_update.sql new file mode 100644 index 0000000000..3bc2c99168 --- /dev/null +++ b/application/src/main/data/upgrade/3.5.1/schema_update.sql @@ -0,0 +1,54 @@ +-- +-- Copyright © 2016-2023 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +ALTER TABLE notification_rule ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT true; + +-- NOTIFICATION CONFIGS VERSION CONTROL START + +ALTER TABLE notification_template + ADD COLUMN IF NOT EXISTS external_id UUID; +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'uq_notification_template_external_id') THEN + ALTER TABLE notification_template ADD CONSTRAINT uq_notification_template_external_id UNIQUE (tenant_id, external_id); + END IF; + END; +$$; + +ALTER TABLE notification_target + ADD COLUMN IF NOT EXISTS external_id UUID; +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'uq_notification_target_external_id') THEN + ALTER TABLE notification_target ADD CONSTRAINT uq_notification_target_external_id UNIQUE (tenant_id, external_id); + END IF; + END; +$$; + +ALTER TABLE notification_rule + ADD COLUMN IF NOT EXISTS external_id UUID; +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'uq_notification_rule_external_id') THEN + ALTER TABLE notification_rule ADD CONSTRAINT uq_notification_rule_external_id UNIQUE (tenant_id, external_id); + END IF; + END; +$$; + +-- NOTIFICATION CONFIGS VERSION CONTROL END diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java index b4a0e019b6..b780f0e8cd 100644 --- a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java +++ b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java index a9c462ee05..0fe6915877 100644 --- a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java +++ b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index e1a41fe401..e0190bb388 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.actors; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -29,8 +28,11 @@ import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.SmsService; +import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.script.api.js.JsInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; @@ -50,6 +52,8 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.dao.alarm.AlarmCommentService; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; @@ -58,6 +62,7 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; @@ -65,6 +70,10 @@ import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; +import org.thingsboard.server.dao.notification.NotificationRequestService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -78,8 +87,10 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.queue.discovery.DiscoveryService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.component.ComponentDiscoveryService; @@ -87,6 +98,7 @@ import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.ExternalCallExecutorService; +import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.executors.SharedEventLoopGroupService; import org.thingsboard.server.service.mail.MailExecutorService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -138,8 +150,6 @@ public class ActorSystemContext { } }; - protected final ObjectMapper mapper = new ObjectMapper(); - private final ConcurrentMap debugPerTenantLimits = new ConcurrentHashMap<>(); public ConcurrentMap getDebugPerTenantLimits() { @@ -168,6 +178,10 @@ public class ActorSystemContext { @Setter private ComponentDiscoveryService componentService; + @Autowired + @Getter + private DiscoveryService discoveryService; + @Autowired @Getter private DataDecodingEncodingService encodingService; @@ -176,6 +190,14 @@ public class ActorSystemContext { @Getter private DeviceService deviceService; + @Autowired + @Getter + private DeviceProfileService deviceProfileService; + + @Autowired + @Getter + private AssetProfileService assetProfileService; + @Autowired @Getter private DeviceCredentialsService deviceCredentialsService; @@ -268,6 +290,10 @@ public class ActorSystemContext { @Getter private AlarmSubscriptionService alarmService; + @Autowired + @Getter + private AlarmCommentService alarmCommentService; + @Autowired @Getter private JsInvokeService jsInvokeService; @@ -292,6 +318,10 @@ public class ActorSystemContext { @Getter private ExternalCallExecutorService externalCallExecutorService; + @Autowired + @Getter + private NotificationExecutorService notificationExecutor; + @Autowired @Getter private SharedEventLoopGroupService sharedEventLoopGroupService; @@ -308,6 +338,34 @@ public class ActorSystemContext { @Getter private SmsSenderFactory smsSenderFactory; + @Autowired + @Getter + private NotificationCenter notificationCenter; + + @Autowired + @Getter + private NotificationRuleProcessor notificationRuleProcessor; + + @Autowired + @Getter + private NotificationTargetService notificationTargetService; + + @Autowired + @Getter + private NotificationTemplateService notificationTemplateService; + + @Autowired + @Getter + private NotificationRequestService notificationRequestService; + + @Autowired + @Getter + private NotificationRuleService notificationRuleService; + + @Autowired + @Getter + private SlackService slackService; + @Lazy @Autowired(required = false) @Getter @@ -586,7 +644,7 @@ public class ActorSystemContext { .dataType(tbMsg.getDataType().name()) .relationType(relationType) .data(tbMsg.getData()) - .metadata(mapper.writeValueAsString(tbMsg.getMetaData().getData())); + .metadata(JacksonUtil.toString(tbMsg.getMetaData().getData())); if (error != null) { event.error(toString(error)); @@ -596,7 +654,7 @@ public class ActorSystemContext { ListenableFuture future = eventService.saveAsync(event.build()); Futures.addCallback(future, RULE_NODE_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); - } catch (IOException ex) { + } catch (IllegalArgumentException ex) { log.warn("Failed to persist rule node debug message", ex); } } diff --git a/application/src/main/java/org/thingsboard/server/actors/TbEntityTypeActorIdPredicate.java b/application/src/main/java/org/thingsboard/server/actors/TbEntityTypeActorIdPredicate.java index 9a8232ebdb..ce2c239b41 100644 --- a/application/src/main/java/org/thingsboard/server/actors/TbEntityTypeActorIdPredicate.java +++ b/application/src/main/java/org/thingsboard/server/actors/TbEntityTypeActorIdPredicate.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index eeb6d8e82e..fb6fbbdff2 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppInitMsg.java b/application/src/main/java/org/thingsboard/server/actors/app/AppInitMsg.java index 772c9ae5b5..14bb93898e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppInitMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppInitMsg.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java index a0f49ba62f..d5833e0c07 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorCreator.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorCreator.java index 7a369a57e9..c5563dfaa8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorCreator.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorCreator.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 8a365b4e56..934f309480 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; @@ -116,7 +115,7 @@ import java.util.stream.Collectors; * @author Andrew Shvayka */ @Slf4j -class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { +public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { static final String SESSION_TIMEOUT_MESSAGE = "session timeout!"; final TenantId tenantId; @@ -816,7 +815,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } private ListenableFuture saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) { - ObjectNode body = mapper.createObjectNode(); + ObjectNode body = JacksonUtil.newObjectNode(); body.put("requestId", requestId); body.put("requestUUID", msg.getId().toString()); body.put("oneway", msg.isOneway()); diff --git a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java index 01e4765190..07fe24b272 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfoMetaData.java b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfoMetaData.java index a26fe90d65..7dd7525561 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfoMetaData.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfoMetaData.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.thingsboard.server.actors.device; import lombok.Data; -import org.thingsboard.server.gen.transport.TransportProtos.SessionType; /** * @author Andrew Shvayka diff --git a/application/src/main/java/org/thingsboard/server/actors/device/SessionTimeoutCheckMsg.java b/application/src/main/java/org/thingsboard/server/actors/device/SessionTimeoutCheckMsg.java index 6815aee675..e74baf8b3d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/SessionTimeoutCheckMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionTimeoutCheckMsg.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java b/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java index d003656874..f876408d24 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/device/ToServerRpcRequestMetadata.java b/application/src/main/java/org/thingsboard/server/actors/device/ToServerRpcRequestMetadata.java index c0a68c178d..f5d9e0595c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/ToServerRpcRequestMetadata.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/ToServerRpcRequestMetadata.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index ef2de35912..8b76924d96 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.thingsboard.server.actors.ruleChain; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.channel.EventLoopGroup; @@ -25,6 +24,7 @@ import org.bouncycastle.util.Arrays; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; @@ -35,6 +35,7 @@ import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.util.TenantIdLoader; import org.thingsboard.server.actors.ActorSystemContext; @@ -71,18 +72,25 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.alarm.AlarmCommentService; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; +import org.thingsboard.server.dao.notification.NotificationRequestService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -102,6 +110,7 @@ import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -111,8 +120,6 @@ import java.util.function.Consumer; @Slf4j class DefaultTbContext implements TbContext { - public final static ObjectMapper mapper = new ObjectMapper(); - private final ActorSystemContext mainCtx; private final String ruleChainName; private final RuleNodeCtx nodeCtx; @@ -412,8 +419,8 @@ class DefaultTbContext implements TbContext { public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, K profile) { try { - return entityActionMsg(id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)), action, profile); - } catch (JsonProcessingException | IllegalArgumentException e) { + return entityActionMsg(id, getActionMetaData(ruleNodeId), JacksonUtil.toString(JacksonUtil.valueToTree(entity)), action, profile); + } catch (IllegalArgumentException e) { throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + action + " msg: " + e); } } @@ -468,6 +475,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getExternalCallExecutorService(); } + @Override + public ListeningExecutor getNotificationExecutor() { + return mainCtx.getNotificationExecutor(); + } + @Override @Deprecated public ScriptEngine createJsScriptEngine(String script, String... argNames) { @@ -559,6 +571,16 @@ class DefaultTbContext implements TbContext { return mainCtx.getDeviceService(); } + @Override + public DeviceProfileService getDeviceProfileService() { + return mainCtx.getDeviceProfileService(); + } + + @Override + public AssetProfileService getAssetProfileService() { + return mainCtx.getAssetProfileService(); + } + @Override public DeviceCredentialsService getDeviceCredentialsService() { return mainCtx.getDeviceCredentialsService(); @@ -579,6 +601,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getAlarmService(); } + @Override + public AlarmCommentService getAlarmCommentService() { + return mainCtx.getAlarmCommentService(); + } + @Override public RuleChainService getRuleChainService() { return mainCtx.getRuleChainService(); @@ -667,6 +694,36 @@ class DefaultTbContext implements TbContext { return mainCtx.getSmsSenderFactory(); } + @Override + public NotificationCenter getNotificationCenter() { + return mainCtx.getNotificationCenter(); + } + + @Override + public NotificationTargetService getNotificationTargetService() { + return mainCtx.getNotificationTargetService(); + } + + @Override + public NotificationTemplateService getNotificationTemplateService() { + return mainCtx.getNotificationTemplateService(); + } + + @Override + public NotificationRequestService getNotificationRequestService() { + return mainCtx.getNotificationRequestService(); + } + + @Override + public NotificationRuleService getNotificationRuleService() { + return mainCtx.getNotificationRuleService(); + } + + @Override + public SlackService getSlackService() { + return mainCtx.getSlackService(); + } + @Override public RuleEngineRpcService getRpcService() { return mainCtx.getTbRuleEngineDeviceRpcService(); @@ -776,6 +833,12 @@ class DefaultTbContext implements TbContext { return metaData; } + + @Override + public void schedule(Runnable runnable, long delay, TimeUnit timeUnit) { + mainCtx.getScheduler().schedule(runnable, delay, timeUnit); + } + @Override public void checkTenantEntity(EntityId entityId) { if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java index ff9e21dedd..da45f0f53d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.thingsboard.server.actors.TbActor; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorId; import org.thingsboard.server.actors.TbEntityActorId; -import org.thingsboard.server.actors.service.ComponentActor; import org.thingsboard.server.actors.service.ContextBasedCreator; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; @@ -30,7 +29,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; -public class RuleChainActor extends ComponentActor { +public class RuleChainActor extends RuleEngineComponentActor { private final RuleChain ruleChain; @@ -101,6 +100,16 @@ public class RuleChainActor extends ComponentActor> extends ComponentActor { + + public RuleEngineComponentActor(ActorSystemContext systemContext, TenantId tenantId, T id) { + super(systemContext, tenantId, id); + } + + @Override + protected void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) { + super.logLifecycleEvent(event, e); + if (e instanceof TbRuleNodeUpdateException || (event == ComponentLifecycleEvent.STARTED && e != null)) { + return; + } + processNotificationRule(event, e); + } + + @Override + public void destroy(TbActorStopReason stopReason, Throwable cause) { + super.destroy(stopReason, cause); + if (stopReason == TbActorStopReason.INIT_FAILED && cause != null) { + processNotificationRule(ComponentLifecycleEvent.STARTED, cause); + } + } + + private void processNotificationRule(ComponentLifecycleEvent event, Throwable e) { + systemContext.getNotificationRuleProcessor().process(RuleEngineComponentLifecycleEventTrigger.builder() + .tenantId(tenantId) + .ruleChainId(getRuleChainId()) + .ruleChainName(getRuleChainName()) + .componentId(id) + .componentName(processor.getComponentName()) + .eventType(event) + .error(e) + .build()); + } + + protected abstract RuleChainId getRuleChainId(); + + protected abstract String getRuleChainName(); + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java index f5847f764b..e8dec535f6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import org.thingsboard.server.actors.TbActor; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorId; import org.thingsboard.server.actors.TbEntityActorId; -import org.thingsboard.server.actors.service.ComponentActor; import org.thingsboard.server.actors.service.ContextBasedCreator; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -32,7 +31,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; @Slf4j -public class RuleNodeActor extends ComponentActor { +public class RuleNodeActor extends RuleEngineComponentActor { private final String ruleChainName; private final RuleChainId ruleChainId; @@ -133,6 +132,16 @@ public class RuleNodeActor extends ComponentActor { private final String ruleChainName; - private final TbActorRef self; private final TbApiUsageReportClient apiUsageClient; + private final DefaultTbContext defaultCtx; private RuleNode ruleNode; private TbNode tbNode; - private DefaultTbContext defaultCtx; private RuleNodeInfo info; RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext @@ -51,7 +53,6 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor perTenantLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap perCustomerLimits = new ConcurrentHashMap<>(); + private final ThingsboardErrorResponseHandler errorResponseHandler; + private final RateLimitService rateLimitService; @Override public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { SecurityUser user = getCurrentUser(); if (user != null && !user.isSystemAdmin()) { - var profile = tenantProfileCache.get(user.getTenantId()); - if (profile == null) { + try { + if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId())) { + rateLimitExceeded(EntityType.TENANT, response); + return; + } + } catch (TenantProfileNotFoundException e) { log.debug("[{}] Failed to lookup tenant profile", user.getTenantId()); errorResponseHandler.handle(new BadCredentialsException("Failed to lookup tenant profile"), response); return; } - var profileConfiguration = profile.getDefaultProfileConfiguration(); - if (!checkRateLimits(user.getTenantId(), profileConfiguration.getTenantServerRestLimitsConfiguration(), perTenantLimits, response)) { - return; - } + if (user.isCustomerUser()) { - if (!checkRateLimits(user.getCustomerId(), profileConfiguration.getCustomerServerRestLimitsConfiguration(), perCustomerLimits, response)) { + if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId(), user.getCustomerId())) { + rateLimitExceeded(EntityType.CUSTOMER, response); return; } } @@ -90,23 +79,8 @@ public class RateLimitProcessingFilter extends OncePerRequestFilter { return false; } - private boolean checkRateLimits(I ownerId, String rateLimitConfig, Map rateLimitsMap, ServletResponse response) { - if (StringUtils.isNotEmpty(rateLimitConfig)) { - TbRateLimits rateLimits = rateLimitsMap.get(ownerId); - if (rateLimits == null || !rateLimits.getConfiguration().equals(rateLimitConfig)) { - rateLimits = new TbRateLimits(rateLimitConfig); - rateLimitsMap.put(ownerId, rateLimits); - } - - if (!rateLimits.tryConsume()) { - errorResponseHandler.handle(new TbRateLimitsException(ownerId.getEntityType()), (HttpServletResponse) response); - return false; - } - } else { - rateLimitsMap.remove(ownerId); - } - - return true; + private void rateLimitExceeded(EntityType type, HttpServletResponse response) { + errorResponseHandler.handle(new TbRateLimitsException(type), response); } protected SecurityUser getCurrentUser() { diff --git a/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java index e23b629fed..1b9ee9f71d 100644 --- a/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index c53cf1b504..4415f1a0bf 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.security.Authority; @@ -86,6 +87,8 @@ import static springfox.documentation.builders.PathSelectors.regex; @Profile("!test") public class SwaggerConfiguration { + @Value("${swagger.enabled:true}") + private boolean enabled; @Value("${swagger.api_path_regex}") private String apiPathRegex; @Value("${swagger.security_path_regex}") @@ -115,6 +118,7 @@ public class SwaggerConfiguration { public Docket thingsboardApi() { TypeResolver typeResolver = new TypeResolver(); return new Docket(DocumentationType.OAS_30) + .enable(enabled) .groupName("thingsboard") .apiInfo(apiInfo()) .additionalModels( @@ -138,6 +142,7 @@ public class SwaggerConfiguration { ) .securitySchemes(newArrayList(httpLogin())) .securityContexts(newArrayList(securityContext())) + .ignoredParameterTypes(AuthenticationPrincipal.class) .enableUrlTemplating(true); } diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java index 112fa5c122..1d0437d56a 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index a6540f007a..a418dc268b 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.config; -import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -30,6 +29,7 @@ import org.springframework.security.config.annotation.authentication.builders.Au import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; @@ -115,20 +115,18 @@ public class ThingsboardSecurityConfiguration { @Autowired private AuthenticationManager authenticationManager; - @Autowired private ObjectMapper objectMapper; - @Autowired private RateLimitProcessingFilter rateLimitProcessingFilter; @Bean protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception { - RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper); + RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler); filter.setAuthenticationManager(this.authenticationManager); return filter; } @Bean protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception { - RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper); + RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler); filter.setAuthenticationManager(this.authenticationManager); return filter; } @@ -146,7 +144,7 @@ public class ThingsboardSecurityConfiguration { @Bean protected RefreshTokenProcessingFilter buildRefreshTokenProcessingFilter() throws Exception { - RefreshTokenProcessingFilter filter = new RefreshTokenProcessingFilter(TOKEN_REFRESH_ENTRY_POINT, successHandler, failureHandler, objectMapper); + RefreshTokenProcessingFilter filter = new RefreshTokenProcessingFilter(TOKEN_REFRESH_ENTRY_POINT, successHandler, failureHandler); filter.setAuthenticationManager(this.authenticationManager); return filter; } @@ -181,15 +179,8 @@ public class ThingsboardSecurityConfiguration { private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver; @Bean - @Order(0) - SecurityFilterChain resources(HttpSecurity http) throws Exception { - http - .requestMatchers((matchers) -> matchers.antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**")) - .authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll()) - .requestCache().disable() - .securityContext().disable() - .sessionManagement().disable(); - return http.build(); + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"); } @Bean diff --git a/application/src/main/java/org/thingsboard/server/config/WebConfig.java b/application/src/main/java/org/thingsboard/server/config/WebConfig.java index 012d57f3bd..65396576c1 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebConfig.java +++ b/application/src/main/java/org/thingsboard/server/config/WebConfig.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java index 13b13123e6..9a6ffb0e32 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ package org.thingsboard.server.config; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; @@ -40,11 +42,15 @@ import java.util.Map; @Configuration @TbCoreComponent @EnableWebSocket +@RequiredArgsConstructor +@Slf4j public class WebSocketConfiguration implements WebSocketConfigurer { public static final String WS_PLUGIN_PREFIX = "/api/ws/plugins/"; private static final String WS_PLUGIN_MAPPING = WS_PLUGIN_PREFIX + "**"; + private final WebSocketHandler wsHandler; + @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); @@ -55,7 +61,11 @@ public class WebSocketConfiguration implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*") + if (!(wsHandler instanceof TbWebSocketHandler)) { + log.error("TbWebSocketHandler expected but [{}] provided", wsHandler); + throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided"); + } + registry.addHandler(wsHandler, WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*") .addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() { @Override @@ -82,11 +92,6 @@ public class WebSocketConfiguration implements WebSocketConfigurer { }); } - @Bean - public WebSocketHandler wsHandler() { - return new TbWebSocketHandler(); - } - protected SecurityUser getCurrentUser() throws ThingsboardException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) { diff --git a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java index e5a217893e..32797801c0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,13 +37,13 @@ import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest; +import org.thingsboard.server.exception.ToErrorResponseEntity; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.rpc.LocalRequestMetaData; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; -import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity; import javax.annotation.Nullable; import java.util.Optional; diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 29f32b8ac9..5cf97a7cda 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,29 +21,42 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.FeaturesInfo; +import org.thingsboard.server.common.data.SystemInfo; import org.thingsboard.server.common.data.UpdateMessage; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.sms.config.TestSmsRequest; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo; -import org.thingsboard.server.common.data.security.model.JwtSettings; -import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; +import org.thingsboard.server.common.data.sync.vc.VcUtils; +import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.permission.Operation; @@ -51,43 +64,31 @@ import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService; +import org.thingsboard.server.service.system.SystemInfoService; import org.thingsboard.server.service.update.UpdateService; -import static org.thingsboard.server.controller.ControllerConstants.*; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; @RestController @TbCoreComponent @RequestMapping("/api/admin") +@RequiredArgsConstructor public class AdminController extends BaseController { - @Autowired - private MailService mailService; - - @Autowired - private SmsService smsService; - - @Autowired - private AdminSettingsService adminSettingsService; - - @Autowired - private SystemSecurityService systemSecurityService; - + private final MailService mailService; + private final SmsService smsService; + private final AdminSettingsService adminSettingsService; + private final SystemSecurityService systemSecurityService; @Lazy - @Autowired - private JwtSettingsService jwtSettingsService; - + private final JwtSettingsService jwtSettingsService; @Lazy - @Autowired - private JwtTokenFactory tokenFactory; - - @Autowired - private EntitiesVersionControlService versionControlService; - - @Autowired - private TbAutoCommitSettingsService autoCommitSettingsService; - - @Autowired - private UpdateService updateService; + private final JwtTokenFactory tokenFactory; + private final EntitiesVersionControlService versionControlService; + private final TbAutoCommitSettingsService autoCommitSettingsService; + private final UpdateService updateService; + private final SystemInfoService systemInfoService; + private final AuditLogService auditLogService; @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @@ -97,19 +98,14 @@ public class AdminController extends BaseController { public AdminSettings getAdminSettings( @ApiParam(value = "A string value of the key (e.g. 'general' or 'mail').") @PathVariable("key") String key) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); - AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key); - if (adminSettings.getKey().equals("mail")) { - ((ObjectNode) adminSettings.getJsonValue()).remove("password"); - } - return adminSettings; - } catch (Exception e) { - throw handleException(e); + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key); + if (adminSettings.getKey().equals("mail")) { + ((ObjectNode) adminSettings.getJsonValue()).remove("password"); } + return adminSettings; } - @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", notes = "Creates or Updates the Administration Settings. Platform generates random Administration Settings Id during settings creation. " + "The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " + @@ -120,20 +116,16 @@ public class AdminController extends BaseController { public AdminSettings saveAdminSettings( @ApiParam(value = "A JSON value representing the Administration Settings.") @RequestBody AdminSettings adminSettings) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); - adminSettings.setTenantId(getTenantId()); - adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); - if (adminSettings.getKey().equals("mail")) { - mailService.updateMailConfiguration(); - ((ObjectNode) adminSettings.getJsonValue()).remove("password"); - } else if (adminSettings.getKey().equals("sms")) { - smsService.updateSmsConfiguration(); - } - return adminSettings; - } catch (Exception e) { - throw handleException(e); + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); + adminSettings.setTenantId(getTenantId()); + adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); + if (adminSettings.getKey().equals("mail")) { + mailService.updateMailConfiguration(); + ((ObjectNode) adminSettings.getJsonValue()).remove("password"); + } else if (adminSettings.getKey().equals("sms")) { + smsService.updateSmsConfiguration(); } + return adminSettings; } @ApiOperation(value = "Get the Security Settings object", @@ -142,12 +134,8 @@ public class AdminController extends BaseController { @RequestMapping(value = "/securitySettings", method = RequestMethod.GET) @ResponseBody public SecuritySettings getSecuritySettings() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); - return checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID)); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + return checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID)); } @ApiOperation(value = "Update Security Settings (saveSecuritySettings)", @@ -158,13 +146,9 @@ public class AdminController extends BaseController { public SecuritySettings saveSecuritySettings( @ApiParam(value = "A JSON value representing the Security Settings.") @RequestBody SecuritySettings securitySettings) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); - securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(TenantId.SYS_TENANT_ID, securitySettings)); - return securitySettings; - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); + securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(TenantId.SYS_TENANT_ID, securitySettings)); + return securitySettings; } @ApiOperation(value = "Get the JWT Settings object (getJwtSettings)", @@ -174,12 +158,8 @@ public class AdminController extends BaseController { @RequestMapping(value = "/jwtSettings", method = RequestMethod.GET) @ResponseBody public JwtSettings getJwtSettings() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); - return checkNotNull(jwtSettingsService.getJwtSettings()); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + return checkNotNull(jwtSettingsService.getJwtSettings()); } @ApiOperation(value = "Update JWT Settings (saveJwtSettings)", @@ -191,14 +171,10 @@ public class AdminController extends BaseController { public JwtPair saveJwtSettings( @ApiParam(value = "A JSON value representing the JWT Settings.") @RequestBody JwtSettings jwtSettings) throws ThingsboardException { - try { - SecurityUser securityUser = getCurrentUser(); - accessControlService.checkPermission(securityUser, Resource.ADMIN_SETTINGS, Operation.WRITE); - checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings)); - return tokenFactory.createTokenPair(securityUser); - } catch (Exception e) { - throw handleException(e); - } + SecurityUser securityUser = getCurrentUser(); + accessControlService.checkPermission(securityUser, Resource.ADMIN_SETTINGS, Operation.WRITE); + checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings)); + return tokenFactory.createTokenPair(securityUser); } @ApiOperation(value = "Send test email (sendTestMail)", @@ -209,19 +185,15 @@ public class AdminController extends BaseController { public void sendTestMail( @ApiParam(value = "A JSON value representing the Mail Settings.") @RequestBody AdminSettings adminSettings) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); - adminSettings = checkNotNull(adminSettings); - if (adminSettings.getKey().equals("mail")) { - if (!adminSettings.getJsonValue().has("password")) { - AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); - ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); - } - String email = getCurrentUser().getEmail(); - mailService.sendTestMail(adminSettings.getJsonValue(), email); + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + adminSettings = checkNotNull(adminSettings); + if (adminSettings.getKey().equals("mail")) { + if (!adminSettings.getJsonValue().has("password")) { + AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); + ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); } - } catch (Exception e) { - throw handleException(e); + String email = getCurrentUser().getEmail(); + mailService.sendTestMail(adminSettings.getJsonValue(), email); } } @@ -233,11 +205,14 @@ public class AdminController extends BaseController { public void sendTestSms( @ApiParam(value = "A JSON value representing the Test SMS request.") @RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ); try { - accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); smsService.sendTestSms(testSmsRequest); - } catch (Exception e) { - throw handleException(e); + auditLogService.logEntityAction(user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), user, ActionType.SMS_SENT, null, testSmsRequest.getNumberTo()); + } catch (ThingsboardException e) { + auditLogService.logEntityAction(user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), user, ActionType.SMS_SENT, e, testSmsRequest.getNumberTo()); + throw e; } } @@ -246,16 +221,12 @@ public class AdminController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping("/repositorySettings") public RepositorySettings getRepositorySettings() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - RepositorySettings versionControlSettings = checkNotNull(versionControlService.getVersionControlSettings(getTenantId())); - versionControlSettings.setPassword(null); - versionControlSettings.setPrivateKey(null); - versionControlSettings.setPrivateKeyPassword(null); - return versionControlSettings; - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + RepositorySettings versionControlSettings = checkNotNull(versionControlService.getVersionControlSettings(getTenantId())); + versionControlSettings.setPassword(null); + versionControlSettings.setPrivateKey(null); + versionControlSettings.setPrivateKeyPassword(null); + return versionControlSettings; } @ApiOperation(value = "Check repository settings exists (repositorySettingsExists)", @@ -263,12 +234,8 @@ public class AdminController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping("/repositorySettings/exists") public Boolean repositorySettingsExists() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - return versionControlService.getVersionControlSettings(getTenantId()) != null; - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + return versionControlService.getVersionControlSettings(getTenantId()) != null; } @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -309,30 +276,21 @@ public class AdminController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public DeferredResult deleteRepositorySettings() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); - return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId())); - } catch (Exception e) { - throw handleException(e); - } + public DeferredResult deleteRepositorySettings() throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); + return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId())); } - @ApiOperation(value = "Check repository access (checkRepositoryAccess)", notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST) public DeferredResult checkRepositoryAccess( @ApiParam(value = "A JSON value representing the Repository Settings.") - @RequestBody RepositorySettings settings) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - settings = checkNotNull(settings); - return wrapFuture(versionControlService.checkVersionControlAccess(getTenantId(), settings)); - } catch (Exception e) { - throw handleException(e); - } + @RequestBody RepositorySettings settings) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + settings = checkNotNull(settings); + return wrapFuture(versionControlService.checkVersionControlAccess(getTenantId(), settings)); } @ApiOperation(value = "Get auto commit settings (getAutoCommitSettings)", @@ -340,12 +298,8 @@ public class AdminController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping("/autoCommitSettings") public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - return checkNotNull(autoCommitSettingsService.get(getTenantId())); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + return checkNotNull(autoCommitSettingsService.get(getTenantId())); } @ApiOperation(value = "Check auto commit settings exists (autoCommitSettingsExists)", @@ -353,12 +307,8 @@ public class AdminController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping("/autoCommitSettings/exists") public Boolean autoCommitSettingsExists() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - return autoCommitSettingsService.get(getTenantId()) != null; - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + return autoCommitSettingsService.get(getTenantId()) != null; } @ApiOperation(value = "Creates or Updates the auto commit settings (saveAutoCommitSettings)", @@ -366,6 +316,7 @@ public class AdminController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/autoCommitSettings") public AutoCommitSettings saveAutoCommitSettings(@RequestBody AutoCommitSettings settings) throws ThingsboardException { + settings.values().forEach(config -> VcUtils.checkBranchName(config.getBranch())); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE); return autoCommitSettingsService.save(getTenantId(), settings); } @@ -377,12 +328,8 @@ public class AdminController extends BaseController { @RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) public void deleteAutoCommitSettings() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); - autoCommitSettingsService.delete(getTenantId()); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); + autoCommitSettingsService.delete(getTenantId()); } @ApiOperation(value = "Check for new Platform Releases (checkUpdates)", @@ -392,11 +339,27 @@ public class AdminController extends BaseController { @RequestMapping(value = "/updates", method = RequestMethod.GET) @ResponseBody public UpdateMessage checkUpdates() throws ThingsboardException { - try { - return updateService.checkUpdates(); - } catch (Exception e) { - throw handleException(e); - } + return updateService.checkUpdates(); + } + + @ApiOperation(value = "Get system info (getSystemInfo)", + notes = "Get main information about system. " + + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/systemInfo", method = RequestMethod.GET) + @ResponseBody + public SystemInfo getSystemInfo() throws ThingsboardException { + return systemInfoService.getSystemInfo(); + } + + @ApiOperation(value = "Get features info (getFeaturesInfo)", + notes = "Get information about enabled/disabled features. " + + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/featuresInfo", method = RequestMethod.GET) + @ResponseBody + public FeaturesInfo getFeaturesInfo() { + return systemInfoService.getFeaturesInfo(); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java new file mode 100644 index 0000000000..92b2cb923f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java @@ -0,0 +1,124 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService; +import org.thingsboard.server.service.security.permission.Operation; + +import static org.thingsboard.server.controller.ControllerConstants.ALARM_COMMENT_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_COMMENT_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequiredArgsConstructor +@RequestMapping("/api") +public class AlarmCommentController extends BaseController { + public static final String ALARM_ID = "alarmId"; + public static final String ALARM_COMMENT_ID = "commentId"; + + private final TbAlarmCommentService tbAlarmCommentService; + + @ApiOperation(value = "Create or update Alarm Comment ", + notes = "Creates or Updates the Alarm Comment. " + + "When creating comment, platform generates Alarm Comment Id as " + UUID_WIKI_LINK + + "The newly created Alarm Comment id will be present in the response. Specify existing Alarm Comment id to update the alarm. " + + "Referencing non-existing Alarm Comment Id will cause 'Not Found' error. " + + "\n\n To create new Alarm comment entity it is enough to specify 'comment' json element with 'text' node, for example: {\"comment\": { \"text\": \"my comment\"}}. " + + "\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + , produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.POST) + @ResponseBody + public AlarmComment saveAlarmComment(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId, @ApiParam(value = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + alarmComment.setAlarmId(alarmId); + return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser()); + } + + @ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)", + notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/comment/{commentId}", method = RequestMethod.DELETE) + @ResponseBody + public void deleteAlarmComment(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @ApiParam(value = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + + AlarmCommentId alarmCommentId = new AlarmCommentId(toUUID(strCommentId)); + AlarmComment alarmComment = checkAlarmCommentId(alarmCommentId, alarmId); + tbAlarmCommentService.deleteAlarmComment(alarm, alarmComment, getCurrentUser()); + } + + @ApiOperation(value = "Get Alarm comments (getAlarmComments)", + notes = "Returns a page of alarm comments for specified alarm. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.GET) + @ResponseBody + public PageData getAlarmComments( + @ApiParam(value = ALARM_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(ALARM_ID) String strAlarmId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_COMMENT_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder + ) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); + return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink)); + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 858a7ee3c4..44263d19bd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; @@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -48,9 +50,18 @@ import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ASSIGNEE_ID; +import static org.thingsboard.server.controller.ControllerConstants.ASSIGN_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE; @@ -76,9 +87,16 @@ public class AlarmController extends BaseController { private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " + "If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. "; private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value"; + + private static final String ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSearchStatus enumeration value"; private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK"; private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value"; private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK"; + private static final String ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSeverity enumeration value"; + private static final String ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES = "CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE"; + + private static final String ALARM_QUERY_TYPE_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing alarm types"; + private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status"; private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; @@ -93,12 +111,8 @@ public class AlarmController extends BaseController { public Alarm getAlarmById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); - try { - AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); - return checkAlarmId(alarmId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + return checkAlarmId(alarmId, Operation.READ); } @ApiOperation(value = "Get Alarm Info (getAlarmInfoById)", @@ -110,15 +124,11 @@ public class AlarmController extends BaseController { public AlarmInfo getAlarmInfoById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); - try { - AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); - return checkAlarmInfoId(alarmId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + return checkAlarmInfoId(alarmId, Operation.READ); } - @ApiOperation(value = "Create or update Alarm (saveAlarm)", + @ApiOperation(value = "Create or Update Alarm (saveAlarm)", notes = "Creates or Updates the Alarm. " + "When creating alarm, platform generates Alarm Id as " + UUID_WIKI_LINK + "The newly created Alarm id will be present in the response. Specify existing Alarm id to update the alarm. " + @@ -135,7 +145,12 @@ public class AlarmController extends BaseController { @ResponseBody public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { alarm.setTenantId(getTenantId()); + checkNotNull(alarm.getOriginator()); checkEntity(alarm.getId(), alarm, Resource.ALARM); + checkEntityId(alarm.getOriginator(), Operation.READ); + if (alarm.getAssigneeId() != null) { + checkUserId(alarm.getAssigneeId(), Operation.READ); + } return tbAlarmService.save(alarm, getCurrentUser()); } @@ -158,11 +173,12 @@ public class AlarmController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + public AlarmInfo ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { checkParameter(ALARM_ID, strAlarmId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - tbAlarmService.ack(alarm, getCurrentUser()).get(); + //TODO: return correct error code if the alarm is not found or already cleared + return tbAlarmService.ack(alarm, getCurrentUser()); } @ApiOperation(value = "Clear Alarm (clearAlarm)", @@ -172,11 +188,50 @@ public class AlarmController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + public AlarmInfo clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + //TODO: return correct error code if the alarm is not found or already cleared + return tbAlarmService.clear(alarm, getCurrentUser()); + } + + @ApiOperation(value = "Assign/Reassign Alarm (assignAlarm)", + notes = "Assign the Alarm. " + + "Once assigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_ASSIGNED' " + + "(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/assign/{assigneeId}", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public Alarm assignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId, + @ApiParam(value = ASSIGN_ID_PARAM_DESCRIPTION) + @PathVariable(ASSIGNEE_ID) String strAssigneeId + ) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + checkParameter(ASSIGNEE_ID, strAssigneeId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + UserId assigneeId = new UserId(UUID.fromString(strAssigneeId)); + checkUserId(assigneeId, Operation.READ); + return tbAlarmService.assign(alarm, assigneeId, System.currentTimeMillis(), getCurrentUser()); + } + + @ApiOperation(value = "Unassign Alarm (unassignAlarm)", + notes = "Unassign the Alarm. " + + "Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/assign", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public Alarm unassignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId + ) throws Exception { checkParameter(ALARM_ID, strAlarmId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - tbAlarmService.clear(alarm, getCurrentUser()).get(); + return tbAlarmService.unassign(alarm, System.currentTimeMillis(), getCurrentUser()); } @ApiOperation(value = "Get Alarms (getAlarms)", @@ -194,6 +249,8 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -210,7 +267,7 @@ public class AlarmController extends BaseController { @RequestParam(required = false) Long endTime, @ApiParam(value = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @RequestParam(required = false) Boolean fetchOriginator - ) throws ThingsboardException { + ) throws ThingsboardException, ExecutionException, InterruptedException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); @@ -221,13 +278,13 @@ public class AlarmController extends BaseController { "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } checkEntityId(entityId, Operation.READ); + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - try { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } @ApiOperation(value = "Get All Alarms (getAllAlarms)", @@ -244,6 +301,8 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -260,23 +319,149 @@ public class AlarmController extends BaseController { @RequestParam(required = false) Long endTime, @ApiParam(value = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @RequestParam(required = false) Boolean fetchOriginator - ) throws ThingsboardException { + ) throws ThingsboardException, ExecutionException, InterruptedException { AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus); AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status); if (alarmSearchStatus != null && alarmStatus != null) { throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " + "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + + if (getCurrentUser().isCustomerUser()) { + return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); + } else { + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); + } + } + + @ApiOperation(value = "Get Alarms (getAlarmsV2)", + notes = "Returns a page of alarms for the selected entity. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/v2/alarm/{entityType}/{entityId}", method = RequestMethod.GET) + @ResponseBody + public PageData getAlarmsV2( + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") + @PathVariable(ENTITY_TYPE) String strEntityType, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_ID) String strEntityId, + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) + @RequestParam(required = false) String[] statusList, + @ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES) + @RequestParam(required = false) String[] severityList, + @ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION) + @RequestParam(required = false) String[] typeList, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder, + @ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION) + @RequestParam(required = false) Long startTime, + @ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION) + @RequestParam(required = false) Long endTime + ) throws ThingsboardException, ExecutionException, InterruptedException { + checkParameter("EntityId", strEntityId); + checkParameter("EntityType", strEntityType); + EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); + checkEntityId(entityId, Operation.READ); + List alarmStatusList = new ArrayList<>(); + if (statusList != null) { + for (String strStatus : statusList) { + if (!StringUtils.isEmpty(strStatus)) { + alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus)); + } + } + } + List alarmSeverityList = new ArrayList<>(); + if (severityList != null) { + for (String strSeverity : severityList) { + if (!StringUtils.isEmpty(strSeverity)) { + alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity)); + } + } + } + List alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList(); + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - try { - if (getCurrentUser().isCustomerUser()) { - return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); - } else { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(entityId, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get()); + } + + @ApiOperation(value = "Get All Alarms (getAllAlarmsV2)", + notes = "Returns a page of alarms that belongs to the current user owner. " + + "If the user has the authority of 'Tenant Administrator', the server returns alarms that belongs to the tenant of current user. " + + "If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/v2/alarms", method = RequestMethod.GET) + @ResponseBody + public PageData getAllAlarmsV2( + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) + @RequestParam(required = false) String[] statusList, + @ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES) + @RequestParam(required = false) String[] severityList, + @ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION) + @RequestParam(required = false) String[] typeList, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder, + @ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION) + @RequestParam(required = false) Long startTime, + @ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION) + @RequestParam(required = false) Long endTime + ) throws ThingsboardException, ExecutionException, InterruptedException { + List alarmStatusList = new ArrayList<>(); + if (statusList != null) { + for (String strStatus : statusList) { + if (!StringUtils.isEmpty(strStatus)) { + alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus)); + } } - } catch (Exception e) { - throw handleException(e); + } + List alarmSeverityList = new ArrayList<>(); + if (severityList != null) { + for (String strSeverity : severityList) { + if (!StringUtils.isEmpty(strSeverity)) { + alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity)); + } + } + } + List alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList(); + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + + if (getCurrentUser().isCustomerUser()) { + return checkNotNull(alarmService.findCustomerAlarmsV2(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get()); + } else { + return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get()); } } @@ -295,7 +480,9 @@ public class AlarmController extends BaseController { @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) - @RequestParam(required = false) String status + @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId ) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); @@ -307,11 +494,8 @@ public class AlarmController extends BaseController { "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } checkEntityId(entityId, Operation.READ); - try { - return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus); - } catch (Exception e) { - throw handleException(e); - } + return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, + alarmStatus, assigneeId); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 968829b65a..510fefe305 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,6 +60,7 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.ASSET_ID_PARAM_DESCRIPTION; @@ -108,12 +109,8 @@ public class AssetController extends BaseController { public Asset getAssetById(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(ASSET_ID, strAssetId); - try { - AssetId assetId = new AssetId(toUUID(strAssetId)); - return checkAssetId(assetId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + AssetId assetId = new AssetId(toUUID(strAssetId)); + return checkAssetId(assetId, Operation.READ); } @ApiOperation(value = "Get Asset Info (getAssetInfoById)", @@ -127,12 +124,8 @@ public class AssetController extends BaseController { public AssetInfo getAssetInfoById(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(ASSET_ID, strAssetId); - try { - AssetId assetId = new AssetId(toUUID(strAssetId)); - return checkAssetInfoId(assetId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + AssetId assetId = new AssetId(toUUID(strAssetId)); + return checkAssetInfoId(assetId, Operation.READ); } @ApiOperation(value = "Create Or Update Asset (saveAsset)", @@ -228,16 +221,12 @@ public class AssetController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink)); - } else { - return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink)); } } @@ -262,19 +251,15 @@ public class AssetController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(assetService.findAssetInfosByTenantIdAndType(tenantId, type, pageLink)); - } else if (assetProfileId != null && assetProfileId.length() > 0) { - AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId)); - return checkNotNull(assetService.findAssetInfosByTenantIdAndAssetProfileId(tenantId, profileId, pageLink)); - } else { - return checkNotNull(assetService.findAssetInfosByTenantId(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(assetService.findAssetInfosByTenantIdAndType(tenantId, type, pageLink)); + } else if (assetProfileId != null && assetProfileId.length() > 0) { + AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId)); + return checkNotNull(assetService.findAssetInfosByTenantIdAndAssetProfileId(tenantId, profileId, pageLink)); + } else { + return checkNotNull(assetService.findAssetInfosByTenantId(tenantId, pageLink)); } } @@ -287,12 +272,8 @@ public class AssetController extends BaseController { public Asset getTenantAsset( @ApiParam(value = ASSET_NAME_DESCRIPTION) @RequestParam String assetName) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName)); } @ApiOperation(value = "Get Customer Assets (getCustomerAssets)", @@ -317,18 +298,14 @@ public class AssetController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); - } else { - return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); + } else { + return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } } @@ -356,21 +333,17 @@ public class AssetController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); - } else if (assetProfileId != null && assetProfileId.length() > 0) { - AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId)); - return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(tenantId, customerId, profileId, pageLink)); - } else { - return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); + } else if (assetProfileId != null && assetProfileId.length() > 0) { + AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId)); + return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(tenantId, customerId, profileId, pageLink)); + } else { + return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } } @@ -381,26 +354,22 @@ public class AssetController extends BaseController { @ResponseBody public List getAssetsByIds( @ApiParam(value = "A list of assets ids, separated by comma ','") - @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException { + @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("assetIds", strAssetIds); - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - CustomerId customerId = user.getCustomerId(); - List assetIds = new ArrayList<>(); - for (String strAssetId : strAssetIds) { - assetIds.add(new AssetId(toUUID(strAssetId))); - } - ListenableFuture> assets; - if (customerId == null || customerId.isNullUid()) { - assets = assetService.findAssetsByTenantIdAndIdsAsync(tenantId, assetIds); - } else { - assets = assetService.findAssetsByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, assetIds); - } - return checkNotNull(assets.get()); - } catch (Exception e) { - throw handleException(e); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = user.getCustomerId(); + List assetIds = new ArrayList<>(); + for (String strAssetId : strAssetIds) { + assetIds.add(new AssetId(toUUID(strAssetId))); } + ListenableFuture> assets; + if (customerId == null || customerId.isNullUid()) { + assets = assetService.findAssetsByTenantIdAndIdsAsync(tenantId, assetIds); + } else { + assets = assetService.findAssetsByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, assetIds); + } + return checkNotNull(assets.get()); } @ApiOperation(value = "Find related assets (findByQuery)", @@ -410,25 +379,21 @@ public class AssetController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/assets", method = RequestMethod.POST) @ResponseBody - public List findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException { + public List findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getAssetTypes()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); - try { - List assets = checkNotNull(assetService.findAssetsByQuery(getTenantId(), query).get()); - assets = assets.stream().filter(asset -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, Operation.READ, asset.getId(), asset); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); - return assets; - } catch (Exception e) { - throw handleException(e); - } + List assets = checkNotNull(assetService.findAssetsByQuery(getTenantId(), query).get()); + assets = assets.stream().filter(asset -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, Operation.READ, asset.getId(), asset); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + return assets; } @ApiOperation(value = "Get Asset Types (getAssetTypes)", @@ -436,15 +401,11 @@ public class AssetController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/asset/types", method = RequestMethod.GET) @ResponseBody - public List getAssetTypes() throws ThingsboardException { - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - ListenableFuture> assetTypes = assetService.findAssetTypesByTenantId(tenantId); - return checkNotNull(assetTypes.get()); - } catch (Exception e) { - throw handleException(e); - } + public List getAssetTypes() throws ThingsboardException, ExecutionException, InterruptedException { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + ListenableFuture> assetTypes = assetService.findAssetTypesByTenantId(tenantId); + return checkNotNull(assetTypes.get()); } @ApiOperation(value = "Assign asset to edge (assignAssetToEdge)", @@ -520,33 +481,29 @@ public class AssetController extends BaseController { @ApiParam(value = "Timestamp. Assets with creation time after it won't be queried") @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - checkEdgeId(edgeId, Operation.READ); - TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - PageData nonFilteredResult; - if (type != null && type.trim().length() > 0) { - nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); - } else { - nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); - } - List filteredAssets = nonFilteredResult.getData().stream().filter(asset -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, Operation.READ, asset.getId(), asset); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); - PageData filteredResult = new PageData<>(filteredAssets, - nonFilteredResult.getTotalPages(), - nonFilteredResult.getTotalElements(), - nonFilteredResult.hasNext()); - return checkNotNull(filteredResult); - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult; + if (type != null && type.trim().length() > 0) { + nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); + } else { + nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); } + List filteredAssets = nonFilteredResult.getData().stream().filter(asset -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, Operation.READ, asset.getId(), asset); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredAssets, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); } @ApiOperation(value = "Import the bulk of assets (processAssetsBulkImport)", diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index 2e84dd6634..47a7eede4b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -36,7 +35,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService; import org.thingsboard.server.service.security.permission.Operation; @@ -78,12 +76,8 @@ public class AssetProfileController extends BaseController { @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException { checkParameter(ASSET_PROFILE_ID, strAssetProfileId); - try { - AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); - return checkAssetProfileId(assetProfileId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); + return checkAssetProfileId(assetProfileId, Operation.READ); } @ApiOperation(value = "Get Asset Profile Info (getAssetProfileInfoById)", @@ -97,12 +91,8 @@ public class AssetProfileController extends BaseController { @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException { checkParameter(ASSET_PROFILE_ID, strAssetProfileId); - try { - AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); - return new AssetProfileInfo(checkAssetProfileId(assetProfileId, Operation.READ)); - } catch (Exception e) { - throw handleException(e); - } + AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); + return new AssetProfileInfo(checkAssetProfileId(assetProfileId, Operation.READ)); } @ApiOperation(value = "Get Default Asset Profile (getDefaultAssetProfileInfo)", @@ -113,11 +103,7 @@ public class AssetProfileController extends BaseController { @RequestMapping(value = "/assetProfileInfo/default", method = RequestMethod.GET) @ResponseBody public AssetProfileInfo getDefaultAssetProfileInfo() throws ThingsboardException { - try { - return checkNotNull(assetProfileService.findDefaultAssetProfileInfo(getTenantId())); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(assetProfileService.findDefaultAssetProfileInfo(getTenantId())); } @ApiOperation(value = "Create Or Update Asset Profile (saveAssetProfile)", @@ -191,12 +177,8 @@ public class AssetProfileController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(assetProfileService.findAssetProfiles(getTenantId(), pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(assetProfileService.findAssetProfiles(getTenantId(), pageLink)); } @ApiOperation(value = "Get Asset Profile infos (getAssetProfileInfos)", @@ -217,11 +199,7 @@ public class AssetProfileController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(assetProfileService.findAssetProfileInfos(getTenantId(), pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(assetProfileService.findAssetProfileInfos(getTenantId(), pageLink)); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index 33792a703f..387f0d014b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,15 +97,11 @@ public class AuditLogController extends BaseController { @RequestParam(required = false) Long endTime, @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION) @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { - try { - checkParameter("CustomerId", strCustomerId); - TenantId tenantId = getCurrentUser().getTenantId(); - TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - List actionTypes = parseActionTypesStr(actionTypesStr); - return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), actionTypes, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + checkParameter("CustomerId", strCustomerId); + TenantId tenantId = getCurrentUser().getTenantId(); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + List actionTypes = parseActionTypesStr(actionTypesStr); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), actionTypes, pageLink)); } @ApiOperation(value = "Get audit logs by user id (getAuditLogsByUserId)", @@ -135,15 +131,11 @@ public class AuditLogController extends BaseController { @RequestParam(required = false) Long endTime, @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION) @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { - try { - checkParameter("UserId", strUserId); - TenantId tenantId = getCurrentUser().getTenantId(); - TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - List actionTypes = parseActionTypesStr(actionTypesStr); - return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), actionTypes, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + checkParameter("UserId", strUserId); + TenantId tenantId = getCurrentUser().getTenantId(); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + List actionTypes = parseActionTypesStr(actionTypesStr); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), actionTypes, pageLink)); } @ApiOperation(value = "Get audit logs by entity id (getAuditLogsByEntityId)", @@ -176,16 +168,12 @@ public class AuditLogController extends BaseController { @RequestParam(required = false) Long endTime, @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION) @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { - try { - checkParameter("EntityId", strEntityId); - checkParameter("EntityType", strEntityType); - TenantId tenantId = getCurrentUser().getTenantId(); - TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - List actionTypes = parseActionTypesStr(actionTypesStr); - return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), actionTypes, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + checkParameter("EntityId", strEntityId); + checkParameter("EntityType", strEntityType); + TenantId tenantId = getCurrentUser().getTenantId(); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + List actionTypes = parseActionTypesStr(actionTypesStr); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), actionTypes, pageLink)); } @ApiOperation(value = "Get all audit logs (getAuditLogs)", @@ -212,14 +200,10 @@ public class AuditLogController extends BaseController { @RequestParam(required = false) Long endTime, @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION) @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - List actionTypes = parseActionTypesStr(actionTypesStr); - TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, actionTypes, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = getCurrentUser().getTenantId(); + List actionTypes = parseActionTypesStr(actionTypesStr); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, actionTypes, pageLink)); } private List parseActionTypesStr(String actionTypesStr) { diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 0cb3a3fc92..566b6900a4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -44,14 +45,15 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; -import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest; -import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest; import org.thingsboard.server.service.security.model.ResetPasswordRequest; import org.thingsboard.server.service.security.model.SecurityUser; @@ -69,11 +71,14 @@ import java.net.URISyntaxException; @Slf4j @RequiredArgsConstructor public class AuthController extends BaseController { + + @Value("${server.rest.rate_limits.reset_password_per_user:5:3600}") + private String defaultLimitsConfiguration; private final BCryptPasswordEncoder passwordEncoder; private final JwtTokenFactory tokenFactory; private final MailService mailService; private final SystemSecurityService systemSecurityService; - private final AuditLogService auditLogService; + private final RateLimitService rateLimitService; private final ApplicationEventPublisher eventPublisher; @@ -83,12 +88,8 @@ public class AuthController extends BaseController { @RequestMapping(value = "/auth/user", method = RequestMethod.GET) public @ResponseBody User getUser() throws ThingsboardException { - try { - SecurityUser securityUser = getCurrentUser(); - return userService.findUserById(securityUser.getTenantId(), securityUser.getId()); - } catch (Exception e) { - throw handleException(e); - } + SecurityUser securityUser = getCurrentUser(); + return userService.findUserById(securityUser.getTenantId(), securityUser.getId()); } @ApiOperation(value = "Logout (logout)", @@ -108,31 +109,27 @@ public class AuthController extends BaseController { public ObjectNode changePassword( @ApiParam(value = "Change Password Request") @RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException { - try { - String currentPassword = changePasswordRequest.getCurrentPassword(); - String newPassword = changePasswordRequest.getNewPassword(); - SecurityUser securityUser = getCurrentUser(); - UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, securityUser.getId()); - if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) { - throw new ThingsboardException("Current password doesn't match!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - systemSecurityService.validatePassword(securityUser.getTenantId(), newPassword, userCredentials); - if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) { - throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - userCredentials.setPassword(passwordEncoder.encode(newPassword)); - userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials); + String currentPassword = changePasswordRequest.getCurrentPassword(); + String newPassword = changePasswordRequest.getNewPassword(); + SecurityUser securityUser = getCurrentUser(); + UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, securityUser.getId()); + if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) { + throw new ThingsboardException("Current password doesn't match!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + systemSecurityService.validatePassword(securityUser.getTenantId(), newPassword, userCredentials); + if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) { + throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + userCredentials.setPassword(passwordEncoder.encode(newPassword)); + userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials); - sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); + sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); - eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); - ObjectNode response = JacksonUtil.newObjectNode(); - response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); - response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken()); - return response; - } catch (Exception e) { - throw handleException(e); - } + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); + ObjectNode response = JacksonUtil.newObjectNode(); + response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); + response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken()); + return response; } @ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)", @@ -140,13 +137,9 @@ public class AuthController extends BaseController { @RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET) @ResponseBody public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException { - try { - SecuritySettings securitySettings = - checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID)); - return securitySettings.getPasswordPolicy(); - } catch (Exception e) { - throw handleException(e); - } + SecuritySettings securitySettings = + checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID)); + return securitySettings.getPasswordPolicy(); } @ApiOperation(value = "Check Activate User Token (checkActivateToken)", @@ -211,7 +204,11 @@ public class AuthController extends BaseController { HttpStatus responseStatus; String resetURI = "/login/resetPassword"; UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); + if (userCredentials != null) { + if (!rateLimitService.checkRateLimit(LimitedApi.PASSWORD_RESET, userCredentials.getUserId(), defaultLimitsConfiguration)) { + return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build(); + } try { URI location = new URI(resetURI + "?resetToken=" + resetToken); headers.setLocation(location); @@ -241,34 +238,30 @@ public class AuthController extends BaseController { @RequestBody ActivateUserRequest activateRequest, @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException { - try { - String activateToken = activateRequest.getActivateToken(); - String password = activateRequest.getPassword(); - systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, null); - String encodedPassword = passwordEncoder.encode(password); - UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword); - User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId()); - UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); - SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); - userService.setUserCredentialsEnabled(user.getTenantId(), user.getId(), true); - String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request); - String loginUrl = String.format("%s/login", baseUrl); - String email = user.getEmail(); + String activateToken = activateRequest.getActivateToken(); + String password = activateRequest.getPassword(); + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, null); + String encodedPassword = passwordEncoder.encode(password); + UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword); + User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId()); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); + userService.setUserCredentialsEnabled(user.getTenantId(), user.getId(), true); + String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request); + String loginUrl = String.format("%s/login", baseUrl); + String email = user.getEmail(); - if (sendActivationMail) { - try { - mailService.sendAccountActivatedEmail(loginUrl, email); - } catch (Exception e) { - log.info("Unable to send account activation email [{}]", e.getMessage()); - } + if (sendActivationMail) { + try { + mailService.sendAccountActivatedEmail(loginUrl, email); + } catch (Exception e) { + log.info("Unable to send account activation email [{}]", e.getMessage()); } + } - sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); + sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); - return tokenFactory.createTokenPair(securityUser); - } catch (Exception e) { - throw handleException(e); - } + return tokenFactory.createTokenPair(securityUser); } @ApiOperation(value = "Reset password (resetPassword)", @@ -282,45 +275,38 @@ public class AuthController extends BaseController { @ApiParam(value = "Reset password request.") @RequestBody ResetPasswordRequest resetPasswordRequest, HttpServletRequest request) throws ThingsboardException { - try { - String resetToken = resetPasswordRequest.getResetToken(); - String password = resetPasswordRequest.getPassword(); - UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); - if (userCredentials != null) { - systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, userCredentials); - if (passwordEncoder.matches(password, userCredentials.getPassword())) { - throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - String encodedPassword = passwordEncoder.encode(password); - userCredentials.setPassword(encodedPassword); - userCredentials.setResetToken(null); - userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials); - User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId()); - UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal); - String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request); - String loginUrl = String.format("%s/login", baseUrl); - String email = user.getEmail(); - mailService.sendPasswordWasResetEmail(loginUrl, email); + String resetToken = resetPasswordRequest.getResetToken(); + String password = resetPasswordRequest.getPassword(); + UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); + if (userCredentials != null) { + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, userCredentials); + if (passwordEncoder.matches(password, userCredentials.getPassword())) { + throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + String encodedPassword = passwordEncoder.encode(password); + userCredentials.setPassword(encodedPassword); + userCredentials.setResetToken(null); + userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials); + User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId()); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal); + String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request); + String loginUrl = String.format("%s/login", baseUrl); + String email = user.getEmail(); + mailService.sendPasswordWasResetEmail(loginUrl, email); - eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); - return tokenFactory.createTokenPair(securityUser); - } else { - throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - } catch (Exception e) { - throw handleException(e); + return tokenFactory.createTokenPair(securityUser); + } else { + throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } } private void logLogoutAction(HttpServletRequest request) throws ThingsboardException { - try { - var user = getCurrentUser(); - systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGOUT, null); - eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId())); - } catch (Exception e) { - throw handleException(e); - } + var user = getCurrentUser(); + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGOUT, null); + eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId())); } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/AutoCommitController.java b/application/src/main/java/org/thingsboard/server/controller/AutoCommitController.java index 1a097e6817..1e7f3d66ce 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AutoCommitController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AutoCommitController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index ffc3fcf3cd..68a987a0bc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -25,7 +24,6 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -43,6 +41,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; @@ -54,16 +53,19 @@ import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; @@ -75,6 +77,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RpcId; @@ -83,6 +86,7 @@ import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; @@ -91,13 +95,17 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rpc.Rpc; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -120,6 +128,8 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; @@ -130,10 +140,12 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.component.ComponentDiscoveryService; -import org.thingsboard.server.service.edge.EdgeNotificationService; +import org.thingsboard.server.service.edge.instructions.EdgeInstallService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; +import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; @@ -143,19 +155,26 @@ import org.thingsboard.server.service.security.permission.AccessControlService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.state.DeviceStateService; +import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService; import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import javax.mail.MessagingException; import javax.servlet.http.HttpServletResponse; +import javax.validation.ConstraintViolation; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; -import static org.thingsboard.server.controller.ControllerConstants.INCORRECT_TENANT_ID; +import static org.thingsboard.server.common.data.StringUtils.isNotEmpty; +import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; import static org.thingsboard.server.controller.UserController.YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -165,8 +184,6 @@ public abstract class BaseController { /*Swagger UI description*/ - private static final ObjectMapper json = new ObjectMapper(); - @Autowired private ThingsboardErrorResponseHandler errorResponseHandler; @@ -185,6 +202,9 @@ public abstract class BaseController { @Autowired protected UserService userService; + @Autowired + protected TbUserSettingsService userSettingsService; + @Autowired protected DeviceService deviceService; @@ -200,6 +220,9 @@ public abstract class BaseController { @Autowired protected AlarmSubscriptionService alarmService; + @Autowired + protected AlarmCommentService alarmCommentService; + @Autowired protected DeviceCredentialsService deviceCredentialsService; @@ -281,15 +304,24 @@ public abstract class BaseController { @Autowired(required = false) protected EdgeRpcService edgeRpcService; + @Autowired(required = false) + protected EdgeInstallService edgeInstallService; + @Autowired protected TbNotificationEntityService notificationEntityService; + @Autowired + protected EntityActionService entityActionService; + @Autowired protected QueueService queueService; @Autowired protected EntitiesVersionControlService vcService; + @Autowired + protected ExportableEntitiesService entitiesService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -361,12 +393,21 @@ public abstract class BaseController { * Handles validation error for controller method arguments annotated with @{@link javax.validation.Valid} * */ @ExceptionHandler(MethodArgumentNotValidException.class) - public void handleValidationError(MethodArgumentNotValidException e, HttpServletResponse response) { - String errorMessage = "Validation error: " + e.getBindingResult().getAllErrors().stream() - .map(DefaultMessageSourceResolvable::getDefaultMessage) - .collect(Collectors.joining(", ")); + public void handleValidationError(MethodArgumentNotValidException validationError, HttpServletResponse response) { + List> constraintsViolations = validationError.getFieldErrors().stream() + .map(fieldError -> { + try { + return (ConstraintViolation) fieldError.unwrap(ConstraintViolation.class); + } catch (Exception e) { + log.warn("FieldError source is not of type ConstraintViolation"); + return null; // should not happen + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + String errorMessage = "Validation error: " + ConstraintValidator.getErrorMessage(constraintsViolations); ThingsboardException thingsboardException = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS); - handleThingsboardException(thingsboardException, response); + handleControllerException(thingsboardException, response); } T checkNotNull(T reference) throws ThingsboardException { @@ -408,6 +449,14 @@ public abstract class BaseController { } } + protected T checkEnumParameter(String name, String param, Function valueOf) throws ThingsboardException { + try { + return valueOf.apply(param.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new ThingsboardException(name + " \"" + param + "\" is not supported!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + UUID toUUID(String id) throws ThingsboardException { try { return UUID.fromString(id); @@ -417,9 +466,12 @@ public abstract class BaseController { } PageLink createPageLink(int pageSize, int page, String textSearch, String sortProperty, String sortOrder) throws ThingsboardException { - if (!StringUtils.isEmpty(sortProperty)) { + if (StringUtils.isNotEmpty(sortProperty)) { + if (!Validator.isValidProperty(sortProperty)) { + throw new IllegalArgumentException("Invalid sort property"); + } SortOrder.Direction direction = SortOrder.Direction.ASC; - if (!StringUtils.isEmpty(sortOrder)) { + if (StringUtils.isNotEmpty(sortOrder)) { try { direction = SortOrder.Direction.valueOf(sortOrder.toUpperCase()); } catch (IllegalArgumentException e) { @@ -449,27 +501,11 @@ public abstract class BaseController { } Tenant checkTenantId(TenantId tenantId, Operation operation) throws ThingsboardException { - try { - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Tenant tenant = tenantService.findTenantById(tenantId); - checkNotNull(tenant, "Tenant with id [" + tenantId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant); - return tenant; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(tenantId, (t, i) -> tenantService.findTenantById(tenantId), operation); } TenantInfo checkTenantInfoId(TenantId tenantId, Operation operation) throws ThingsboardException { - try { - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - TenantInfo tenant = tenantService.findTenantInfoById(tenantId); - checkNotNull(tenant, "Tenant with id [" + tenantId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant); - return tenant; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(tenantId, (t, i) -> tenantService.findTenantInfoById(tenantId), operation); } TenantProfile checkTenantProfileId(TenantProfileId tenantProfileId, Operation operation) throws ThingsboardException { @@ -489,33 +525,16 @@ public abstract class BaseController { } Customer checkCustomerId(CustomerId customerId, Operation operation) throws ThingsboardException { - try { - validateId(customerId, "Incorrect customerId " + customerId); - Customer customer = customerService.findCustomerById(getTenantId(), customerId); - checkNotNull(customer, "Customer with id [" + customerId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.CUSTOMER, operation, customerId, customer); - return customer; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(customerId, customerService::findCustomerById, operation); } User checkUserId(UserId userId, Operation operation) throws ThingsboardException { - try { - validateId(userId, "Incorrect userId " + userId); - User user = userService.findUserById(getCurrentUser().getTenantId(), userId); - checkNotNull(user, "User with id [" + userId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.USER, operation, userId, user); - return user; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(userId, userService::findUserById, operation); } protected void checkEntity(I entityId, T entity, Resource resource) throws ThingsboardException { if (entityId == null) { - accessControlService - .checkPermission(getCurrentUser(), resource, Operation.CREATE, null, entity); + accessControlService.checkPermission(getCurrentUser(), resource, Operation.CREATE, null, entity); } else { checkEntityId(entityId, Operation.WRITE); } @@ -586,203 +605,107 @@ public abstract class BaseController { checkQueueId(new QueueId(entityId.getId()), operation); return; default: - throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType()); + checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); } } catch (Exception e) { throw handleException(e, false); } } - Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException { + protected & HasTenantId, I extends EntityId> E checkEntityId(I entityId, ThrowingBiFunction findingFunction, Operation operation) throws ThingsboardException { try { - validateId(deviceId, "Incorrect deviceId " + deviceId); - Device device = deviceService.findDeviceById(getCurrentUser().getTenantId(), deviceId); - checkNotNull(device, "Device with id [" + deviceId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device); - return device; + validateId((UUIDBased) entityId, "Invalid entity id"); + SecurityUser user = getCurrentUser(); + E entity = findingFunction.apply(user.getTenantId(), entityId); + checkNotNull(entity, entityId.getEntityType().getNormalName() + " with id [" + entityId + "] is not found"); + return checkEntity(user, entity, operation); } catch (Exception e) { throw handleException(e, false); } } + protected & HasTenantId, I extends EntityId> E checkEntity(SecurityUser user, E entity, Operation operation) throws ThingsboardException { + checkNotNull(entity, "Entity not found"); + accessControlService.checkPermission(user, Resource.of(entity.getId().getEntityType()), operation, entity.getId(), entity); + return entity; + } + + Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException { + return checkEntityId(deviceId, deviceService::findDeviceById, operation); + } + DeviceInfo checkDeviceInfoId(DeviceId deviceId, Operation operation) throws ThingsboardException { - try { - validateId(deviceId, "Incorrect deviceId " + deviceId); - DeviceInfo device = deviceService.findDeviceInfoById(getCurrentUser().getTenantId(), deviceId); - checkNotNull(device, "Device with id [" + deviceId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device); - return device; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(deviceId, deviceService::findDeviceInfoById, operation); } DeviceProfile checkDeviceProfileId(DeviceProfileId deviceProfileId, Operation operation) throws ThingsboardException { - try { - validateId(deviceProfileId, "Incorrect deviceProfileId " + deviceProfileId); - DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(getCurrentUser().getTenantId(), deviceProfileId); - checkNotNull(deviceProfile, "Device profile with id [" + deviceProfileId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE_PROFILE, operation, deviceProfileId, deviceProfile); - return deviceProfile; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(deviceProfileId, deviceProfileService::findDeviceProfileById, operation); } protected EntityView checkEntityViewId(EntityViewId entityViewId, Operation operation) throws ThingsboardException { - try { - validateId(entityViewId, "Incorrect entityViewId " + entityViewId); - EntityView entityView = entityViewService.findEntityViewById(getCurrentUser().getTenantId(), entityViewId); - checkNotNull(entityView, "Entity view with id [" + entityViewId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, operation, entityViewId, entityView); - return entityView; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(entityViewId, entityViewService::findEntityViewById, operation); } EntityViewInfo checkEntityViewInfoId(EntityViewId entityViewId, Operation operation) throws ThingsboardException { - try { - validateId(entityViewId, "Incorrect entityViewId " + entityViewId); - EntityViewInfo entityView = entityViewService.findEntityViewInfoById(getCurrentUser().getTenantId(), entityViewId); - checkNotNull(entityView, "Entity view with id [" + entityViewId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, operation, entityViewId, entityView); - return entityView; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(entityViewId, entityViewService::findEntityViewInfoById, operation); } Asset checkAssetId(AssetId assetId, Operation operation) throws ThingsboardException { - try { - validateId(assetId, "Incorrect assetId " + assetId); - Asset asset = assetService.findAssetById(getCurrentUser().getTenantId(), assetId); - checkNotNull(asset, "Asset with id [" + assetId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, operation, assetId, asset); - return asset; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(assetId, assetService::findAssetById, operation); } AssetInfo checkAssetInfoId(AssetId assetId, Operation operation) throws ThingsboardException { - try { - validateId(assetId, "Incorrect assetId " + assetId); - AssetInfo asset = assetService.findAssetInfoById(getCurrentUser().getTenantId(), assetId); - checkNotNull(asset, "Asset with id [" + assetId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, operation, assetId, asset); - return asset; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(assetId, assetService::findAssetInfoById, operation); } AssetProfile checkAssetProfileId(AssetProfileId assetProfileId, Operation operation) throws ThingsboardException { - try { - validateId(assetProfileId, "Incorrect assetProfileId " + assetProfileId); - AssetProfile assetProfile = assetProfileService.findAssetProfileById(getCurrentUser().getTenantId(), assetProfileId); - checkNotNull(assetProfile, "Asset profile with id [" + assetProfileId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.ASSET_PROFILE, operation, assetProfileId, assetProfile); - return assetProfile; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(assetProfileId, assetProfileService::findAssetProfileById, operation); } Alarm checkAlarmId(AlarmId alarmId, Operation operation) throws ThingsboardException { - try { - validateId(alarmId, "Incorrect alarmId " + alarmId); - Alarm alarm = alarmService.findAlarmByIdAsync(getCurrentUser().getTenantId(), alarmId).get(); - checkNotNull(alarm, "Alarm with id [" + alarmId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarm); - return alarm; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(alarmId, alarmService::findAlarmById, operation); } AlarmInfo checkAlarmInfoId(AlarmId alarmId, Operation operation) throws ThingsboardException { + return checkEntityId(alarmId, alarmService::findAlarmInfoById, operation); + } + + AlarmComment checkAlarmCommentId(AlarmCommentId alarmCommentId, AlarmId alarmId) throws ThingsboardException { try { - validateId(alarmId, "Incorrect alarmId " + alarmId); - AlarmInfo alarmInfo = alarmService.findAlarmInfoByIdAsync(getCurrentUser().getTenantId(), alarmId).get(); - checkNotNull(alarmInfo, "Alarm with id [" + alarmId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarmInfo); - return alarmInfo; + validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + AlarmComment alarmComment = alarmCommentService.findAlarmCommentByIdAsync(getCurrentUser().getTenantId(), alarmCommentId).get(); + checkNotNull(alarmComment, "Alarm comment with id [" + alarmCommentId + "] is not found"); + if (!alarmId.equals(alarmComment.getAlarmId())) { + throw new ThingsboardException("Alarm id does not match with comment alarm id", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmComment; } catch (Exception e) { throw handleException(e, false); } } WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, Operation operation) throws ThingsboardException { - try { - validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId); - WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(getCurrentUser().getTenantId(), widgetsBundleId); - checkNotNull(widgetsBundle, "Widgets bundle with id [" + widgetsBundleId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.WIDGETS_BUNDLE, operation, widgetsBundleId, widgetsBundle); - return widgetsBundle; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(widgetsBundleId, widgetsBundleService::findWidgetsBundleById, operation); } WidgetTypeDetails checkWidgetTypeId(WidgetTypeId widgetTypeId, Operation operation) throws ThingsboardException { - try { - validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId); - WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(getCurrentUser().getTenantId(), widgetTypeId); - checkNotNull(widgetTypeDetails, "Widget type with id [" + widgetTypeId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, operation, widgetTypeId, widgetTypeDetails); - return widgetTypeDetails; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(widgetTypeId, widgetTypeService::findWidgetTypeDetailsById, operation); } Dashboard checkDashboardId(DashboardId dashboardId, Operation operation) throws ThingsboardException { - try { - validateId(dashboardId, "Incorrect dashboardId " + dashboardId); - Dashboard dashboard = dashboardService.findDashboardById(getCurrentUser().getTenantId(), dashboardId); - checkNotNull(dashboard, "Dashboard with id [" + dashboardId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, operation, dashboardId, dashboard); - return dashboard; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(dashboardId, dashboardService::findDashboardById, operation); } Edge checkEdgeId(EdgeId edgeId, Operation operation) throws ThingsboardException { - try { - validateId(edgeId, "Incorrect edgeId " + edgeId); - Edge edge = edgeService.findEdgeById(getTenantId(), edgeId); - checkNotNull(edge, "Edge with id [" + edgeId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge); - return edge; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(edgeId, edgeService::findEdgeById, operation); } EdgeInfo checkEdgeInfoId(EdgeId edgeId, Operation operation) throws ThingsboardException { - try { - validateId(edgeId, "Incorrect edgeId " + edgeId); - EdgeInfo edge = edgeService.findEdgeInfoById(getCurrentUser().getTenantId(), edgeId); - checkNotNull(edge, "Edge with id [" + edgeId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge); - return edge; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(edgeId, edgeService::findEdgeInfoById, operation); } DashboardInfo checkDashboardInfoId(DashboardId dashboardId, Operation operation) throws ThingsboardException { - try { - validateId(dashboardId, "Incorrect dashboardId " + dashboardId); - DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(getCurrentUser().getTenantId(), dashboardId); - checkNotNull(dashboardInfo, "Dashboard with id [" + dashboardId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, operation, dashboardId, dashboardInfo); - return dashboardInfo; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(dashboardId, dashboardService::findDashboardInfoById, operation); } ComponentDescriptor checkComponentDescriptorByClazz(String clazz) throws ThingsboardException { @@ -813,11 +736,7 @@ public abstract class BaseController { } protected RuleChain checkRuleChain(RuleChainId ruleChainId, Operation operation) throws ThingsboardException { - validateId(ruleChainId, "Incorrect ruleChainId " + ruleChainId); - RuleChain ruleChain = ruleChainService.findRuleChainById(getCurrentUser().getTenantId(), ruleChainId); - checkNotNull(ruleChain, "Rule chain with id [" + ruleChainId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.RULE_CHAIN, operation, ruleChainId, ruleChain); - return ruleChain; + return checkEntityId(ruleChainId, ruleChainService::findRuleChainById, operation); } protected RuleNode checkRuleNode(RuleNodeId ruleNodeId, Operation operation) throws ThingsboardException { @@ -829,70 +748,27 @@ public abstract class BaseController { } TbResource checkResourceId(TbResourceId resourceId, Operation operation) throws ThingsboardException { - try { - validateId(resourceId, "Incorrect resourceId " + resourceId); - TbResource resource = resourceService.findResourceById(getCurrentUser().getTenantId(), resourceId); - checkNotNull(resource, "Resource with id [" + resourceId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resource); - return resource; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(resourceId, resourceService::findResourceById, operation); } TbResourceInfo checkResourceInfoId(TbResourceId resourceId, Operation operation) throws ThingsboardException { - try { - validateId(resourceId, "Incorrect resourceId " + resourceId); - TbResourceInfo resourceInfo = resourceService.findResourceInfoById(getCurrentUser().getTenantId(), resourceId); - checkNotNull(resourceInfo, "Resource with id [" + resourceId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resourceInfo); - return resourceInfo; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(resourceId, resourceService::findResourceInfoById, operation); } OtaPackage checkOtaPackageId(OtaPackageId otaPackageId, Operation operation) throws ThingsboardException { - try { - validateId(otaPackageId, "Incorrect otaPackageId " + otaPackageId); - OtaPackage otaPackage = otaPackageService.findOtaPackageById(getCurrentUser().getTenantId(), otaPackageId); - checkNotNull(otaPackage, "OTA package with id [" + otaPackageId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.OTA_PACKAGE, operation, otaPackageId, otaPackage); - return otaPackage; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(otaPackageId, otaPackageService::findOtaPackageById, operation); } OtaPackageInfo checkOtaPackageInfoId(OtaPackageId otaPackageId, Operation operation) throws ThingsboardException { - try { - validateId(otaPackageId, "Incorrect otaPackageId " + otaPackageId); - OtaPackageInfo otaPackageIn = otaPackageService.findOtaPackageInfoById(getCurrentUser().getTenantId(), otaPackageId); - checkNotNull(otaPackageIn, "OTA package with id [" + otaPackageId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.OTA_PACKAGE, operation, otaPackageId, otaPackageIn); - return otaPackageIn; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(otaPackageId, otaPackageService::findOtaPackageInfoById, operation); } Rpc checkRpcId(RpcId rpcId, Operation operation) throws ThingsboardException { - try { - validateId(rpcId, "Incorrect rpcId " + rpcId); - Rpc rpc = rpcService.findById(getCurrentUser().getTenantId(), rpcId); - checkNotNull(rpc, "RPC with id [" + rpcId + "] is not found"); - accessControlService.checkPermission(getCurrentUser(), Resource.RPC, operation, rpcId, rpc); - return rpc; - } catch (Exception e) { - throw handleException(e, false); - } + return checkEntityId(rpcId, rpcService::findById, operation); } protected Queue checkQueueId(QueueId queueId, Operation operation) throws ThingsboardException { - validateId(queueId, "Incorrect queueId " + queueId); - Queue queue = queueService.findQueueById(getCurrentUser().getTenantId(), queueId); - checkNotNull(queue); - accessControlService.checkPermission(getCurrentUser(), Resource.QUEUE, operation, queueId, queue); + Queue queue = checkEntityId(queueId, queueService::findQueueById, operation); TenantId tenantId = getTenantId(); if (queue.getTenantId().isNullUid() && !tenantId.isNullUid()) { TenantProfile tenantProfile = tenantProfileCache.get(tenantId); @@ -912,6 +788,42 @@ public abstract class BaseController { return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null; } + protected > void logEntityAction(SecurityUser user, EntityType entityType, E savedEntity, ActionType actionType) { + logEntityAction(user, entityType, null, savedEntity, actionType, null); + } + + protected > void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) { + EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType); + if (!user.isSystemAdmin()) { + entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity, + user.getCustomerId(), actionType, e); + } + } + + protected > E doSaveAndLog(EntityType entityType, E entity, BiFunction savingFunction) throws Exception { + ActionType actionType = entity.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + SecurityUser user = getCurrentUser(); + try { + E savedEntity = savingFunction.apply(user.getTenantId(), entity); + logEntityAction(user, entityType, savedEntity, actionType); + return savedEntity; + } catch (Exception e) { + logEntityAction(user, entityType, entity, null, actionType, e); + throw e; + } + } + + protected , I extends EntityId> void doDeleteAndLog(EntityType entityType, E entity, BiConsumer deleteFunction) throws Exception { + SecurityUser user = getCurrentUser(); + try { + deleteFunction.accept(user.getTenantId(), entity.getId()); + logEntityAction(user, entityType, entity, ActionType.DELETED); + } catch (Exception e) { + logEntityAction(user, entityType, entity, entity, ActionType.DELETED, e); + throw e; + } + } + protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { sendNotificationMsgToEdge(tenantId, null, entityId, null, null, action); } @@ -956,4 +868,18 @@ public abstract class BaseController { }, MoreExecutors.directExecutor()); return deferredResult; } + + protected EntityDataSortOrder createEntityDataSortOrder(String sortProperty, String sortOrder) { + if (isNotEmpty(sortProperty)) { + EntityDataSortOrder entityDataSortOrder = new EntityDataSortOrder(); + entityDataSortOrder.setKey(new EntityKey(ENTITY_FIELD, sortProperty)); + if (isNotEmpty(sortOrder)) { + entityDataSortOrder.setDirection(EntityDataSortOrder.Direction.valueOf(sortOrder)); + } + return entityDataSortOrder; + } else { + return null; + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 31f64254af..33a4edfbe0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,11 +57,7 @@ public class ComponentDescriptorController extends BaseController { @ApiParam(value = "Component Descriptor class name", required = true) @PathVariable("componentDescriptorClazz") String strComponentDescriptorClazz) throws ThingsboardException { checkParameter("strComponentDescriptorClazz", strComponentDescriptorClazz); - try { - return checkComponentDescriptorByClazz(strComponentDescriptorClazz); - } catch (Exception e) { - throw handleException(e); - } + return checkComponentDescriptorByClazz(strComponentDescriptorClazz); } @ApiOperation(value = "Get Component Descriptors (getComponentDescriptorsByType)", @@ -76,11 +72,7 @@ public class ComponentDescriptorController extends BaseController { @ApiParam(value = "Type of the Rule Chain", allowableValues = "CORE,EDGE") @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkParameter("componentType", strComponentType); - try { - return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType)); - } catch (Exception e) { - throw handleException(e); - } + return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType)); } @ApiOperation(value = "Get Component Descriptors (getComponentDescriptorsByTypes)", @@ -95,15 +87,11 @@ public class ComponentDescriptorController extends BaseController { @ApiParam(value = "Type of the Rule Chain", allowableValues = "CORE,EDGE") @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkArrayParameter("componentTypes", strComponentTypes); - try { - Set componentTypes = new HashSet<>(); - for (String strComponentType : strComponentTypes) { - componentTypes.add(ComponentType.valueOf(strComponentType)); - } - return checkComponentDescriptorsByTypes(componentTypes, getRuleChainType(strRuleChainType)); - } catch (Exception e) { - throw handleException(e); + Set componentTypes = new HashSet<>(); + for (String strComponentType : strComponentTypes) { + componentTypes.add(ComponentType.valueOf(strComponentType)); } + return checkComponentDescriptorsByTypes(componentTypes, getRuleChainType(strRuleChainType)); } private RuleChainType getRuleChainType(String strRuleChainType) { diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index e00ccaa4bc..f425bde86c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ public class ControllerConstants { protected static final String EDGE_ID = "edgeId"; protected static final String RPC_ID = "rpcId"; protected static final String ENTITY_ID = "entityId"; + protected static final String ASSIGNEE_ID = "assigneeId"; protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + "See the 'Model' tab of the Response Class for more details. "; @@ -44,6 +45,9 @@ public class ControllerConstants { protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String ASSIGN_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + + protected static final String ALARM_COMMENT_ID_PARAM_DESCRIPTION = "A string value representing the alarm comment id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String OTA_PACKAGE_ID_PARAM_DESCRIPTION = "A string value representing the ota package id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_TYPE_PARAM_DESCRIPTION = "A string value representing the entity type. For example, 'DEVICE'"; @@ -62,6 +66,7 @@ public class ControllerConstants { protected static final String PAGE_SIZE_DESCRIPTION = "Maximum amount of entities in a one page"; protected static final String PAGE_NUMBER_DESCRIPTION = "Sequence number of page starting from 0"; protected static final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile"; + protected static final String DEVICE_ACTIVE_PARAM_DESCRIPTION = "A boolean value representing the device active flag."; protected static final String ENTITY_VIEW_TYPE_DESCRIPTION = "Entity View type"; protected static final String ASSET_TYPE_DESCRIPTION = "Asset type"; protected static final String EDGE_TYPE_DESCRIPTION = "A string value representing the edge type. For example, 'default'"; @@ -100,6 +105,7 @@ public class ControllerConstants { protected static final String ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault"; protected static final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; protected static final String ALARM_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, startTs, endTs, type, ackTs, clearTs, severity, status"; + protected static final String ALARM_COMMENT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, id"; protected static final String EVENT_SORT_PROPERTY_ALLOWABLE_VALUES = "ts, id"; protected static final String EDGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; protected static final String RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, root"; @@ -1433,7 +1439,8 @@ public class ControllerConstants { protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'."; protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'."; - protected static final String ATTRIBUTES_SCOPE_ALLOWED_VALUES = "SERVER_SCOPE, CLIENT_SCOPE, SHARED_SCOPE"; + protected static final String ATTRIBUTES_SAVE_SCOPE_ALLOWED_VALUES = "SERVER_SCOPE, SHARED_SCOPE"; + protected static final String ATTRIBUTES_SCOPE_ALLOWED_VALUES = ATTRIBUTES_SAVE_SCOPE_ALLOWED_VALUES + ", CLIENT_SCOPE"; protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details."; protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys."; @@ -1459,10 +1466,12 @@ public class ControllerConstants { protected static final String SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " + "Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED' that includes an error stacktrace."; - protected static final String ENTITY_ATTRIBUTE_SCOPES = " List of possible attribute scopes depends on the entity type: " + + protected static final String ENTITY_ATTRIBUTE_SCOPES_TEMPLATE = " List of possible attribute scopes depends on the entity type: " + "\n\n * SERVER_SCOPE - supported for all entity types;" + - "\n * CLIENT_SCOPE - supported for devices;" + - "\n * SHARED_SCOPE - supported for devices. "+ "\n\n"; + "\n * SHARED_SCOPE - supported for devices"; + protected static final String ENTITY_SAVE_ATTRIBUTE_SCOPES = ENTITY_ATTRIBUTE_SCOPES_TEMPLATE + ".\n\n"; + protected static final String ENTITY_GET_ATTRIBUTE_SCOPES = ENTITY_ATTRIBUTE_SCOPES_TEMPLATE + + ";\n * CLIENT_SCOPE - supported for devices. " + "\n\n"; protected static final String ATTRIBUTE_DATA_EXAMPLE = "[\n" + " {\"key\": \"stringAttributeKey\", \"value\": \"value\", \"lastUpdateTs\": 1609459200000},\n" + diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 591eaa1318..dd904add2e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -31,6 +30,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -79,16 +79,12 @@ public class CustomerController extends BaseController { @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); - try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - Customer customer = checkCustomerId(customerId, Operation.READ); - if (!customer.getAdditionalInfo().isNull()) { - processDashboardIdFromAdditionalInfo((ObjectNode) customer.getAdditionalInfo(), HOME_DASHBOARD); - } - return customer; - } catch (Exception e) { - throw handleException(e); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.READ); + if (!customer.getAdditionalInfo().isNull()) { + processDashboardIdFromAdditionalInfo((ObjectNode) customer.getAdditionalInfo(), HOME_DASHBOARD); } + return customer; } @@ -102,17 +98,12 @@ public class CustomerController extends BaseController { @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); - try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - Customer customer = checkCustomerId(customerId, Operation.READ); - ObjectMapper objectMapper = new ObjectMapper(); - ObjectNode infoObject = objectMapper.createObjectNode(); - infoObject.put("title", customer.getTitle()); - infoObject.put(IS_PUBLIC, customer.isPublic()); - return infoObject; - } catch (Exception e) { - throw handleException(e); - } + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.READ); + ObjectNode infoObject = JacksonUtil.newObjectNode(); + infoObject.put("title", customer.getTitle()); + infoObject.put(IS_PUBLIC, customer.isPublic()); + return infoObject; } @ApiOperation(value = "Get Customer Title (getCustomerTitleById)", @@ -125,13 +116,9 @@ public class CustomerController extends BaseController { @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); - try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - Customer customer = checkCustomerId(customerId, Operation.READ); - return customer.getTitle(); - } catch (Exception e) { - throw handleException(e); - } + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.READ); + return customer.getTitle(); } @ApiOperation(value = "Create or update Customer (saveCustomer)", @@ -182,13 +169,9 @@ public class CustomerController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(customerService.findCustomersByTenantId(tenantId, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(customerService.findCustomersByTenantId(tenantId, pageLink)); } @ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)", @@ -199,11 +182,7 @@ public class CustomerController extends BaseController { public Customer getTenantCustomer( @ApiParam(value = "A string value representing the Customer title.") @RequestParam String customerTitle) throws ThingsboardException { - try { TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found"); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found"); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 9ceeb0d14c..afc737ab88 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -140,12 +140,8 @@ public class DashboardController extends BaseController { @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); - try { - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - return checkDashboardInfoId(dashboardId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + return checkDashboardInfoId(dashboardId, Operation.READ); } @ApiOperation(value = "Get Dashboard (getDashboardById)", @@ -159,12 +155,8 @@ public class DashboardController extends BaseController { @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); - try { - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - return checkDashboardId(dashboardId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + return checkDashboardId(dashboardId, Operation.READ); } @ApiOperation(value = "Create Or Update Dashboard (saveDashboard)", @@ -362,14 +354,10 @@ public class DashboardController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); - checkTenantId(tenantId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); + checkTenantId(tenantId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); } @ApiOperation(value = "Get Tenant Dashboards (getTenantDashboards)", @@ -392,16 +380,12 @@ public class DashboardController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (mobile != null && mobile) { - return checkNotNull(dashboardService.findMobileDashboardsByTenantId(tenantId, pageLink)); - } else { - return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (mobile != null && mobile) { + return checkNotNull(dashboardService.findMobileDashboardsByTenantId(tenantId, pageLink)); + } else { + return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); } } @@ -428,18 +412,14 @@ public class DashboardController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (mobile != null && mobile) { - return checkNotNull(dashboardService.findMobileDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); - } else { - return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (mobile != null && mobile) { + return checkNotNull(dashboardService.findMobileDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); + } else { + return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } } @@ -453,31 +433,27 @@ public class DashboardController extends BaseController { @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET) @ResponseBody public HomeDashboard getHomeDashboard() throws ThingsboardException { - try { - SecurityUser securityUser = getCurrentUser(); - if (securityUser.isSystemAdmin()) { - return null; + SecurityUser securityUser = getCurrentUser(); + if (securityUser.isSystemAdmin()) { + return null; + } + User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); + JsonNode additionalInfo = user.getAdditionalInfo(); + HomeDashboard homeDashboard; + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); + if (homeDashboard == null) { + if (securityUser.isCustomerUser()) { + Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId()); + additionalInfo = customer.getAdditionalInfo(); + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); } - User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); - JsonNode additionalInfo = user.getAdditionalInfo(); - HomeDashboard homeDashboard; - homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); if (homeDashboard == null) { - if (securityUser.isCustomerUser()) { - Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId()); - additionalInfo = customer.getAdditionalInfo(); - homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); - } - if (homeDashboard == null) { - Tenant tenant = tenantService.findTenantById(securityUser.getTenantId()); - additionalInfo = tenant.getAdditionalInfo(); - homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); - } + Tenant tenant = tenantService.findTenantById(securityUser.getTenantId()); + additionalInfo = tenant.getAdditionalInfo(); + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); } - return homeDashboard; - } catch (Exception e) { - throw handleException(e); } + return homeDashboard; } @ApiOperation(value = "Get Home Dashboard Info (getHomeDashboardInfo)", @@ -490,31 +466,27 @@ public class DashboardController extends BaseController { @RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET) @ResponseBody public HomeDashboardInfo getHomeDashboardInfo() throws ThingsboardException { - try { - SecurityUser securityUser = getCurrentUser(); - if (securityUser.isSystemAdmin()) { - return null; + SecurityUser securityUser = getCurrentUser(); + if (securityUser.isSystemAdmin()) { + return null; + } + User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); + JsonNode additionalInfo = user.getAdditionalInfo(); + HomeDashboardInfo homeDashboardInfo; + homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); + if (homeDashboardInfo == null) { + if (securityUser.isCustomerUser()) { + Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId()); + additionalInfo = customer.getAdditionalInfo(); + homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); } - User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); - JsonNode additionalInfo = user.getAdditionalInfo(); - HomeDashboardInfo homeDashboardInfo; - homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); if (homeDashboardInfo == null) { - if (securityUser.isCustomerUser()) { - Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId()); - additionalInfo = customer.getAdditionalInfo(); - homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); - } - if (homeDashboardInfo == null) { - Tenant tenant = tenantService.findTenantById(securityUser.getTenantId()); - additionalInfo = tenant.getAdditionalInfo(); - homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); - } + Tenant tenant = tenantService.findTenantById(securityUser.getTenantId()); + additionalInfo = tenant.getAdditionalInfo(); + homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); } - return homeDashboardInfo; - } catch (Exception e) { - throw handleException(e); } + return homeDashboardInfo; } @ApiOperation(value = "Get Tenant Home Dashboard Info (getTenantHomeDashboardInfo)", @@ -525,22 +497,18 @@ public class DashboardController extends BaseController { @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET) @ResponseBody public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException { - try { - Tenant tenant = tenantService.findTenantById(getTenantId()); - JsonNode additionalInfo = tenant.getAdditionalInfo(); - DashboardId dashboardId = null; - boolean hideDashboardToolbar = true; - if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) { - String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText(); - dashboardId = new DashboardId(toUUID(strDashboardId)); - if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) { - hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean(); - } + Tenant tenant = tenantService.findTenantById(getTenantId()); + JsonNode additionalInfo = tenant.getAdditionalInfo(); + DashboardId dashboardId = null; + boolean hideDashboardToolbar = true; + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) { + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText(); + dashboardId = new DashboardId(toUUID(strDashboardId)); + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) { + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean(); } - return new HomeDashboardInfo(dashboardId, hideDashboardToolbar); - } catch (Exception e) { - throw handleException(e); } + return new HomeDashboardInfo(dashboardId, hideDashboardToolbar); } @ApiOperation(value = "Update Tenant Home Dashboard Info (getTenantHomeDashboardInfo)", @@ -554,27 +522,23 @@ public class DashboardController extends BaseController { @ApiParam(value = "A JSON object that represents home dashboard id and other parameters", required = true) @RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException { - try { - if (homeDashboardInfo.getDashboardId() != null) { - checkDashboardId(homeDashboardInfo.getDashboardId(), Operation.READ); - } - Tenant tenant = tenantService.findTenantById(getTenantId()); - JsonNode additionalInfo = tenant.getAdditionalInfo(); - if (additionalInfo == null || !(additionalInfo instanceof ObjectNode)) { - additionalInfo = JacksonUtil.OBJECT_MAPPER.createObjectNode(); - } - if (homeDashboardInfo.getDashboardId() != null) { - ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_ID, homeDashboardInfo.getDashboardId().getId().toString()); - ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_HIDE_TOOLBAR, homeDashboardInfo.isHideDashboardToolbar()); - } else { - ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_ID); - ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_HIDE_TOOLBAR); - } - tenant.setAdditionalInfo(additionalInfo); - tenantService.saveTenant(tenant); - } catch (Exception e) { - throw handleException(e); + if (homeDashboardInfo.getDashboardId() != null) { + checkDashboardId(homeDashboardInfo.getDashboardId(), Operation.READ); + } + Tenant tenant = tenantService.findTenantById(getTenantId()); + JsonNode additionalInfo = tenant.getAdditionalInfo(); + if (additionalInfo == null || !(additionalInfo instanceof ObjectNode)) { + additionalInfo = JacksonUtil.newObjectNode(); + } + if (homeDashboardInfo.getDashboardId() != null) { + ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_ID, homeDashboardInfo.getDashboardId().getId().toString()); + ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_HIDE_TOOLBAR, homeDashboardInfo.isHideDashboardToolbar()); + } else { + ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_ID); + ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_HIDE_TOOLBAR); } + tenant.setAdditionalInfo(additionalInfo); + tenantService.saveTenant(tenant); } private HomeDashboardInfo extractHomeDashboardInfoFromAdditionalInfo(JsonNode additionalInfo) { @@ -681,28 +645,24 @@ public class DashboardController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("edgeId", strEdgeId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - checkEdgeId(edgeId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - PageData nonFilteredResult = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); - List filteredDashboards = nonFilteredResult.getData().stream().filter(dashboardInfo -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboardInfo.getId(), dashboardInfo); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); - PageData filteredResult = new PageData<>(filteredDashboards, - nonFilteredResult.getTotalPages(), - nonFilteredResult.getTotalElements(), - nonFilteredResult.hasNext()); - return checkNotNull(filteredResult); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData nonFilteredResult = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + List filteredDashboards = nonFilteredResult.getData().stream().filter(dashboardInfo -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboardInfo.getId(), dashboardInfo); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredDashboards, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); } private Set customerIdFromStr(String[] strCustomerIds) { diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index ae1558381f..bb34f6d5b2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.Tenant; @@ -59,6 +60,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; import org.thingsboard.server.dao.device.claim.ReclaimResult; @@ -66,8 +69,6 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.device.DeviceBulkImportService; -import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; -import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.service.entitiy.device.TbDeviceService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; @@ -77,11 +78,13 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ACTIVE_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_INFO_DESCRIPTION; @@ -308,16 +311,12 @@ public class DeviceController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(deviceService.findDevicesByTenantIdAndType(tenantId, type, pageLink)); - } else { - return checkNotNull(deviceService.findDevicesByTenantId(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(deviceService.findDevicesByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(deviceService.findDevicesByTenantId(tenantId, pageLink)); } } @@ -336,6 +335,8 @@ public class DeviceController extends BaseController { @RequestParam(required = false) String type, @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(required = false) String deviceProfileId, + @ApiParam(value = DEVICE_ACTIVE_PARAM_DESCRIPTION) + @RequestParam(required = false) Boolean active, @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @@ -343,20 +344,17 @@ public class DeviceController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder ) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(deviceService.findDeviceInfosByTenantIdAndType(tenantId, type, pageLink)); - } else if (deviceProfileId != null && deviceProfileId.length() > 0) { - DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId)); - return checkNotNull(deviceService.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, profileId, pageLink)); - } else { - return checkNotNull(deviceService.findDeviceInfosByTenantId(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + DeviceInfoFilter.DeviceInfoFilterBuilder filter = DeviceInfoFilter.builder(); + filter.tenantId(tenantId); + filter.active(active); + if (type != null && type.trim().length() > 0) { + filter.type(type); + } else if (deviceProfileId != null && deviceProfileId.length() > 0) { + filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId))); } + return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink)); } @ApiOperation(value = "Get Tenant Device (getTenantDevice)", @@ -368,12 +366,8 @@ public class DeviceController extends BaseController { public Device getTenantDevice( @ApiParam(value = DEVICE_NAME_DESCRIPTION) @RequestParam String deviceName) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); } @ApiOperation(value = "Get Customer Devices (getCustomerDevices)", @@ -398,18 +392,14 @@ public class DeviceController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); - } else { - return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); + } else { + return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } } @@ -430,6 +420,8 @@ public class DeviceController extends BaseController { @RequestParam(required = false) String type, @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(required = false) String deviceProfileId, + @ApiParam(value = DEVICE_ACTIVE_PARAM_DESCRIPTION) + @RequestParam(required = false) Boolean active, @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @@ -437,22 +429,20 @@ public class DeviceController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); - } else if (deviceProfileId != null && deviceProfileId.length() > 0) { - DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId)); - return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId, customerId, profileId, pageLink)); - } else { - return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + DeviceInfoFilter.DeviceInfoFilterBuilder filter = DeviceInfoFilter.builder(); + filter.tenantId(tenantId); + filter.customerId(customerId); + filter.active(active); + if (type != null && type.trim().length() > 0) { + filter.type(type); + } else if (deviceProfileId != null && deviceProfileId.length() > 0) { + filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId))); } + return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink)); } @ApiOperation(value = "Get Devices By Ids (getDevicesByIds)", @@ -462,26 +452,22 @@ public class DeviceController extends BaseController { @ResponseBody public List getDevicesByIds( @ApiParam(value = "A list of devices ids, separated by comma ','") - @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException { + @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("deviceIds", strDeviceIds); - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - CustomerId customerId = user.getCustomerId(); - List deviceIds = new ArrayList<>(); - for (String strDeviceId : strDeviceIds) { - deviceIds.add(new DeviceId(toUUID(strDeviceId))); - } - ListenableFuture> devices; - if (customerId == null || customerId.isNullUid()) { - devices = deviceService.findDevicesByTenantIdAndIdsAsync(tenantId, deviceIds); - } else { - devices = deviceService.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, deviceIds); - } - return checkNotNull(devices.get()); - } catch (Exception e) { - throw handleException(e); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = user.getCustomerId(); + List deviceIds = new ArrayList<>(); + for (String strDeviceId : strDeviceIds) { + deviceIds.add(new DeviceId(toUUID(strDeviceId))); + } + ListenableFuture> devices; + if (customerId == null || customerId.isNullUid()) { + devices = deviceService.findDevicesByTenantIdAndIdsAsync(tenantId, deviceIds); + } else { + devices = deviceService.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, deviceIds); } + return checkNotNull(devices.get()); } @ApiOperation(value = "Find related devices (findByQuery)", @@ -493,25 +479,21 @@ public class DeviceController extends BaseController { @ResponseBody public List findByQuery( @ApiParam(value = "The device search query JSON") - @RequestBody DeviceSearchQuery query) throws ThingsboardException { + @RequestBody DeviceSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getDeviceTypes()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); - try { - List devices = checkNotNull(deviceService.findDevicesByQuery(getCurrentUser().getTenantId(), query).get()); - devices = devices.stream().filter(device -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, Operation.READ, device.getId(), device); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); - return devices; - } catch (Exception e) { - throw handleException(e); - } + List devices = checkNotNull(deviceService.findDevicesByQuery(getCurrentUser().getTenantId(), query).get()); + devices = devices.stream().filter(device -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, Operation.READ, device.getId(), device); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + return devices; } @ApiOperation(value = "Get Device Types (getDeviceTypes)", @@ -520,15 +502,11 @@ public class DeviceController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device/types", method = RequestMethod.GET) @ResponseBody - public List getDeviceTypes() throws ThingsboardException { - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - ListenableFuture> deviceTypes = deviceService.findDeviceTypesByTenantId(tenantId); - return checkNotNull(deviceTypes.get()); - } catch (Exception e) { - throw handleException(e); - } + public List getDeviceTypes() throws ThingsboardException, ExecutionException, InterruptedException { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + ListenableFuture> deviceTypes = deviceService.findDeviceTypesByTenantId(tenantId); + return checkNotNull(deviceTypes.get()); } @ApiOperation(value = "Claim device (claimDevice)", @@ -702,7 +680,7 @@ public class DeviceController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getEdgeDevices( + public PageData getEdgeDevices( @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @@ -711,6 +689,10 @@ public class DeviceController extends BaseController { @RequestParam int page, @ApiParam(value = DEVICE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) + @RequestParam(required = false) String deviceProfileId, + @ApiParam(value = DEVICE_ACTIVE_PARAM_DESCRIPTION) + @RequestParam(required = false) Boolean active, @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @@ -722,33 +704,20 @@ public class DeviceController extends BaseController { @ApiParam(value = "Timestamp. Devices with creation time after it won't be queried") @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - checkEdgeId(edgeId, Operation.READ); - TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - PageData nonFilteredResult; - if (type != null && type.trim().length() > 0) { - nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); - } else { - nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink); - } - List filteredDevices = nonFilteredResult.getData().stream().filter(device -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, Operation.READ, device.getId(), device); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); - PageData filteredResult = new PageData<>(filteredDevices, - nonFilteredResult.getTotalPages(), - nonFilteredResult.getTotalElements(), - nonFilteredResult.hasNext()); - return checkNotNull(filteredResult); - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + DeviceInfoFilter.DeviceInfoFilterBuilder filter = DeviceInfoFilter.builder(); + filter.tenantId(tenantId); + filter.edgeId(edgeId); + filter.active(active); + if (type != null && type.trim().length() > 0) { + filter.type(type); + } else if (deviceProfileId != null && deviceProfileId.length() > 0) { + filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId))); } + return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink)); } @ApiOperation(value = "Count devices by device profile (countByDeviceProfileAndEmptyOtaPackage)", @@ -766,14 +735,10 @@ public class DeviceController extends BaseController { @PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException { checkParameter("OtaPackageType", otaPackageType); checkParameter("DeviceProfileId", deviceProfileId); - try { - return deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage( - getTenantId(), - new DeviceProfileId(UUID.fromString(deviceProfileId)), - OtaPackageType.valueOf(otaPackageType)); - } catch (Exception e) { - throw handleException(e); - } + return deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage( + getTenantId(), + new DeviceProfileId(UUID.fromString(deviceProfileId)), + OtaPackageType.valueOf(otaPackageType)); } @ApiOperation(value = "Import the bulk of devices (processDevicesBulkImport)", diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index bd2a5a2ad9..c888d67252 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,12 +87,8 @@ public class DeviceProfileController extends BaseController { @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId); - try { - DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); - return checkDeviceProfileId(deviceProfileId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + return checkDeviceProfileId(deviceProfileId, Operation.READ); } @ApiOperation(value = "Get Device Profile Info (getDeviceProfileInfoById)", @@ -106,12 +102,8 @@ public class DeviceProfileController extends BaseController { @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId); - try { - DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); - return new DeviceProfileInfo(checkDeviceProfileId(deviceProfileId, Operation.READ)); - } catch (Exception e) { - throw handleException(e); - } + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + return new DeviceProfileInfo(checkDeviceProfileId(deviceProfileId, Operation.READ)); } @ApiOperation(value = "Get Default Device Profile (getDefaultDeviceProfileInfo)", @@ -122,11 +114,7 @@ public class DeviceProfileController extends BaseController { @RequestMapping(value = "/deviceProfileInfo/default", method = RequestMethod.GET) @ResponseBody public DeviceProfileInfo getDefaultDeviceProfileInfo() throws ThingsboardException { - try { - return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId())); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId())); } @ApiOperation(value = "Get time-series keys (getTimeseriesKeys)", @@ -150,11 +138,7 @@ public class DeviceProfileController extends BaseController { deviceProfileId = null; } - try { - return timeseriesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId); - } catch (Exception e) { - throw handleException(e); - } + return timeseriesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId); } @ApiOperation(value = "Get attribute keys (getAttributesKeys)", @@ -178,11 +162,7 @@ public class DeviceProfileController extends BaseController { deviceProfileId = null; } - try { - return attributesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId); - } catch (Exception e) { - throw handleException(e); - } + return attributesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId); } @ApiOperation(value = "Create Or Update Device Profile (saveDeviceProfile)", @@ -256,12 +236,8 @@ public class DeviceProfileController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(deviceProfileService.findDeviceProfiles(getTenantId(), pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(deviceProfileService.findDeviceProfiles(getTenantId(), pageLink)); } @ApiOperation(value = "Get Device Profiles for transport type (getDeviceProfileInfos)", @@ -284,11 +260,7 @@ public class DeviceProfileController extends BaseController { @RequestParam(required = false) String sortOrder, @ApiParam(value = "Type of the transport", allowableValues = TRANSPORT_TYPE_ALLOWABLE_VALUES) @RequestParam(required = false) String transportType) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType)); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 6a5dd44d0b..9a021643cc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -49,6 +50,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.dao.exception.DataValidationException; @@ -57,15 +60,15 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeBulkImportService; import org.thingsboard.server.service.entitiy.edge.TbEdgeService; -import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; -import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; @@ -116,12 +119,8 @@ public class EdgeController extends BaseController { public Edge getEdgeById(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - return checkEdgeId(edgeId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + return checkEdgeId(edgeId, Operation.READ); } @ApiOperation(value = "Get Edge Info (getEdgeInfoById)", @@ -133,12 +132,8 @@ public class EdgeController extends BaseController { public EdgeInfo getEdgeInfoById(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - return checkEdgeInfoId(edgeId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + return checkEdgeInfoId(edgeId, Operation.READ); } @ApiOperation(value = "Create Or Update Edge (saveEdge)", @@ -203,13 +198,9 @@ public class EdgeController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); } @ApiOperation(value = "Assign edge to customer (assignEdgeToCustomer)", @@ -285,16 +276,12 @@ public class EdgeController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(edgeService.findEdgesByTenantIdAndType(tenantId, type, pageLink)); - } else { - return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(edgeService.findEdgesByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); } } @@ -318,16 +305,12 @@ public class EdgeController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(edgeService.findEdgeInfosByTenantIdAndType(tenantId, type, pageLink)); - } else { - return checkNotNull(edgeService.findEdgeInfosByTenantId(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(edgeService.findEdgeInfosByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(edgeService.findEdgeInfosByTenantId(tenantId, pageLink)); } } @@ -340,12 +323,8 @@ public class EdgeController extends BaseController { @ResponseBody public Edge getTenantEdge(@ApiParam(value = "Unique name of the edge", required = true) @RequestParam String edgeName) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName)); } @ApiOperation(value = "Set root rule chain for provided edge (setEdgeRootRuleChain)", @@ -391,22 +370,18 @@ public class EdgeController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - PageData result; - if (type != null && type.trim().length() > 0) { - result = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); - } else { - result = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); - } - return checkNotNull(result); - } catch (Exception e) { - throw handleException(e); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData result; + if (type != null && type.trim().length() > 0) { + result = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); + } else { + result = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); } + return checkNotNull(result); } @ApiOperation(value = "Get Customer Edge Infos (getCustomerEdgeInfos)", @@ -431,22 +406,18 @@ public class EdgeController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - PageData result; - if (type != null && type.trim().length() > 0) { - result = edgeService.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); - } else { - result = edgeService.findEdgeInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); - } - return checkNotNull(result); - } catch (Exception e) { - throw handleException(e); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData result; + if (type != null && type.trim().length() > 0) { + result = edgeService.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); + } else { + result = edgeService.findEdgeInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); } + return checkNotNull(result); } @ApiOperation(value = "Get Edges By Ids (getEdgesByIds)", @@ -457,27 +428,23 @@ public class EdgeController extends BaseController { @ResponseBody public List getEdgesByIds( @ApiParam(value = "A list of edges ids, separated by comma ','", required = true) - @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException { + @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("edgeIds", strEdgeIds); - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - CustomerId customerId = user.getCustomerId(); - List edgeIds = new ArrayList<>(); - for (String strEdgeId : strEdgeIds) { - edgeIds.add(new EdgeId(toUUID(strEdgeId))); - } - ListenableFuture> edgesFuture; - if (customerId == null || customerId.isNullUid()) { - edgesFuture = edgeService.findEdgesByTenantIdAndIdsAsync(tenantId, edgeIds); - } else { - edgesFuture = edgeService.findEdgesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, edgeIds); - } - List edges = edgesFuture.get(); - return checkNotNull(edges); - } catch (Exception e) { - throw handleException(e); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = user.getCustomerId(); + List edgeIds = new ArrayList<>(); + for (String strEdgeId : strEdgeIds) { + edgeIds.add(new EdgeId(toUUID(strEdgeId))); + } + ListenableFuture> edgesFuture; + if (customerId == null || customerId.isNullUid()) { + edgesFuture = edgeService.findEdgesByTenantIdAndIdsAsync(tenantId, edgeIds); + } else { + edgesFuture = edgeService.findEdgesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, edgeIds); } + List edges = edgesFuture.get(); + return checkNotNull(edges); } @ApiOperation(value = "Find related edges (findByQuery)", @@ -488,27 +455,23 @@ public class EdgeController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edges", method = RequestMethod.POST) @ResponseBody - public List findByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException { + public List findByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getEdgeTypes()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - List edges = checkNotNull(edgeService.findEdgesByQuery(tenantId, query).get()); - edges = edges.stream().filter(edge -> { - try { - accessControlService.checkPermission(user, Resource.EDGE, Operation.READ, edge.getId(), edge); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); - return edges; - } catch (Exception e) { - throw handleException(e); - } + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List edges = checkNotNull(edgeService.findEdgesByQuery(tenantId, query).get()); + edges = edges.stream().filter(edge -> { + try { + accessControlService.checkPermission(user, Resource.EDGE, Operation.READ, edge.getId(), edge); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + return edges; } @ApiOperation(value = "Get Edge Types (getEdgeTypes)", @@ -518,15 +481,11 @@ public class EdgeController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edge/types", method = RequestMethod.GET) @ResponseBody - public List getEdgeTypes() throws ThingsboardException { - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - ListenableFuture> edgeTypes = edgeService.findEdgeTypesByTenantId(tenantId); - return checkNotNull(edgeTypes.get()); - } catch (Exception e) { - throw handleException(e); - } + public List getEdgeTypes() throws ThingsboardException, ExecutionException, InterruptedException { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + ListenableFuture> edgeTypes = edgeService.findEdgeTypesByTenantId(tenantId); + return checkNotNull(edgeTypes.get()); } @ApiOperation(value = "Sync edge (syncEdge)", @@ -537,22 +496,18 @@ public class EdgeController extends BaseController { public DeferredResult syncEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("edgeId") String strEdgeId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); - try { - final DeferredResult response = new DeferredResult<>(); - if (isEdgesEnabled()) { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - edgeId = checkNotNull(edgeId); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId); - edgeRpcService.processSyncRequest(request, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse)); - } else { - throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); - } - return response; - } catch (Exception e) { - throw handleException(e); + final DeferredResult response = new DeferredResult<>(); + if (isEdgesEnabled()) { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId); + edgeRpcService.processSyncRequest(request, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse)); + } else { + throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } + return response; } private void reply(DeferredResult response, FromEdgeSyncResponse fromEdgeSyncResponse) { @@ -570,15 +525,11 @@ public class EdgeController extends BaseController { @ResponseBody public String findMissingToRelatedRuleChains(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("edgeId") String strEdgeId) throws ThingsboardException { - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - edgeId = checkNotNull(edgeId); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - return edgeService.findMissingToRelatedRuleChains(tenantId, edgeId, TbRuleChainInputNode.class.getName()); - } catch (Exception e) { - throw handleException(e); - } + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + return edgeService.findMissingToRelatedRuleChains(tenantId, edgeId, TbRuleChainInputNode.class.getName()); } @ApiOperation(value = "Import the bulk of edges (processEdgesBulkImport)", @@ -595,4 +546,20 @@ public class EdgeController extends BaseController { return edgeBulkImportService.processBulkImport(request, user); } + + @ApiOperation(value = "Get Edge Docker Install Instructions (getEdgeDockerInstallInstructions)", + notes = "Get a docker install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/instructions/{edgeId}", method = RequestMethod.GET) + @ResponseBody + public EdgeInstallInstructions getEdgeDockerInstallInstructions( + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("edgeId") String strEdgeId, + HttpServletRequest request) throws ThingsboardException { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + Edge edge = checkEdgeId(edgeId, Operation.READ); + return checkNotNull(edgeInstallService.getDockerInstallInstructions(getTenantId(), edge, request)); + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java index 5dd43736a8..fc85f439e0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,14 +81,10 @@ public class EdgeEventController extends BaseController { @ApiParam(value = "Timestamp. Edge events with creation time after it won't be queried") @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - checkEdgeId(edgeId, Operation.READ); - TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false)); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java index f2bfbc9478..b5c8e18ed2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; @@ -61,16 +60,16 @@ import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.BRANCH_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; -import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.VC_REQUEST_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.VERSION_ID_PARAM_DESCRIPTION; diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index bc6c628bff..fbb8901ca9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,9 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -38,6 +40,7 @@ import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.query.EntityQueryService; +import org.thingsboard.server.service.security.permission.Operation; import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; @@ -54,18 +57,14 @@ public class EntityQueryController extends BaseController { private static final int MAX_PAGE_SIZE = 100; @ApiOperation(value = "Count Entities by Query", notes = ENTITY_COUNT_QUERY_DESCRIPTION) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST) @ResponseBody public long countEntitiesByQuery( @ApiParam(value = "A JSON value representing the entity count query. See API call notes above for more details.") @RequestBody EntityCountQuery query) throws ThingsboardException { checkNotNull(query); - try { - return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); - } catch (Exception e) { - throw handleException(e); - } + return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION) @@ -76,11 +75,7 @@ public class EntityQueryController extends BaseController { @ApiParam(value = "A JSON value representing the entity data query. See API call notes above for more details.") @RequestBody EntityDataQuery query) throws ThingsboardException { checkNotNull(query); - try { - return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); - } catch (Exception e) { - throw handleException(e); - } + return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Alarms by Query", notes = ALARM_DATA_QUERY_DESCRIPTION) @@ -91,11 +86,26 @@ public class EntityQueryController extends BaseController { @ApiParam(value = "A JSON value representing the alarm data query. See API call notes above for more details.") @RequestBody AlarmDataQuery query) throws ThingsboardException { checkNotNull(query); - try { - return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); - } catch (Exception e) { - throw handleException(e); + checkNotNull(query.getPageLink()); + UserId assigneeId = query.getPageLink().getAssigneeId(); + if (assigneeId != null) { + checkUserId(assigneeId, Operation.READ); + } + return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); + } + + @ApiOperation(value = "Count Alarms by Query (countAlarmsByQuery)", notes = "Returns the number of alarms that match the query definition.") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarmsQuery/count", method = RequestMethod.POST) + @ResponseBody + public long countAlarmsByQuery(@ApiParam(value = "A JSON value representing the alarm count query.") + @RequestBody AlarmCountQuery query) throws ThingsboardException { + checkNotNull(query); + UserId assigneeId = query.getAssigneeId(); + if (assigneeId != null) { + checkUserId(assigneeId, Operation.READ); } + return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Entity Keys by Query", @@ -112,15 +122,11 @@ public class EntityQueryController extends BaseController { @RequestParam("attributes") boolean isAttributes) throws ThingsboardException { TenantId tenantId = getTenantId(); checkNotNull(query); - try { - EntityDataPageLink pageLink = query.getPageLink(); - if (pageLink.getPageSize() > MAX_PAGE_SIZE) { - pageLink.setPageSize(MAX_PAGE_SIZE); - } - return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes); - } catch (Exception e) { - throw handleException(e); + EntityDataPageLink pageLink = query.getPageLink(); + if (pageLink.getPageSize() > MAX_PAGE_SIZE) { + pageLink.setPageSize(MAX_PAGE_SIZE); } + return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index efc4237b3f..08448f1d28 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; @@ -143,21 +144,17 @@ public class EntityRelationController extends BaseController { @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { - try { - checkParameter(FROM_ID, strFromId); - checkParameter(FROM_TYPE, strFromType); - checkParameter(RELATION_TYPE, strRelationType); - checkParameter(TO_ID, strToId); - checkParameter(TO_TYPE, strToType); - EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); - EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId); - checkEntityId(fromId, Operation.READ); - checkEntityId(toId, Operation.READ); - RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); - return checkNotNull(relationService.getRelation(getTenantId(), fromId, toId, strRelationType, typeGroup)); - } catch (Exception e) { - throw handleException(e); - } + checkParameter(FROM_ID, strFromId); + checkParameter(FROM_TYPE, strFromType); + checkParameter(RELATION_TYPE, strRelationType); + checkParameter(TO_ID, strToId); + checkParameter(TO_TYPE, strToType); + EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); + EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId); + checkEntityId(fromId, Operation.READ); + checkEntityId(toId, Operation.READ); + RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); + return checkNotNull(relationService.getRelation(getTenantId(), fromId, toId, strRelationType, typeGroup)); } @ApiOperation(value = "Get List of Relations (findByFrom)", @@ -176,11 +173,7 @@ public class EntityRelationController extends BaseController { EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); checkEntityId(entityId, Operation.READ); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); - try { - return checkNotNull(filterRelationsByReadPermission(relationService.findByFrom(getTenantId(), entityId, typeGroup))); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(filterRelationsByReadPermission(relationService.findByFrom(getTenantId(), entityId, typeGroup))); } @ApiOperation(value = "Get List of Relation Infos (findInfoByFrom)", @@ -193,17 +186,13 @@ public class EntityRelationController extends BaseController { public List findInfoByFrom(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) - @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); checkEntityId(entityId, Operation.READ); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); - try { - return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get())); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get())); } @ApiOperation(value = "Get List of Relations (findByFrom)", @@ -224,11 +213,7 @@ public class EntityRelationController extends BaseController { EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); checkEntityId(entityId, Operation.READ); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); - try { - return checkNotNull(filterRelationsByReadPermission(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup))); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(filterRelationsByReadPermission(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup))); } @ApiOperation(value = "Get List of Relations (findByTo)", @@ -247,11 +232,7 @@ public class EntityRelationController extends BaseController { EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId); checkEntityId(entityId, Operation.READ); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); - try { - return checkNotNull(filterRelationsByReadPermission(relationService.findByTo(getTenantId(), entityId, typeGroup))); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(filterRelationsByReadPermission(relationService.findByTo(getTenantId(), entityId, typeGroup))); } @ApiOperation(value = "Get List of Relation Infos (findInfoByTo)", @@ -264,17 +245,13 @@ public class EntityRelationController extends BaseController { public List findInfoByTo(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) - @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException { checkParameter(TO_ID, strToId); checkParameter(TO_TYPE, strToType); EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId); checkEntityId(entityId, Operation.READ); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); - try { - return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get())); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get())); } @ApiOperation(value = "Get List of Relations (findByTo)", @@ -295,11 +272,7 @@ public class EntityRelationController extends BaseController { EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId); checkEntityId(entityId, Operation.READ); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); - try { - return checkNotNull(filterRelationsByReadPermission(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup))); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(filterRelationsByReadPermission(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup))); } @ApiOperation(value = "Find related entities (findByQuery)", @@ -310,16 +283,12 @@ public class EntityRelationController extends BaseController { @RequestMapping(value = "/relations", method = RequestMethod.POST) @ResponseBody public List findByQuery(@ApiParam(value = "A JSON value representing the entity relations query object.", required = true) - @RequestBody EntityRelationsQuery query) throws ThingsboardException { + @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getFilters()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); - try { - return checkNotNull(filterRelationsByReadPermission(relationService.findByQuery(getTenantId(), query).get())); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(filterRelationsByReadPermission(relationService.findByQuery(getTenantId(), query).get())); } @ApiOperation(value = "Find related entity infos (findInfoByQuery)", @@ -330,16 +299,12 @@ public class EntityRelationController extends BaseController { @RequestMapping(value = "/relations/info", method = RequestMethod.POST) @ResponseBody public List findInfoByQuery(@ApiParam(value = "A JSON value representing the entity relations query object.", required = true) - @RequestBody EntityRelationsQuery query) throws ThingsboardException { + @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getFilters()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); - try { - return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByQuery(getTenantId(), query).get())); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByQuery(getTenantId(), query).get())); } private void checkCanCreateRelation(EntityId entityId) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index 0f299fefae..b2904a0989 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,7 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; @@ -105,11 +106,7 @@ public class EntityViewController extends BaseController { @ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(ENTITY_VIEW_ID, strEntityViewId); - try { - return checkEntityViewId(new EntityViewId(toUUID(strEntityViewId)), Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + return checkEntityViewId(new EntityViewId(toUUID(strEntityViewId)), Operation.READ); } @ApiOperation(value = "Get Entity View info (getEntityViewInfoById)", @@ -123,12 +120,8 @@ public class EntityViewController extends BaseController { @ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(ENTITY_VIEW_ID, strEntityViewId); - try { - EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); - return checkEntityViewInfoId(entityViewId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); + return checkEntityViewInfoId(entityViewId, Operation.READ); } @ApiOperation(value = "Save or update entity view (saveEntityView)", @@ -177,12 +170,8 @@ public class EntityViewController extends BaseController { public EntityView getTenantEntityView( @ApiParam(value = "Entity View name") @RequestParam String entityViewName) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName)); } @ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)", @@ -249,18 +238,14 @@ public class EntityViewController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId, customerId, pageLink, type)); - } else { - return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId, customerId, pageLink, type)); + } else { + return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } } @@ -286,18 +271,14 @@ public class EntityViewController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); - } else { - return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); + } else { + return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } } @@ -320,17 +301,13 @@ public class EntityViewController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - - if (type != null && type.trim().length() > 0) { - return checkNotNull(entityViewService.findEntityViewByTenantIdAndType(tenantId, pageLink, type)); - } else { - return checkNotNull(entityViewService.findEntityViewByTenantId(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + + if (type != null && type.trim().length() > 0) { + return checkNotNull(entityViewService.findEntityViewByTenantIdAndType(tenantId, pageLink, type)); + } else { + return checkNotNull(entityViewService.findEntityViewByTenantId(tenantId, pageLink)); } } @@ -353,16 +330,12 @@ public class EntityViewController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { - return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndType(tenantId, type, pageLink)); - } else { - return checkNotNull(entityViewService.findEntityViewInfosByTenantId(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(entityViewService.findEntityViewInfosByTenantId(tenantId, pageLink)); } } @@ -375,25 +348,21 @@ public class EntityViewController extends BaseController { @ResponseBody public List findByQuery( @ApiParam(value = "The entity view search query JSON") - @RequestBody EntityViewSearchQuery query) throws ThingsboardException { + @RequestBody EntityViewSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getEntityViewTypes()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); - try { - List entityViews = checkNotNull(entityViewService.findEntityViewsByQuery(getTenantId(), query).get()); - entityViews = entityViews.stream().filter(entityView -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); - return entityViews; - } catch (Exception e) { - throw handleException(e); - } + List entityViews = checkNotNull(entityViewService.findEntityViewsByQuery(getTenantId(), query).get()); + entityViews = entityViews.stream().filter(entityView -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + return entityViews; } @ApiOperation(value = "Get Entity View Types (getEntityViewTypes)", @@ -402,15 +371,11 @@ public class EntityViewController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entityView/types", method = RequestMethod.GET) @ResponseBody - public List getEntityViewTypes() throws ThingsboardException { - try { - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - ListenableFuture> entityViewTypes = entityViewService.findEntityViewTypesByTenantId(tenantId); - return checkNotNull(entityViewTypes.get()); - } catch (Exception e) { - throw handleException(e); - } + public List getEntityViewTypes() throws ThingsboardException, ExecutionException, InterruptedException { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + ListenableFuture> entityViewTypes = entityViewService.findEntityViewTypesByTenantId(tenantId); + return checkNotNull(entityViewTypes.get()); } @ApiOperation(value = "Make entity view publicly available (assignEntityViewToPublicCustomer)", @@ -426,11 +391,7 @@ public class EntityViewController extends BaseController { checkParameter(ENTITY_VIEW_ID, strEntityViewId); EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); checkEntityViewId(entityViewId, Operation.ASSIGN_TO_CUSTOMER); - - Customer publicCustomer = customerService.findOrCreatePublicCustomer(getTenantId()); - - return tbEntityViewService.assignEntityViewToPublicCustomer(getTenantId(), getCurrentUser().getCustomerId(), - publicCustomer, entityViewId, getCurrentUser()); + return tbEntityViewService.assignEntityViewToPublicCustomer(getTenantId(), entityViewId, getCurrentUser()); } @ApiOperation(value = "Assign entity view to edge (assignEntityViewToEdge)", @@ -497,32 +458,28 @@ public class EntityViewController extends BaseController { @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - checkEdgeId(edgeId, Operation.READ); - TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - PageData nonFilteredResult; - if (type != null && type.trim().length() > 0) { - nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); - } else { - nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); - } - List filteredEntityViews = nonFilteredResult.getData().stream().filter(entityView -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); - PageData filteredResult = new PageData<>(filteredEntityViews, - nonFilteredResult.getTotalPages(), - nonFilteredResult.getTotalElements(), - nonFilteredResult.hasNext()); - return checkNotNull(filteredResult); - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult; + if (type != null && type.trim().length() > 0) { + nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); + } else { + nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); } + List filteredEntityViews = nonFilteredResult.getData().stream().filter(entityView -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredEntityViews, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index db040599fa..ec75f981a7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.event.EventType; @@ -41,12 +40,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; -import java.util.Locale; - import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE; @@ -248,14 +244,10 @@ public class EventController extends BaseController { @RequestBody EventFilter eventFilter) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); - try { - EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); - checkEntityId(entityId, Operation.WRITE); + EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); + checkEntityId(entityId, Operation.WRITE); - eventService.removeEvents(getTenantId(), entityId, eventFilter, startTime, endTime); - } catch (Exception e) { - throw handleException(e); - } + eventService.removeEvents(getTenantId(), entityId, eventFilter, startTime, endTime); } private static EventType resolveEventType(String eventType) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java b/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java index ca2101f145..2a05e9bf7c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java +++ b/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java index 8a47ed2c15..8949cd95dd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java +++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; @@ -28,6 +27,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault; @@ -64,11 +64,7 @@ public class Lwm2mController extends BaseController { public LwM2MServerSecurityConfigDefault getLwm2mBootstrapSecurityInfo( @ApiParam(value = IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION) @PathVariable(IS_BOOTSTRAP_SERVER) boolean bootstrapServer) throws ThingsboardException { - try { - return lwM2MService.getServerSecurityInfo(bootstrapServer); - } catch (Exception e) { - throw handleException(e); - } + return lwM2MService.getServerSecurityInfo(bootstrapServer); } @ApiOperation(hidden = true, value = "Save device with credentials (Deprecated)") @@ -76,9 +72,8 @@ public class Lwm2mController extends BaseController { @RequestMapping(value = "/lwm2m/device-credentials", method = RequestMethod.POST) @ResponseBody public Device saveDeviceWithCredentials(@RequestBody Map, Object> deviceWithDeviceCredentials) throws ThingsboardException { - ObjectMapper mapper = new ObjectMapper(); - Device device = checkNotNull(mapper.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); - DeviceCredentials credentials = checkNotNull(mapper.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); + Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); + DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials)); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java new file mode 100644 index 0000000000..0f4a288e33 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -0,0 +1,440 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.NotificationId; +import org.thingsboard.server.common.data.id.NotificationRequestId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; +import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.dao.notification.NotificationRequestService; +import org.thingsboard.server.dao.notification.NotificationService; +import org.thingsboard.server.dao.notification.NotificationSettingsService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.notification.NotificationProcessingContext; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import javax.validation.Valid; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class NotificationController extends BaseController { + + private final NotificationService notificationService; + private final NotificationRequestService notificationRequestService; + private final NotificationTemplateService notificationTemplateService; + private final NotificationTargetService notificationTargetService; + private final NotificationCenter notificationCenter; + private final NotificationSettingsService notificationSettingsService; + + @ApiOperation(value = "Get notifications (getNotifications)", + notes = "Returns the page of notifications for current user." + NEW_LINE + + PAGE_DATA_PARAMETERS + + AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE + + "**WebSocket API**:\n\n" + + "There are 2 types of subscriptions: one for unread notifications count, another for unread notifications themselves.\n\n" + + "The URI for opening WS session for notifications: `/api/ws/plugins/notifications`.\n\n" + + "Subscription command for unread notifications count:\n" + + "```\n{\n \"unreadCountSubCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" + + "To subscribe for latest unread notifications:\n" + + "```\n{\n \"unreadSubCmd\": {\n \"cmdId\": 1234,\n \"limit\": 10\n }\n}\n```\n" + + "To unsubscribe from any subscription:\n" + + "```\n{\n \"unsubCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" + + "To mark certain notifications as read, use following command:\n" + + "```\n{\n \"markAsReadCmd\": {\n \"cmdId\": 1234,\n \"notifications\": [\n \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\",\n \"5b6dfee0-8d0d-11ed-b61f-35a57b03dade\"\n ]\n }\n}\n\n```\n" + + "To mark all notifications as read:\n" + + "```\n{\n \"markAllAsReadCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" + + "\n\n" + + "Update structure for unread **notifications count subscription**:\n" + + "```\n{\n \"cmdId\": 1234,\n \"totalUnreadCount\": 55\n}\n```\n" + + "For **notifications subscription**:\n" + + "- full update of latest unread notifications:\n" + + "```\n{\n" + + " \"cmdId\": 1234,\n" + + " \"notifications\": [\n" + + " {\n" + + " \"id\": {\n" + + " \"entityType\": \"NOTIFICATION\",\n" + + " \"id\": \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\"\n" + + " },\n" + + " ...\n" + + " }\n" + + " ],\n" + + " \"totalUnreadCount\": 1\n" + + "}\n```\n" + + "- when new notification arrives or shown notification is updated:\n" + + "```\n{\n" + + " \"cmdId\": 1234,\n" + + " \"update\": {\n" + + " \"id\": {\n" + + " \"entityType\": \"NOTIFICATION\",\n" + + " \"id\": \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\"\n" + + " },\n" + + " # updated notification info, text, subject etc.\n" + + " ...\n" + + " },\n" + + " \"totalUnreadCount\": 2\n" + + "}\n```\n" + + "- when unread notifications count changes:\n" + + "```\n{\n" + + " \"cmdId\": 1234,\n" + + " \"totalUnreadCount\": 5\n" + + "}\n```") + @GetMapping("/notifications") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public PageData getNotifications(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = "Case-insensitive 'substring' filter based on notification subject or text") + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder, + @ApiParam(value = "To search for unread notifications only") + @RequestParam(defaultValue = "false") boolean unreadOnly, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + // no permissions + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink); + } + + @ApiOperation(value = "Mark notification as read (markNotificationAsRead)", + notes = "Marks notification as read by its id." + + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PutMapping("/notification/{id}/read") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public void markNotificationAsRead(@PathVariable UUID id, + @AuthenticationPrincipal SecurityUser user) { + // no permissions + NotificationId notificationId = new NotificationId(id); + notificationCenter.markNotificationAsRead(user.getTenantId(), user.getId(), notificationId); + } + + @ApiOperation(value = "Mark all notifications as read (markAllNotificationsAsRead)", + notes = "Marks all unread notifications as read." + + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PutMapping("/notifications/read") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) { + // no permissions + notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId()); + } + + @ApiOperation(value = "Delete notification (deleteNotification)", + notes = "Deletes notification by its id." + + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @DeleteMapping("/notification/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public void deleteNotification(@PathVariable UUID id, + @AuthenticationPrincipal SecurityUser user) { + // no permissions + NotificationId notificationId = new NotificationId(id); + notificationCenter.deleteNotification(user.getTenantId(), user.getId(), notificationId); + } + + @ApiOperation(value = "Create notification request (createNotificationRequest)", + notes = "Processes notification request.\n" + + "Mandatory request properties are `targets` (list of targets ids to send notification to), " + + "and either `templateId` (existing notification template id) or `template` (to send notification without saving the template).\n" + + "Optionally, you can set `sendingDelayInSec` inside the `additionalConfig` field to schedule the notification." + NEW_LINE + + "For each enabled delivery method in the notification template, there must be a target in the `targets` list that supports this delivery method: " + + "if you chose `WEB`, `EMAIL` or `SMS` - there must be at least one target in `targets` of `PLATFORM_USERS` type.\n" + + "For `SLACK` delivery method - you need to chose at least one `SLACK` notification target." + NEW_LINE + + "Notification request object with `PROCESSING` status will be returned immediately, " + + "and the notification sending itself is done asynchronously. After all notifications are sent, " + + "the `status` of the request becomes `SENT`. Use `getNotificationRequestById` to see " + + "the notification request processing status and some sending stats. " + NEW_LINE + + "Here is an example of notification request to one target using saved template:\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"templateId\": {\n" + + " \"entityType\": \"NOTIFICATION_TEMPLATE\",\n" + + " \"id\": \"6dbc3670-e4dd-11ed-9401-dbcc5dff78be\"\n" + + " },\n" + + " \"targets\": [\n" + + " \"320e3ed0-d785-11ed-a06c-21dd57dd88ca\"\n" + + " ],\n" + + " \"additionalConfig\": {\n" + + " \"sendingDelayInSec\": 0\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH + ) + @PostMapping("/notification/request") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationRequest createNotificationRequest(@RequestBody @Valid NotificationRequest notificationRequest, + @AuthenticationPrincipal SecurityUser user) throws Exception { + if (notificationRequest.getId() != null) { + throw new IllegalArgumentException("Notification request cannot be updated. You may only cancel/delete it"); + } + notificationRequest.setTenantId(user.getTenantId()); + checkEntity(notificationRequest.getId(), notificationRequest, NOTIFICATION); + + notificationRequest.setOriginatorEntityId(user.getId()); + notificationRequest.setInfo(null); + notificationRequest.setRuleId(null); + notificationRequest.setStatus(null); + notificationRequest.setStats(null); + + return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, (tenantId, request) -> notificationCenter.processNotificationRequest(tenantId, request, null)); + } + + @ApiOperation(value = "Get notification request preview (getNotificationRequestPreview)", + notes = "Returns preview for notification request." + NEW_LINE + + "`processedTemplates` shows how the notifications for each delivery method will look like " + + "for the first recipient of the corresponding notification target." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PostMapping("/notification/request/preview") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request, + @ApiParam(value = "Amount of the recipients to show in preview") + @RequestParam(defaultValue = "20") int recipientsPreviewSize, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + NotificationTemplate template; + if (request.getTemplateId() != null) { + template = checkEntityId(request.getTemplateId(), notificationTemplateService::findNotificationTemplateById, Operation.READ); + } else { + template = request.getTemplate(); + } + if (template == null) { + throw new IllegalArgumentException("Template is missing"); + } + request.setOriginatorEntityId(user.getId()); + List targets = request.getTargets().stream() + .map(NotificationTargetId::new) + .map(targetId -> notificationTargetService.findNotificationTargetById(user.getTenantId(), targetId)) + .sorted(Comparator.comparing(target -> target.getConfiguration().getType())) + .collect(Collectors.toList()); + + NotificationRequestPreview preview = new NotificationRequestPreview(); + + Set recipientsPreview = new LinkedHashSet<>(); + Map recipientsCountByTarget = new LinkedHashMap<>(); + Map firstRecipient = new HashMap<>(); + for (NotificationTarget target : targets) { + int recipientsCount; + List recipientsPart; + NotificationTargetType targetType = target.getConfiguration().getType(); + if (targetType == NotificationTargetType.PLATFORM_USERS) { + PageData recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), + (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null, + new SortOrder("createdTime", SortOrder.Direction.DESC))); + recipientsCount = (int) recipients.getTotalElements(); + recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); + } else { + recipientsCount = 1; + recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation()); + } + firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null); + for (NotificationRecipient recipient : recipientsPart) { + if (recipientsPreview.size() < recipientsPreviewSize) { + String title = recipient.getTitle(); + if (recipient instanceof SlackConversation) { + title = ((SlackConversation) recipient).getPointer() + title; + } else if (recipient instanceof User) { + if (!title.equals(recipient.getEmail())) { + title += " (" + recipient.getEmail() + ")"; + } + } + recipientsPreview.add(title); + } else { + break; + } + } + recipientsCountByTarget.put(target.getName(), recipientsCount); + } + preview.setRecipientsPreview(recipientsPreview); + preview.setRecipientsCountByTarget(recipientsCountByTarget); + preview.setTotalRecipientsCount(recipientsCountByTarget.values().stream().mapToInt(Integer::intValue).sum()); + + Set deliveryMethods = template.getConfiguration().getDeliveryMethodsTemplates().entrySet() + .stream().filter(entry -> entry.getValue().isEnabled()).map(Map.Entry::getKey).collect(Collectors.toSet()); + NotificationProcessingContext ctx = NotificationProcessingContext.builder() + .tenantId(user.getTenantId()) + .request(request) + .deliveryMethods(deliveryMethods) + .template(template) + .settings(null) + .build(); + Map processedTemplates = ctx.getDeliveryMethods().stream() + .collect(Collectors.toMap(m -> m, deliveryMethod -> { + NotificationTargetType targetType = NotificationTargetType.forDeliveryMethod(deliveryMethod); + return ctx.getProcessedTemplate(deliveryMethod, firstRecipient.get(targetType)); + })); + preview.setProcessedTemplates(processedTemplates); + + return preview; + } + + @ApiOperation(value = "Get notification request by id (getNotificationRequestById)", + notes = "Fetches notification request info by request id." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/notification/request/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationRequestInfo getNotificationRequestById(@PathVariable UUID id) throws ThingsboardException { + NotificationRequestId notificationRequestId = new NotificationRequestId(id); + return checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestInfoById, Operation.READ); + } + + @ApiOperation(value = "Get notification requests (getNotificationRequests)", + notes = "Returns the page of notification requests submitted by users of this tenant or sysadmins." + NEW_LINE + + PAGE_DATA_PARAMETERS + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/notification/requests") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getNotificationRequests(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = "Case-insensitive 'substring' filed based on the used template name") + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + // generic permission + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(user.getTenantId(), EntityType.USER, pageLink); + } + + @ApiOperation(value = "Delete notification request (deleteNotificationRequest)", + notes = "Deletes notification request by its id." + NEW_LINE + + "If the request has status `SENT` - all sent notifications for this request will be deleted. " + + "If it is `SCHEDULED`, the request will be cancelled." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @DeleteMapping("/notification/request/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public void deleteNotificationRequest(@PathVariable UUID id) throws Exception { + NotificationRequestId notificationRequestId = new NotificationRequestId(id); + NotificationRequest notificationRequest = checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestById, Operation.DELETE); + doDeleteAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationCenter::deleteNotificationRequest); + } + + + @ApiOperation(value = "Save notification settings (saveNotificationSettings)", + notes = "Saves notification settings for this tenant or sysadmin.\n" + + "`deliveryMethodsConfigs` of the settings must be specified." + NEW_LINE + + "Here is an example of the notification settings with Slack configuration:\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"deliveryMethodsConfigs\": {\n" + + " \"SLACK\": {\n" + + " \"method\": \"SLACK\",\n" + + " \"botToken\": \"xoxb-....\"\n" + + " }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PostMapping("/notification/settings") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationSettings saveNotificationSettings(@RequestBody @Valid NotificationSettings notificationSettings, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.WRITE); + TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId(); + notificationSettingsService.saveNotificationSettings(tenantId, notificationSettings); + return notificationSettings; + } + + @ApiOperation(value = "Get notification settings (getNotificationSettings)", + notes = "Retrieves notification settings for this tenant or sysadmin." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/notification/settings") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationSettings getNotificationSettings(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ); + TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId(); + return notificationSettingsService.findNotificationSettings(tenantId); + } + + @ApiOperation(value = "Get available delivery methods (getAvailableDeliveryMethods)", + notes = "Returns the list of delivery methods that are properly configured and are allowed to be used for sending notifications." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/notification/deliveryMethods") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public Set getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ); + return notificationCenter.getAvailableDeliveryMethods(user.getTenantId()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java new file mode 100644 index 0000000000..ccf2124113 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java @@ -0,0 +1,183 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; + +import javax.validation.Valid; +import java.util.UUID; + +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; + +@RestController +@TbCoreComponent +@RequestMapping("/api/notification") +@RequiredArgsConstructor +@Slf4j +public class NotificationRuleController extends BaseController { + + private final NotificationRuleService notificationRuleService; + + @ApiOperation(value = "Save notification rule (saveNotificationRule)", + notes = "Creates or updates notification rule. " + NEW_LINE + + "Mandatory properties are `name`, `templateId` (of a template with `notificationType` matching to rule's `triggerType`), " + + "`triggerType`, `triggerConfig` and `recipientConfig`. Additionally, you may specify rule `description` " + + "inside of `additionalConfig`." + NEW_LINE + + "Trigger type of the rule cannot be changed. " + + "Available trigger types for tenant: `ENTITY_ACTION`, `ALARM`, `ALARM_COMMENT`, `ALARM_ASSIGNMENT`, " + + "`DEVICE_ACTIVITY`, `RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT`.\n" + + "For sysadmin, there are following trigger types available: `ENTITIES_LIMIT`, `API_USAGE_LIMIT`, " + + "`NEW_PLATFORM_VERSION`." + NEW_LINE + + "Here is an example of notification rule to send notification when a " + + "device, asset or customer is created or deleted:\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"name\": \"Entity action\",\n" + + " \"templateId\": {\n" + + " \"entityType\": \"NOTIFICATION_TEMPLATE\",\n" + + " \"id\": \"32117320-d785-11ed-a06c-21dd57dd88ca\"\n" + + " },\n" + + " \"triggerType\": \"ENTITY_ACTION\",\n" + + " \"triggerConfig\": {\n" + + " \"entityTypes\": [\n" + + " \"CUSTOMER\",\n" + + " \"DEVICE\",\n" + + " \"ASSET\"\n" + + " ],\n" + + " \"created\": true,\n" + + " \"updated\": false,\n" + + " \"deleted\": true,\n" + + " \"triggerType\": \"ENTITY_ACTION\"\n" + + " },\n" + + " \"recipientsConfig\": {\n" + + " \"targets\": [\n" + + " \"320f2930-d785-11ed-a06c-21dd57dd88ca\"\n" + + " ],\n" + + " \"triggerType\": \"ENTITY_ACTION\"\n" + + " },\n" + + " \"additionalConfig\": {\n" + + " \"description\": \"Send notification to tenant admins or customer users when a device, asset or customer is created\"\n" + + " },\n" + + " \"templateName\": \"Entity action notification\",\n" + + " \"deliveryMethods\": [\n" + + " \"WEB\"\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PostMapping("/rule") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule, + @AuthenticationPrincipal SecurityUser user) throws Exception { + notificationRule.setTenantId(user.getTenantId()); + checkEntity(notificationRule.getId(), notificationRule, NOTIFICATION); + + NotificationRuleTriggerType triggerType = notificationRule.getTriggerType(); + if ((user.isTenantAdmin() && !triggerType.isTenantLevel()) || (user.isSystemAdmin() && triggerType.isTenantLevel())) { + throw new IllegalArgumentException("Trigger type " + triggerType + " is not available"); + } + + boolean created = notificationRule.getId() == null; + notificationRule = doSaveAndLog(EntityType.NOTIFICATION_RULE, notificationRule, notificationRuleService::saveNotificationRule); + tbClusterService.broadcastEntityStateChangeEvent(user.getTenantId(), notificationRule.getId(), created ? + ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + return notificationRule; + } + + @ApiOperation(value = "Get notification rule by id (getNotificationRuleById)", + notes = "Fetches notification rule info by rule's id.\n" + + "In addition to regular notification rule fields, " + + "there are `templateName` and `deliveryMethods` in the response." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/rule/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationRuleInfo getNotificationRuleById(@PathVariable UUID id) throws ThingsboardException { + NotificationRuleId notificationRuleId = new NotificationRuleId(id); + return checkEntityId(notificationRuleId, notificationRuleService::findNotificationRuleInfoById, Operation.READ); + } + + @ApiOperation(value = "Get notification rules (getNotificationRules)", + notes = "Returns the page of notification rules." + NEW_LINE + + PAGE_DATA_PARAMETERS + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/rules") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getNotificationRules(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = "Case-insensitive 'substring' filter based on rule's name") + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + // generic permission + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return notificationRuleService.findNotificationRulesInfosByTenantId(user.getTenantId(), pageLink); + } + + @ApiOperation(value = "Delete notification rule (deleteNotificationRule)", + notes = "Deletes notification rule by id.\n" + + "Cancels all related scheduled notification requests (e.g. due to escalation table)" + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @DeleteMapping("/rule/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public void deleteNotificationRule(@PathVariable UUID id, + @AuthenticationPrincipal SecurityUser user) throws Exception { + NotificationRuleId notificationRuleId = new NotificationRuleId(id); + NotificationRule notificationRule = checkEntityId(notificationRuleId, notificationRuleService::findNotificationRuleById, Operation.DELETE); + doDeleteAndLog(EntityType.NOTIFICATION_RULE, notificationRule, notificationRuleService::deleteNotificationRuleById); + tbClusterService.broadcastEntityStateChangeEvent(user.getTenantId(), notificationRuleId, ComponentLifecycleEvent.DELETED); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java new file mode 100644 index 0000000000..a4c829796b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java @@ -0,0 +1,245 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; + +import javax.validation.Valid; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; + +@RestController +@TbCoreComponent +@RequestMapping("/api/notification") +@RequiredArgsConstructor +@Slf4j +public class NotificationTargetController extends BaseController { + + private final NotificationTargetService notificationTargetService; + + @ApiOperation(value = "Save notification target (saveNotificationTarget)", + notes = "Creates or updates notification target." + NEW_LINE + + "Available `configuration` types are `PLATFORM_USERS` and `SLACK`.\n" + + "For `PLATFORM_USERS` the `usersFilter` must be specified. " + + "For tenant, there are following users filter types available: " + + "`USER_LIST`, `CUSTOMER_USERS`, `TENANT_ADMINISTRATORS`, `ALL_USERS`, " + + "`ORIGINATOR_ENTITY_OWNER_USERS`, `AFFECTED_USER`.\n" + + "For sysadmin: `TENANT_ADMINISTRATORS`, `AFFECTED_TENANT_ADMINISTRATORS`, " + + "`SYSTEM_ADMINISTRATORS`, `ALL_USERS`." + NEW_LINE + + "Here is an example of tenant-level notification target to send notification to customer's users:\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"name\": \"Users of Customer A\",\n" + + " \"configuration\": {\n" + + " \"type\": \"PLATFORM_USERS\",\n" + + " \"usersFilter\": {\n" + + " \"type\": \"CUSTOMER_USERS\",\n" + + " \"customerId\": \"32499a20-d785-11ed-a06c-21dd57dd88ca\"\n" + + " },\n" + + " \"description\": \"Users of Customer A\"\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PostMapping("/target") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationTarget saveNotificationTarget(@RequestBody @Valid NotificationTarget notificationTarget, + @AuthenticationPrincipal SecurityUser user) throws Exception { + notificationTarget.setTenantId(user.getTenantId()); + checkEntity(notificationTarget.getId(), notificationTarget, NOTIFICATION); + + NotificationTargetConfig targetConfig = notificationTarget.getConfiguration(); + if (targetConfig.getType() == NotificationTargetType.PLATFORM_USERS) { + checkTargetUsers(user, targetConfig); + } + + return doSaveAndLog(EntityType.NOTIFICATION_TARGET, notificationTarget, notificationTargetService::saveNotificationTarget); + } + + @ApiOperation(value = "Get notification target by id (getNotificationTargetById)", + notes = "Fetches notification target by id." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/target/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationTarget getNotificationTargetById(@PathVariable UUID id) throws ThingsboardException { + NotificationTargetId notificationTargetId = new NotificationTargetId(id); + return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, Operation.READ); + } + + @ApiOperation(value = "Get recipients for notification target config (getRecipientsForNotificationTargetConfig)", + notes = "Returns the page of recipients for such notification target configuration." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PostMapping("/target/recipients") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getRecipientsForNotificationTargetConfig(@RequestBody NotificationTarget notificationTarget, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + // generic permission + NotificationTargetConfig targetConfig = notificationTarget.getConfiguration(); + if (targetConfig.getType() == NotificationTargetType.PLATFORM_USERS) { + checkTargetUsers(user, targetConfig); + } else { + throw new IllegalArgumentException("Target type is not platform users"); + } + + PageLink pageLink = createPageLink(pageSize, page, null, null, null); + return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration(), pageLink); + } + + @ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)", + notes = "Returns the list of notification targets found by provided ids." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping(value = "/targets", params = {"ids"}) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public List getNotificationTargetsByIds(@ApiParam(value = "Comma-separated list of uuids representing targets ids", required = true) + @RequestParam("ids") UUID[] ids, + @AuthenticationPrincipal SecurityUser user) { + // generic permission + List targetsIds = Arrays.stream(ids).map(NotificationTargetId::new).collect(Collectors.toList()); + return notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), targetsIds); + } + + @ApiOperation(value = "Get notification targets (getNotificationTargets)", + notes = "Returns the page of notification targets owned by sysadmin or tenant." + NEW_LINE + + PAGE_DATA_PARAMETERS + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/targets") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getNotificationTargets(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = "Case-insensitive 'substring' filed based on the target's name") + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + // generic permission + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink); + } + + @ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)", + notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE + + PAGE_DATA_PARAMETERS + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping(value = "/targets", params = "notificationType") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) NotificationType notificationType, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return notificationTargetService.findNotificationTargetsByTenantIdAndSupportedNotificationType(user.getTenantId(), notificationType, pageLink); + } + + @ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)", + notes = "Deletes notification target by its id." + NEW_LINE + + "This target cannot be referenced by existing scheduled notification requests or any notification rules." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @DeleteMapping("/target/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public void deleteNotificationTargetById(@PathVariable UUID id) throws Exception { + NotificationTargetId notificationTargetId = new NotificationTargetId(id); + NotificationTarget notificationTarget = checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, Operation.DELETE); + doDeleteAndLog(EntityType.NOTIFICATION_TARGET, notificationTarget, notificationTargetService::deleteNotificationTargetById); + } + + private void checkTargetUsers(SecurityUser user, NotificationTargetConfig targetConfig) throws ThingsboardException { + if (user.isSystemAdmin()) { + return; + } + // generic permission for users + UsersFilter usersFilter = ((PlatformUsersNotificationTargetConfig) targetConfig).getUsersFilter(); + switch (usersFilter.getType()) { + case USER_LIST: + for (UUID recipientId : ((UserListFilter) usersFilter).getUsersIds()) { + checkUserId(new UserId(recipientId), Operation.READ); + } + break; + case CUSTOMER_USERS: + CustomerId customerId = new CustomerId(((CustomerUsersFilter) usersFilter).getCustomerId()); + checkEntityId(customerId, Operation.READ); + break; + case TENANT_ADMINISTRATORS: + if (CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantsIds()) || + CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantProfilesIds())) { + throw new AccessDeniedException(""); + } + break; + case SYSTEM_ADMINISTRATORS: + throw new AccessDeniedException(""); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java new file mode 100644 index 0000000000..ab171efacf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java @@ -0,0 +1,197 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.rule.engine.api.slack.SlackService; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; +import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.notification.NotificationSettingsService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; + +import javax.validation.Valid; +import java.util.List; +import java.util.UUID; + +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; + +@RestController +@TbCoreComponent +@RequiredArgsConstructor +@RequestMapping("/api/notification") +public class NotificationTemplateController extends BaseController { + + private final NotificationTemplateService notificationTemplateService; + private final NotificationSettingsService notificationSettingsService; + private final SlackService slackService; + + @ApiOperation(value = "Save notification template (saveNotificationTemplate)", + notes = "Creates or updates notification template." + NEW_LINE + + "Here is an example of template to send notification via Web, SMS and Slack:\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"name\": \"Greetings\",\n" + + " \"notificationType\": \"GENERAL\",\n" + + " \"configuration\": {\n" + + " \"deliveryMethodsTemplates\": {\n" + + " \"WEB\": {\n" + + " \"enabled\": true,\n" + + " \"subject\": \"Greetings\",\n" + + " \"body\": \"Hi there, ${recipientTitle}\",\n" + + " \"additionalConfig\": {\n" + + " \"icon\": {\n" + + " \"enabled\": true,\n" + + " \"icon\": \"back_hand\",\n" + + " \"color\": \"#757575\"\n" + + " },\n" + + " \"actionButtonConfig\": {\n" + + " \"enabled\": false\n" + + " }\n" + + " },\n" + + " \"method\": \"WEB\"\n" + + " },\n" + + " \"SMS\": {\n" + + " \"enabled\": true,\n" + + " \"body\": \"Hi there, ${recipientTitle}\",\n" + + " \"method\": \"SMS\"\n" + + " },\n" + + " \"SLACK\": {\n" + + " \"enabled\": true,\n" + + " \"body\": \"Hi there, @${recipientTitle}\",\n" + + " \"method\": \"SLACK\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PostMapping("/template") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationTemplate saveNotificationTemplate(@RequestBody @Valid NotificationTemplate notificationTemplate) throws Exception { + notificationTemplate.setTenantId(getTenantId()); + checkEntity(notificationTemplate.getId(), notificationTemplate, NOTIFICATION); + return doSaveAndLog(EntityType.NOTIFICATION_TEMPLATE, notificationTemplate, notificationTemplateService::saveNotificationTemplate); + } + + @ApiOperation(value = "Get notification template by id (getNotificationTemplateById)", + notes = "Fetches notification template by id." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/template/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public NotificationTemplate getNotificationTemplateById(@PathVariable UUID id) throws ThingsboardException { + NotificationTemplateId notificationTemplateId = new NotificationTemplateId(id); + return checkEntityId(notificationTemplateId, notificationTemplateService::findNotificationTemplateById, Operation.READ); + } + + @ApiOperation(value = "Get notification templates (getNotificationTemplates)", + notes = "Returns the page of notification templates owned by sysadmin or tenant." + NEW_LINE + + PAGE_DATA_PARAMETERS + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/templates") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getNotificationTemplates(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = "Case-insensitive 'substring' filter based on template's name and notification type") + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder, + @ApiParam(value = "Comma-separated list of notification types to filter the templates") + @RequestParam(required = false) NotificationType[] notificationTypes, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + // generic permission + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (notificationTypes == null || notificationTypes.length == 0) { + notificationTypes = NotificationType.values(); + } + return notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(user.getTenantId(), + List.of(notificationTypes), pageLink); + } + + @ApiOperation(value = "Delete notification template by id (deleteNotificationTemplateById", + notes = "Deletes notification template by its id." + NEW_LINE + + "This template cannot be referenced by existing scheduled notification requests or any notification rules." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @DeleteMapping("/template/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public void deleteNotificationTemplateById(@PathVariable UUID id) throws Exception { + NotificationTemplateId notificationTemplateId = new NotificationTemplateId(id); + NotificationTemplate notificationTemplate = checkEntityId(notificationTemplateId, notificationTemplateService::findNotificationTemplateById, Operation.DELETE); + doDeleteAndLog(EntityType.NOTIFICATION_TEMPLATE, notificationTemplate, notificationTemplateService::deleteNotificationTemplateById); + } + + @ApiOperation(value = "List Slack conversations (listSlackConversations)", + notes = "List available Slack conversations by type." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/slack/conversations") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public List listSlackConversations(@RequestParam SlackConversationType type, + @ApiParam(value = "Slack bot token. If absent - system Slack settings will be used") + @RequestParam(required = false) String token, + @AuthenticationPrincipal SecurityUser user) { + // generic permission + if (StringUtils.isEmpty(token)) { + NotificationSettings settings = notificationSettingsService.findNotificationSettings(user.getTenantId()); + SlackNotificationDeliveryMethodConfig slackConfig = (SlackNotificationDeliveryMethodConfig) + settings.getDeliveryMethodsConfigs().get(NotificationDeliveryMethod.SLACK); + if (slackConfig == null) { + throw new IllegalArgumentException("Slack is not configured"); + } + token = slackConfig.getBotToken(); + } + + return slackService.listConversations(user.getTenantId(), token, type); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java index f08f8f380a..f547a5f0ad 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,12 +54,8 @@ public class OAuth2ConfigTemplateController extends BaseController { @RequestMapping(method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(@RequestBody OAuth2ClientRegistrationTemplate clientRegistrationTemplate) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.WRITE); - return oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.WRITE); + return oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); } @ApiOperation(value = "Delete OAuth2 client registration template by id (deleteClientRegistrationTemplate)" + SYSTEM_AUTHORITY_PARAGRAPH, @@ -70,13 +66,9 @@ public class OAuth2ConfigTemplateController extends BaseController { public void deleteClientRegistrationTemplate(@ApiParam(value = "String representation of client registration template id to delete", example = "139b1f81-2f5d-11ec-9dbe-9b627e1a88f4") @PathVariable(CLIENT_REGISTRATION_TEMPLATE_ID) String strClientRegistrationTemplateId) throws ThingsboardException { checkParameter(CLIENT_REGISTRATION_TEMPLATE_ID, strClientRegistrationTemplateId); - try { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.DELETE); - OAuth2ClientRegistrationTemplateId clientRegistrationTemplateId = new OAuth2ClientRegistrationTemplateId(toUUID(strClientRegistrationTemplateId)); - oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.DELETE); + OAuth2ClientRegistrationTemplateId clientRegistrationTemplateId = new OAuth2ClientRegistrationTemplateId(toUUID(strClientRegistrationTemplateId)); + oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId); } @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, @@ -85,12 +77,8 @@ public class OAuth2ConfigTemplateController extends BaseController { @RequestMapping(method = RequestMethod.GET, produces = "application/json") @ResponseBody public List getClientRegistrationTemplates() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ); - return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates(); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ); + return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates(); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 5904fab60d..ef5739a481 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,25 +69,21 @@ public class OAuth2Controller extends BaseController { "If platform type is not one of allowable values - it will just be ignored", allowableValues = "WEB, ANDROID, IOS") @RequestParam(required = false) String platform) throws ThingsboardException { - try { - if (log.isDebugEnabled()) { - log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort()); - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String header = headerNames.nextElement(); - log.debug("Header: {} {}", header, request.getHeader(header)); - } + if (log.isDebugEnabled()) { + log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort()); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String header = headerNames.nextElement(); + log.debug("Header: {} {}", header, request.getHeader(header)); } - PlatformType platformType = null; - if (StringUtils.isNotEmpty(platform)) { - try { - platformType = PlatformType.valueOf(platform); - } catch (Exception e) {} - } - return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request), pkgName, platformType); - } catch (Exception e) { - throw handleException(e); } + PlatformType platformType = null; + if (StringUtils.isNotEmpty(platform)) { + try { + platformType = PlatformType.valueOf(platform); + } catch (Exception e) {} + } + return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request), pkgName, platformType); } @ApiOperation(value = "Get current OAuth2 settings (getCurrentOAuth2Info)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @@ -95,12 +91,8 @@ public class OAuth2Controller extends BaseController { @RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json") @ResponseBody public OAuth2Info getCurrentOAuth2Info() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); - return oAuth2Service.findOAuth2Info(); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); + return oAuth2Service.findOAuth2Info(); } @ApiOperation(value = "Save OAuth2 settings (saveOAuth2Info)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @@ -108,13 +100,9 @@ public class OAuth2Controller extends BaseController { @RequestMapping(value = "/oauth2/config", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) public OAuth2Info saveOAuth2Info(@RequestBody OAuth2Info oauth2Info) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE); - oAuth2Service.saveOAuth2Info(oauth2Info); - return oAuth2Service.findOAuth2Info(); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE); + oAuth2Service.saveOAuth2Info(oauth2Info); + return oAuth2Service.findOAuth2Info(); } @ApiOperation(value = "Get OAuth2 log in processing URL (getLoginProcessingUrl)", notes = "Returns the URL enclosed in " + @@ -125,12 +113,8 @@ public class OAuth2Controller extends BaseController { @RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET) @ResponseBody public String getLoginProcessingUrl() throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); - return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\""; - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); + return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\""; } } diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 0040187f02..9b923e01dc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,24 +87,20 @@ public class OtaPackageController extends BaseController { public ResponseEntity downloadOtaPackage(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); - try { - OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); - OtaPackage otaPackage = checkOtaPackageId(otaPackageId, Operation.READ); - - if (otaPackage.hasUrl()) { - return ResponseEntity.badRequest().build(); - } + OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); + OtaPackage otaPackage = checkOtaPackageId(otaPackageId, Operation.READ); - ByteArrayResource resource = new ByteArrayResource(otaPackage.getData().array()); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + otaPackage.getFileName()) - .header("x-filename", otaPackage.getFileName()) - .contentLength(resource.contentLength()) - .contentType(parseMediaType(otaPackage.getContentType())) - .body(resource); - } catch (Exception e) { - throw handleException(e); + if (otaPackage.hasUrl()) { + return ResponseEntity.badRequest().build(); } + + ByteArrayResource resource = new ByteArrayResource(otaPackage.getData().array()); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + otaPackage.getFileName()) + .header("x-filename", otaPackage.getFileName()) + .contentLength(resource.contentLength()) + .contentType(parseMediaType(otaPackage.getContentType())) + .body(resource); } @ApiOperation(value = "Get OTA Package Info (getOtaPackageInfoById)", @@ -117,12 +113,8 @@ public class OtaPackageController extends BaseController { public OtaPackageInfo getOtaPackageInfoById(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); - try { - OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); - return checkNotNull(otaPackageService.findOtaPackageInfoById(getTenantId(), otaPackageId)); - } catch (Exception e) { - throw handleException(e); - } + OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); + return checkNotNull(otaPackageService.findOtaPackageInfoById(getTenantId(), otaPackageId)); } @ApiOperation(value = "Get OTA Package (getOtaPackageById)", @@ -135,12 +127,8 @@ public class OtaPackageController extends BaseController { public OtaPackage getOtaPackageById(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); - try { - OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); - return checkOtaPackageId(otaPackageId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); + return checkOtaPackageId(otaPackageId, Operation.READ); } @ApiOperation(value = "Create Or Update OTA Package Info (saveOtaPackageInfo)", @@ -204,12 +192,8 @@ public class OtaPackageController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantId(getTenantId(), pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantId(getTenantId(), pageLink)); } @ApiOperation(value = "Get OTA Package Infos (getOtaPackages)", @@ -235,13 +219,9 @@ public class OtaPackageController extends BaseController { @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("deviceProfileId", strDeviceProfileId); checkParameter("type", strType); - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(getTenantId(), - new DeviceProfileId(toUUID(strDeviceProfileId)), OtaPackageType.valueOf(strType), pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(getTenantId(), + new DeviceProfileId(toUUID(strDeviceProfileId)), OtaPackageType.valueOf(strType), pageLink)); } @ApiOperation(value = "Delete OTA Package (deleteOtaPackage)", diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 3b37e6727f..0e79ae7955 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,7 +83,7 @@ public class QueueController extends BaseController { @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("serviceType", serviceType); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - ServiceType type = ServiceType.valueOf(serviceType); + ServiceType type = ServiceType.of(serviceType); switch (type) { case TB_RULE_ENGINE: return queueService.findQueuesByTenantId(getTenantId(), pageLink); @@ -136,7 +136,7 @@ public class QueueController extends BaseController { checkEntity(queue.getId(), queue, Resource.QUEUE); - ServiceType type = ServiceType.valueOf(serviceType); + ServiceType type = ServiceType.of(serviceType); switch (type) { case TB_RULE_ENGINE: queue.setTenantId(getTenantId()); diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java index 05cdf6170a..509e91e546 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index 300d522dbe..464dc528a5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,10 +44,10 @@ import org.thingsboard.server.common.data.rpc.Rpc; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.exception.ToErrorResponseEntity; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.rpc.RemoveRpcActorMsg; import org.thingsboard.server.service.security.permission.Operation; -import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity; import javax.annotation.Nullable; import java.util.UUID; @@ -159,12 +159,8 @@ public class RpcV2Controller extends AbstractRpcController { @ApiParam(value = RPC_ID_PARAM_DESCRIPTION, required = true) @PathVariable(RPC_ID) String strRpc) throws ThingsboardException { checkParameter("RpcId", strRpc); - try { - RpcId rpcId = new RpcId(UUID.fromString(strRpc)); - return checkRpcId(rpcId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + RpcId rpcId = new RpcId(UUID.fromString(strRpc)); + return checkRpcId(rpcId, Operation.READ); } @ApiOperation(value = "Get persistent RPC requests", notes = "Allows to query RPC calls for specific device using pagination." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @@ -187,43 +183,39 @@ public class RpcV2Controller extends AbstractRpcController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("DeviceId", strDeviceId); - try { - if (rpcStatus != null && rpcStatus.equals(RpcStatus.DELETED)) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "RpcStatus: DELETED"); - } - - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - DeviceId deviceId = new DeviceId(UUID.fromString(strDeviceId)); - final DeferredResult response = new DeferredResult<>(); + if (rpcStatus != null && rpcStatus.equals(RpcStatus.DELETED)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "RpcStatus: DELETED"); + } - accessValidator.validate(getCurrentUser(), Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable DeferredResult result) { - PageData rpcCalls; - if (rpcStatus != null) { - rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); - } else { - rpcCalls = rpcService.findAllByDeviceId(tenantId, deviceId, pageLink); - } - response.setResult(new ResponseEntity<>(rpcCalls, HttpStatus.OK)); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + DeviceId deviceId = new DeviceId(UUID.fromString(strDeviceId)); + final DeferredResult response = new DeferredResult<>(); + + accessValidator.validate(getCurrentUser(), Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable DeferredResult result) { + PageData rpcCalls; + if (rpcStatus != null) { + rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); + } else { + rpcCalls = rpcService.findAllByDeviceId(tenantId, deviceId, pageLink); } + response.setResult(new ResponseEntity<>(rpcCalls, HttpStatus.OK)); + } - @Override - public void onFailure(Throwable e) { - ResponseEntity entity; - if (e instanceof ToErrorResponseEntity) { - entity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); - } else { - entity = new ResponseEntity(HttpStatus.UNAUTHORIZED); - } - response.setResult(entity); + @Override + public void onFailure(Throwable e) { + ResponseEntity entity; + if (e instanceof ToErrorResponseEntity) { + entity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); + } else { + entity = new ResponseEntity(HttpStatus.UNAUTHORIZED); } - })); - return response; - } catch (Exception e) { - throw handleException(e); - } + response.setResult(entity); + } + })); + return response; } @ApiOperation(value = "Delete persistent RPC", notes = "Deletes the persistent RPC request." + TENANT_AUTHORITY_PARAGRAPH) @@ -234,25 +226,21 @@ public class RpcV2Controller extends AbstractRpcController { @ApiParam(value = RPC_ID_PARAM_DESCRIPTION, required = true) @PathVariable(RPC_ID) String strRpc) throws ThingsboardException { checkParameter("RpcId", strRpc); - try { - RpcId rpcId = new RpcId(UUID.fromString(strRpc)); - Rpc rpc = checkRpcId(rpcId, Operation.DELETE); - - if (rpc != null) { - if (rpc.getStatus().equals(RpcStatus.QUEUED)) { - RemoveRpcActorMsg removeMsg = new RemoveRpcActorMsg(getTenantId(), rpc.getDeviceId(), rpc.getUuidId()); - log.trace("[{}] Forwarding msg {} to queue actor!", rpc.getDeviceId(), rpc); - tbClusterService.pushMsgToCore(removeMsg, null); - } + RpcId rpcId = new RpcId(UUID.fromString(strRpc)); + Rpc rpc = checkRpcId(rpcId, Operation.DELETE); + + if (rpc != null) { + if (rpc.getStatus().equals(RpcStatus.QUEUED)) { + RemoveRpcActorMsg removeMsg = new RemoveRpcActorMsg(getTenantId(), rpc.getDeviceId(), rpc.getUuidId()); + log.trace("[{}] Forwarding msg {} to queue actor!", rpc.getDeviceId(), rpc); + tbClusterService.pushMsgToCore(removeMsg, null); + } - rpcService.deleteRpc(getTenantId(), rpcId); - rpc.setStatus(RpcStatus.DELETED); + rpcService.deleteRpc(getTenantId(), rpcId); + rpc.setStatus(RpcStatus.DELETED); - TbMsg msg = TbMsg.newMsg(RPC_DELETED, rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); - tbClusterService.pushMsgToRuleEngine(getTenantId(), rpc.getDeviceId(), msg, null); - } - } catch (Exception e) { - throw handleException(e); + TbMsg msg = TbMsg.newMsg(RPC_DELETED, rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); + tbClusterService.pushMsgToRuleEngine(getTenantId(), rpc.getDeviceId(), msg, null); } } } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index dc74f36f86..50abb3de1d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,9 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; @@ -36,6 +36,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.script.api.js.JsInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; @@ -114,7 +115,6 @@ public class RuleChainController extends BaseController { private static final int DEFAULT_PAGE_SIZE = 1000; - private static final ObjectMapper objectMapper = new ObjectMapper(); public static final int TIMEOUT = 20; private static final String RULE_CHAIN_DESCRIPTION = "The rule chain object is lightweight and contains general information about the rule chain. " + @@ -166,12 +166,8 @@ public class RuleChainController extends BaseController { @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); - try { - RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); - return checkRuleChain(ruleChainId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + return checkRuleChain(ruleChainId, Operation.READ); } @ApiOperation(value = "Get Rule Chain output labels (getRuleChainOutputLabels)", @@ -184,13 +180,9 @@ public class RuleChainController extends BaseController { @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); - try { - RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); - checkRuleChain(ruleChainId, Operation.READ); - return tbRuleChainService.getRuleChainOutputLabels(getTenantId(), ruleChainId); - } catch (Exception e) { - throw handleException(e); - } + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + checkRuleChain(ruleChainId, Operation.READ); + return tbRuleChainService.getRuleChainOutputLabels(getTenantId(), ruleChainId); } @ApiOperation(value = "Get output labels usage (getRuleChainOutputLabelsUsage)", @@ -203,13 +195,9 @@ public class RuleChainController extends BaseController { @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); - try { - RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); - checkRuleChain(ruleChainId, Operation.READ); - return tbRuleChainService.getOutputLabelUsage(getCurrentUser().getTenantId(), ruleChainId); - } catch (Exception e) { - throw handleException(e); - } + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + checkRuleChain(ruleChainId, Operation.READ); + return tbRuleChainService.getOutputLabelUsage(getCurrentUser().getTenantId(), ruleChainId); } @ApiOperation(value = "Get Rule Chain (getRuleChainById)", @@ -221,13 +209,9 @@ public class RuleChainController extends BaseController { @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); - try { - RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); - checkRuleChain(ruleChainId, Operation.READ); - return ruleChainService.loadRuleChainMetaData(getTenantId(), ruleChainId); - } catch (Exception e) { - throw handleException(e); - } + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + checkRuleChain(ruleChainId, Operation.READ); + return ruleChainService.loadRuleChainMetaData(getTenantId(), ruleChainId); } @ApiOperation(value = "Create Or Update Rule Chain (saveRuleChain)", @@ -319,17 +303,13 @@ public class RuleChainController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - RuleChainType type = RuleChainType.CORE; - if (typeStr != null && typeStr.trim().length() > 0) { - type = RuleChainType.valueOf(typeStr); - } - return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink)); - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + RuleChainType type = RuleChainType.CORE; + if (typeStr != null && typeStr.trim().length() > 0) { + type = RuleChainType.valueOf(typeStr); } + return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink)); } @ApiOperation(value = "Delete rule chain (deleteRuleChain)", @@ -357,25 +337,21 @@ public class RuleChainController extends BaseController { @ApiParam(value = RULE_NODE_ID_PARAM_DESCRIPTION) @PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException { checkParameter(RULE_NODE_ID, strRuleNodeId); - try { - RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId)); - checkRuleNode(ruleNodeId, Operation.READ); - TenantId tenantId = getCurrentUser().getTenantId(); - List events = eventService.findLatestEvents(tenantId, ruleNodeId, EventType.DEBUG_RULE_NODE, 2); - JsonNode result = null; - if (events != null) { - for (EventInfo event : events) { - JsonNode body = event.getBody(); - if (body.has("type") && body.get("type").asText().equals("IN")) { - result = body; - break; - } + RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId)); + checkRuleNode(ruleNodeId, Operation.READ); + TenantId tenantId = getCurrentUser().getTenantId(); + List events = eventService.findLatestEvents(tenantId, ruleNodeId, EventType.DEBUG_RULE_NODE, 2); + JsonNode result = null; + if (events != null) { + for (EventInfo event : events) { + JsonNode body = event.getBody(); + if (body.has("type") && body.get("type").asText().equals("IN")) { + result = body; + break; } } - return result; - } catch (Exception e) { - throw handleException(e); } + return result; } @ApiOperation(value = "Is TBEL script executor enabled", @@ -396,74 +372,70 @@ public class RuleChainController extends BaseController { @ApiParam(value = "Script language: JS or TBEL") @RequestParam(required = false) ScriptLanguage scriptLang, @ApiParam(value = "Test JS request. See API call description above.") - @RequestBody JsonNode inputParams) throws ThingsboardException { + @RequestBody JsonNode inputParams) throws ThingsboardException, JsonProcessingException { + String script = inputParams.get("script").asText(); + String scriptType = inputParams.get("scriptType").asText(); + JsonNode argNamesJson = inputParams.get("argNames"); + String[] argNames = JacksonUtil.treeToValue(argNamesJson, String[].class); + + String data = inputParams.get("msg").asText(); + JsonNode metadataJson = inputParams.get("metadata"); + Map metadata = JacksonUtil.convertValue(metadataJson, new TypeReference>() { + }); + String msgType = inputParams.get("msgType").asText(); + String output = ""; + String errorText = ""; + ScriptEngine engine = null; try { - String script = inputParams.get("script").asText(); - String scriptType = inputParams.get("scriptType").asText(); - JsonNode argNamesJson = inputParams.get("argNames"); - String[] argNames = objectMapper.treeToValue(argNamesJson, String[].class); - - String data = inputParams.get("msg").asText(); - JsonNode metadataJson = inputParams.get("metadata"); - Map metadata = objectMapper.convertValue(metadataJson, new TypeReference>() { - }); - String msgType = inputParams.get("msgType").asText(); - String output = ""; - String errorText = ""; - ScriptEngine engine = null; - try { - if (scriptLang == null) { - scriptLang = ScriptLanguage.JS; - } - if (ScriptLanguage.JS.equals(scriptLang)) { - engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, script, argNames); - } else { - if (tbelInvokeService == null) { - throw new IllegalArgumentException("TBEL script engine is disabled!"); - } - engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames); - } - TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); - switch (scriptType) { - case "update": - output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); - break; - case "generate": - output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); - break; - case "filter": - boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - output = Boolean.toString(result); - break; - case "switch": - Set states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - output = objectMapper.writeValueAsString(states); - break; - case "json": - JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - output = objectMapper.writeValueAsString(json); - break; - case "string": - output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - break; - default: - throw new IllegalArgumentException("Unsupported script type: " + scriptType); - } - } catch (Exception e) { - log.error("Error evaluating JS function", e); - errorText = e.getMessage(); - } finally { - if (engine != null) { - engine.destroy(); + if (scriptLang == null) { + scriptLang = ScriptLanguage.JS; + } + if (ScriptLanguage.JS.equals(scriptLang)) { + engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, script, argNames); + } else { + if (tbelInvokeService == null) { + throw new IllegalArgumentException("TBEL script engine is disabled!"); } + engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames); + } + TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); + switch (scriptType) { + case "update": + output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + break; + case "generate": + output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + break; + case "filter": + boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); + output = Boolean.toString(result); + break; + case "switch": + Set states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); + output = JacksonUtil.toString(states); + break; + case "json": + JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); + output = JacksonUtil.toString(json); + break; + case "string": + output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); + break; + default: + throw new IllegalArgumentException("Unsupported script type: " + scriptType); } - ObjectNode result = objectMapper.createObjectNode(); - result.put("output", output); - result.put("error", errorText); - return result; } catch (Exception e) { - throw handleException(e); + log.error("Error evaluating JS function", e); + errorText = e.getMessage(); + } finally { + if (engine != null) { + engine.destroy(); + } } + ObjectNode result = JacksonUtil.newObjectNode(); + result.put("output", output); + result.put("error", errorText); + return result; } @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @@ -473,13 +445,9 @@ public class RuleChainController extends BaseController { public RuleChainData exportRuleChains( @ApiParam(value = "A limit of rule chains to export.", required = true) @RequestParam("limit") int limit) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - PageLink pageLink = new PageLink(limit); - return checkNotNull(ruleChainService.exportTenantRuleChains(tenantId, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = new PageLink(limit); + return checkNotNull(ruleChainService.exportTenantRuleChains(tenantId, pageLink)); } @ApiOperation(value = "Import Rule Chains", notes = "Imports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @@ -491,30 +459,26 @@ public class RuleChainController extends BaseController { @RequestBody RuleChainData ruleChainData, @ApiParam(value = "Enables overwrite for existing rule chains with the same name.") @RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite); - for (RuleChainImportResult importResult : importResults) { - if (importResult.getError() == null) { - tbClusterService.broadcastEntityStateChangeEvent(importResult.getTenantId(), importResult.getRuleChainId(), - importResult.isUpdated() ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED); - } + TenantId tenantId = getCurrentUser().getTenantId(); + List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite); + for (RuleChainImportResult importResult : importResults) { + if (importResult.getError() == null) { + tbClusterService.broadcastEntityStateChangeEvent(importResult.getTenantId(), importResult.getRuleChainId(), + importResult.isUpdated() ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED); } - return importResults; - } catch (Exception e) { - throw handleException(e); } + return importResults; } private String msgToOutput(TbMsg msg) throws Exception { JsonNode resultNode = convertMsgToOut(msg); - return objectMapper.writeValueAsString(resultNode); + return JacksonUtil.toString(resultNode); } private String msgToOutput(List msgs) throws Exception { JsonNode resultNode; if (msgs.size() > 1) { - resultNode = objectMapper.createArrayNode(); + resultNode = JacksonUtil.newArrayNode(); for (TbMsg msg : msgs) { JsonNode convertedData = convertMsgToOut(msg); ((ArrayNode) resultNode).add(convertedData); @@ -522,16 +486,16 @@ public class RuleChainController extends BaseController { } else { resultNode = convertMsgToOut(msgs.get(0)); } - return objectMapper.writeValueAsString(resultNode); + return JacksonUtil.toString(resultNode); } private JsonNode convertMsgToOut(TbMsg msg) throws Exception { - ObjectNode msgData = objectMapper.createObjectNode(); + ObjectNode msgData = JacksonUtil.newObjectNode(); if (!StringUtils.isEmpty(msg.getData())) { - msgData.set("msg", objectMapper.readTree(msg.getData())); + msgData.set("msg", JacksonUtil.toJsonNode(msg.getData())); } Map metadata = msg.getMetaData().getData(); - msgData.set("metadata", objectMapper.valueToTree(metadata)); + msgData.set("metadata", JacksonUtil.valueToTree(metadata)); msgData.put("msgType", msg.getType()); return msgData; } @@ -601,15 +565,11 @@ public class RuleChainController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - TenantId tenantId = getCurrentUser().getTenantId(); - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - checkEdgeId(edgeId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); } @ApiOperation(value = "Set Edge Template Root Rule Chain (setEdgeTemplateRootRuleChain)", @@ -661,17 +621,13 @@ public class RuleChainController extends BaseController { @RequestMapping(value = "/ruleChain/autoAssignToEdgeRuleChains", method = RequestMethod.GET) @ResponseBody public List getAutoAssignToEdgeRuleChains() throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - List result = new ArrayList<>(); - PageDataIterableByTenant autoAssignRuleChainsIterator = - new PageDataIterableByTenant<>(ruleChainService::findAutoAssignToEdgeRuleChainsByTenantId, tenantId, DEFAULT_PAGE_SIZE); - for (RuleChain ruleChain : autoAssignRuleChainsIterator) { - result.add(ruleChain); - } - return checkNotNull(result); - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = getCurrentUser().getTenantId(); + List result = new ArrayList<>(); + PageDataIterableByTenant autoAssignRuleChainsIterator = + new PageDataIterableByTenant<>(ruleChainService::findAutoAssignToEdgeRuleChainsByTenantId, tenantId, DEFAULT_PAGE_SIZE); + for (RuleChain ruleChain : autoAssignRuleChainsIterator) { + result.add(ruleChain); } + return checkNotNull(result); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 8cb7bce6ab..3633cbbdeb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,33 +16,61 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.info.BuildProperties; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.SystemParams; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; +import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import springfox.documentation.annotations.ApiIgnore; import javax.annotation.PostConstruct; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; @ApiIgnore @RestController @TbCoreComponent @RequestMapping("/api") @Slf4j -public class SystemInfoController { +public class SystemInfoController extends BaseController { + + @Value("${security.user_token_access_enabled}") + private boolean userTokenAccessEnabled; + + @Value("${tbel.enabled:true}") + private boolean tbelEnabled; + + @Value("${state.persistToTelemetry:false}") + private boolean persistToTelemetry; + + @Value("${ui.dashboard.max_datapoints_limit}") + private long maxDatapointsLimit; @Autowired(required = false) private BuildProperties buildProperties; + @Autowired + private EntitiesVersionControlService versionControlService; + @PostConstruct public void init() { JsonNode info = buildInfoObject(); @@ -56,9 +84,64 @@ public class SystemInfoController { return buildInfoObject(); } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/system/params", method = RequestMethod.GET) + @ResponseBody + public SystemParams getSystemParams() throws ThingsboardException { + SystemParams systemParams = new SystemParams(); + SecurityUser currentUser = getCurrentUser(); + TenantId tenantId = currentUser.getTenantId(); + CustomerId customerId = currentUser.getCustomerId(); + if (currentUser.isSystemAdmin() || currentUser.isTenantAdmin()) { + systemParams.setUserTokenAccessEnabled(userTokenAccessEnabled); + } else { + systemParams.setUserTokenAccessEnabled(false); + } + boolean forceFullscreen = isForceFullscreen(currentUser); + if (forceFullscreen && (currentUser.isTenantAdmin() || currentUser.isCustomerUser())) { + PageLink pageLink = new PageLink(100); + List dashboards; + if (currentUser.isTenantAdmin()) { + dashboards = dashboardService.findDashboardsByTenantId(tenantId, pageLink).getData(); + } else { + dashboards = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).getData(); + } + systemParams.setAllowedDashboardIds(dashboards.stream().map(d -> d.getId().getId().toString()).collect(Collectors.toList())); + } else { + systemParams.setAllowedDashboardIds(Collections.emptyList()); + } + systemParams.setEdgesSupportEnabled(edgesEnabled); + if (currentUser.isTenantAdmin()) { + systemParams.setHasRepository(versionControlService.getVersionControlSettings(tenantId) != null); + systemParams.setTbelEnabled(tbelEnabled); + } else { + systemParams.setHasRepository(false); + systemParams.setTbelEnabled(false); + } + if (currentUser.isTenantAdmin() || currentUser.isCustomerUser()) { + systemParams.setPersistDeviceStateToTelemetry(persistToTelemetry); + } else { + systemParams.setPersistDeviceStateToTelemetry(false); + } + UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL); + ObjectNode userSettingsNode = userSettings == null ? JacksonUtil.newObjectNode() : (ObjectNode) userSettings.getSettings(); + if (!userSettingsNode.has("openedMenuSections")) { + userSettingsNode.set("openedMenuSections", JacksonUtil.newArrayNode()); + } + systemParams.setUserSettings(userSettingsNode); + systemParams.setMaxDatapointsLimit(maxDatapointsLimit); + return systemParams; + } + + private boolean isForceFullscreen(SecurityUser currentUser) { + return UserPrincipal.Type.PUBLIC_ID.equals(currentUser.getUserPrincipal().getType()) || + (currentUser.getAdditionalInfo() != null && + currentUser.getAdditionalInfo().has("defaultDashboardFullscreen") && + currentUser.getAdditionalInfo().get("defaultDashboardFullscreen").booleanValue()); + } + private JsonNode buildInfoObject() { - ObjectMapper objectMapper = new ObjectMapper(); - ObjectNode infoObject = objectMapper.createObjectNode(); + ObjectNode infoObject = JacksonUtil.newObjectNode(); if (buildProperties != null) { infoObject.put("version", buildProperties.getVersion()); infoObject.put("artifact", buildProperties.getArtifact()); diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index ec0b530117..d94dc31fa1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,20 +82,16 @@ public class TbResourceController extends BaseController { public ResponseEntity downloadResource(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); - try { - TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); - TbResource tbResource = checkResourceId(resourceId, Operation.READ); + TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); + TbResource tbResource = checkResourceId(resourceId, Operation.READ); - ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes())); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName()) - .header("x-filename", tbResource.getFileName()) - .contentLength(resource.contentLength()) - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(resource); - } catch (Exception e) { - throw handleException(e); - } + ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes())); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName()) + .header("x-filename", tbResource.getFileName()) + .contentLength(resource.contentLength()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(resource); } @ApiOperation(value = "Get Resource Info (getResourceInfoById)", @@ -108,12 +104,8 @@ public class TbResourceController extends BaseController { public TbResourceInfo getResourceInfoById(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); - try { - TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); - return checkResourceInfoId(resourceId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); + return checkResourceInfoId(resourceId, Operation.READ); } @ApiOperation(value = "Get Resource (getResourceById)", @@ -126,12 +118,8 @@ public class TbResourceController extends BaseController { public TbResource getResourceById(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); - try { - TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); - return checkResourceId(resourceId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); + return checkResourceId(resourceId, Operation.READ); } @ApiOperation(value = "Create Or Update Resource (saveResource)", @@ -171,15 +159,11 @@ public class TbResourceController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { - return checkNotNull(resourceService.findTenantResourcesByTenantId(getTenantId(), pageLink)); - } else { - return checkNotNull(resourceService.findAllTenantResourcesByTenantId(getTenantId(), pageLink)); - } - } catch (Exception e) { - throw handleException(e); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { + return checkNotNull(resourceService.findTenantResourcesByTenantId(getTenantId(), pageLink)); + } else { + return checkNotNull(resourceService.findAllTenantResourcesByTenantId(getTenantId(), pageLink)); } } @@ -200,12 +184,8 @@ public class TbResourceController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = new PageLink(pageSize, page, textSearch); - return checkNotNull(resourceService.findLwM2mObjectPage(getTenantId(), sortProperty, sortOrder, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = new PageLink(pageSize, page, textSearch); + return checkNotNull(resourceService.findLwM2mObjectPage(getTenantId(), sortProperty, sortOrder, pageLink)); } @ApiOperation(value = "Get LwM2M Objects (getLwm2mListObjects)", @@ -221,11 +201,7 @@ public class TbResourceController extends BaseController { @RequestParam String sortProperty, @ApiParam(value = "LwM2M Object ids.", required = true) @RequestParam(required = false) String[] objectIds) throws ThingsboardException { - try { - return checkNotNull(resourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds)); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(resourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds)); } @ApiOperation(value = "Delete Resource (deleteResource)", diff --git a/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java b/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java index afc6f38402..766b325b0c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index 6f6c4dda85..05f56e93bd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,6 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.Aggregation; -import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; @@ -76,27 +75,24 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.transport.adaptor.JsonConverter; -import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.exception.InvalidParametersException; +import org.thingsboard.server.exception.UncheckedApiException; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.telemetry.AttributeData; import org.thingsboard.server.service.telemetry.TsData; -import org.thingsboard.server.service.telemetry.exception.InvalidParametersException; -import org.thingsboard.server.service.telemetry.exception.UncheckedApiException; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -104,13 +100,15 @@ import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_JSON_REQUEST_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_KEYS_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SAVE_SCOPE_ALLOWED_VALUES; import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_ALLOWED_VALUES; import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTE_DATA_EXAMPLE; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ATTRIBUTE_SCOPES; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_GET_ATTRIBUTE_SCOPES; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_SAVE_ATTRIBUTE_SCOPES; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.INVALID_STRUCTURE_OF_THE_REQUEST; @@ -185,11 +183,7 @@ public class TelemetryController extends BaseController { public DeferredResult getAttributeKeys( @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException { - try { - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, this::getAttributeKeysCallback); - } catch (Exception e) { - throw handleException(e); - } + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, this::getAttributeKeysCallback); } @ApiOperation(value = "Get all attribute keys by scope (getAttributeKeysByScope)", @@ -206,12 +200,8 @@ public class TelemetryController extends BaseController { @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope) throws ThingsboardException { - try { - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, (result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope)); - } catch (Exception e) { - throw handleException(e); - } } @ApiOperation(value = "Get attributes (getAttributes)", @@ -229,19 +219,15 @@ public class TelemetryController extends BaseController { @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { - try { SecurityUser user = getCurrentUser(); - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); - } catch (Exception e) { - throw handleException(e); - } } @ApiOperation(value = "Get attributes by scope (getAttributesByScope)", notes = "Returns all attributes of a specified scope that belong to specified entity." + - ENTITY_ATTRIBUTE_SCOPES + + ENTITY_GET_ATTRIBUTE_SCOPES + "Use optional 'keys' parameter to return specific attributes." + "\n Example of the result: \n\n" + MARKDOWN_CODE_BLOCK_START @@ -257,13 +243,9 @@ public class TelemetryController extends BaseController { @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { - try { - SecurityUser user = getCurrentUser(); - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, + SecurityUser user = getCurrentUser(); + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); - } catch (Exception e) { - throw handleException(e); - } } @ApiOperation(value = "Get time-series keys (getTimeseriesKeys)", @@ -276,12 +258,8 @@ public class TelemetryController extends BaseController { public DeferredResult getTimeseriesKeys( @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException { - try { - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, (result, tenantId, entityId) -> Futures.addCallback(tsService.findAllLatest(tenantId, entityId), getTsKeysToResponseCallback(result), MoreExecutors.directExecutor())); - } catch (Exception e) { - throw handleException(e); - } } @ApiOperation(value = "Get latest time-series value (getLatestTimeseries)", @@ -306,13 +284,9 @@ public class TelemetryController extends BaseController { @ApiParam(value = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @ApiParam(value = STRICT_DATA_TYPES_DESCRIPTION) @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { - try { - SecurityUser user = getCurrentUser(); - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, - (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes)); - } catch (Exception e) { - throw handleException(e); - } + SecurityUser user = getCurrentUser(); + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, + (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes)); } @ApiOperation(value = "Get time-series data (getTimeseries)", @@ -349,19 +323,15 @@ public class TelemetryController extends BaseController { @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, @ApiParam(value = STRICT_DATA_TYPES_DESCRIPTION) @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { - try { - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, - (result, tenantId, entityId) -> { - // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted - Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr); - List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg, orderBy)) - .collect(Collectors.toList()); - - Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); - }); - } catch (Exception e) { - throw handleException(e); - } + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, + (result, tenantId, entityId) -> { + // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted + Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr); + List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg, orderBy)) + .collect(Collectors.toList()); + + Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); + }); } @ApiOperation(value = "Save device attributes (saveDeviceAttributes)", @@ -383,19 +353,15 @@ public class TelemetryController extends BaseController { @ResponseBody public DeferredResult saveDeviceAttributes( @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr, - @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, + @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SAVE_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, @ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { - try { - EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); - return saveAttributes(getTenantId(), entityId, scope, request); - } catch (Exception e) { - throw handleException(e); - } + EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); + return saveAttributes(getTenantId(), entityId, scope, request); } @ApiOperation(value = "Save entity attributes (saveEntityAttributesV1)", notes = "Creates or updates the entity attributes based on Entity Id and the specified attribute scope. " + - ENTITY_ATTRIBUTE_SCOPES + + ENTITY_SAVE_ATTRIBUTE_SCOPES + SAVE_ATTRIBUTES_REQUEST_PAYLOAD + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @@ -411,19 +377,15 @@ public class TelemetryController extends BaseController { public DeferredResult saveEntityAttributesV1( @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope, + @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SAVE_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope, @ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { - try { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return saveAttributes(getTenantId(), entityId, scope, request); - } catch (Exception e) { - throw handleException(e); - } + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return saveAttributes(getTenantId(), entityId, scope, request); } @ApiOperation(value = "Save entity attributes (saveEntityAttributesV2)", notes = "Creates or updates the entity attributes based on Entity Id and the specified attribute scope. " + - ENTITY_ATTRIBUTE_SCOPES + + ENTITY_SAVE_ATTRIBUTE_SCOPES + SAVE_ATTRIBUTES_REQUEST_PAYLOAD + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @@ -439,14 +401,10 @@ public class TelemetryController extends BaseController { public DeferredResult saveEntityAttributesV2( @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, + @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SAVE_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, @ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { - try { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return saveAttributes(getTenantId(), entityId, scope, request); - } catch (Exception e) { - throw handleException(e); - } + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return saveAttributes(getTenantId(), entityId, scope, request); } @@ -470,12 +428,8 @@ public class TelemetryController extends BaseController { @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @ApiParam(value = TELEMETRY_SCOPE_DESCRIPTION, required = true, allowableValues = "ANY") @PathVariable("scope") String scope, @ApiParam(value = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { - try { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return saveTelemetry(getTenantId(), entityId, requestBody, 0L); - } catch (Exception e) { - throw handleException(e); - } + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return saveTelemetry(getTenantId(), entityId, requestBody, 0L); } @ApiOperation(value = "Save or update time-series data with TTL (saveEntityTelemetryWithTTL)", @@ -500,12 +454,8 @@ public class TelemetryController extends BaseController { @ApiParam(value = TELEMETRY_SCOPE_DESCRIPTION, required = true, allowableValues = "ANY") @PathVariable("scope") String scope, @ApiParam(value = "A long value representing TTL (Time to Live) parameter.", required = true) @PathVariable("ttl") Long ttl, @ApiParam(value = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { - try { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return saveTelemetry(getTenantId(), entityId, requestBody, ttl); - } catch (Exception e) { - throw handleException(e); - } + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return saveTelemetry(getTenantId(), entityId, requestBody, ttl); } @ApiOperation(value = "Delete entity time-series data (deleteEntityTimeseries)", @@ -538,12 +488,8 @@ public class TelemetryController extends BaseController { @RequestParam(name = "endTs", required = false) Long endTs, @ApiParam(value = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.") @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException { - try { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted); - } catch (Exception e) { - throw handleException(e); - } + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted); } private DeferredResult deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys, @@ -608,12 +554,8 @@ public class TelemetryController extends BaseController { @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { - try { - EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); - return deleteAttributes(entityId, scope, keysStr); - } catch (Exception e) { - throw handleException(e); - } + EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); + return deleteAttributes(entityId, scope, keysStr); } @ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)", @@ -636,12 +578,8 @@ public class TelemetryController extends BaseController { @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope, @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { - try { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteAttributes(entityId, scope, keysStr); - } catch (Exception e) { - throw handleException(e); - } + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return deleteAttributes(entityId, scope, keysStr); } private DeferredResult deleteAttributes(EntityId entityIdSrc, String scope, String keysStr) throws ThingsboardException { @@ -685,7 +623,6 @@ public class TelemetryController extends BaseController { } if (json.isObject()) { List attributes = extractRequestAttributes(json); - attributes.forEach(ConstraintValidator::validateFields); if (attributes.isEmpty()) { return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index b44c5327cd..6574969e15 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,16 +79,12 @@ public class TenantController extends BaseController { @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION) @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { checkParameter(TENANT_ID, strTenantId); - try { - TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); - Tenant tenant = checkTenantId(tenantId, Operation.READ); - if (!tenant.getAdditionalInfo().isNull()) { - processDashboardIdFromAdditionalInfo((ObjectNode) tenant.getAdditionalInfo(), HOME_DASHBOARD); - } - return tenant; - } catch (Exception e) { - throw handleException(e); + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); + Tenant tenant = checkTenantId(tenantId, Operation.READ); + if (!tenant.getAdditionalInfo().isNull()) { + processDashboardIdFromAdditionalInfo((ObjectNode) tenant.getAdditionalInfo(), HOME_DASHBOARD); } + return tenant; } @ApiOperation(value = "Get Tenant Info (getTenantInfoById)", @@ -101,12 +97,8 @@ public class TenantController extends BaseController { @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION) @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { checkParameter(TENANT_ID, strTenantId); - try { - TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); - return checkTenantInfoId(tenantId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); + return checkTenantInfoId(tenantId, Operation.READ); } @ApiOperation(value = "Create Or update Tenant (saveTenant)", @@ -154,12 +146,8 @@ public class TenantController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(tenantService.findTenants(pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantService.findTenants(pageLink)); } @ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. " @@ -179,12 +167,8 @@ public class TenantController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder ) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(tenantService.findTenantInfos(pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantService.findTenantInfos(pageLink)); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index e5a33da948..a7fe973ec0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -40,6 +42,9 @@ import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileServ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.List; +import java.util.UUID; + import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; @@ -75,12 +80,8 @@ public class TenantProfileController extends BaseController { @ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { checkParameter("tenantProfileId", strTenantProfileId); - try { - TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); - return checkTenantProfileId(tenantProfileId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + return checkTenantProfileId(tenantProfileId, Operation.READ); } @ApiOperation(value = "Get Tenant Profile Info (getTenantProfileInfoById)", @@ -92,12 +93,8 @@ public class TenantProfileController extends BaseController { @ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { checkParameter("tenantProfileId", strTenantProfileId); - try { - TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); - return checkNotNull(tenantProfileService.findTenantProfileInfoById(getTenantId(), tenantProfileId)); - } catch (Exception e) { - throw handleException(e); - } + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + return checkNotNull(tenantProfileService.findTenantProfileInfoById(getTenantId(), tenantProfileId)); } @ApiOperation(value = "Get default Tenant Profile Info (getDefaultTenantProfileInfo)", @@ -106,11 +103,7 @@ public class TenantProfileController extends BaseController { @RequestMapping(value = "/tenantProfileInfo/default", method = RequestMethod.GET) @ResponseBody public EntityInfo getDefaultTenantProfileInfo() throws ThingsboardException { - try { - return checkNotNull(tenantProfileService.findDefaultTenantProfileInfo(getTenantId())); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(tenantProfileService.findDefaultTenantProfileInfo(getTenantId())); } @ApiOperation(value = "Create Or update Tenant Profile (saveTenantProfile)", @@ -171,19 +164,15 @@ public class TenantProfileController extends BaseController { @ResponseBody public TenantProfile saveTenantProfile(@ApiParam(value = "A JSON value representing the tenant profile.") @RequestBody TenantProfile tenantProfile) throws ThingsboardException { - try { - TenantProfile oldProfile; - if (tenantProfile.getId() == null) { - accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE); - oldProfile = null; - } else { - oldProfile = checkTenantProfileId(tenantProfile.getId(), Operation.WRITE); - } - - return tbTenantProfileService.save(getTenantId(), tenantProfile, oldProfile); - } catch (Exception e) { - throw handleException(e); + TenantProfile oldProfile; + if (tenantProfile.getId() == null) { + accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE); + oldProfile = null; + } else { + oldProfile = checkTenantProfileId(tenantProfile.getId(), Operation.WRITE); } + + return tbTenantProfileService.save(getTenantId(), tenantProfile, oldProfile); } @ApiOperation(value = "Delete Tenant Profile (deleteTenantProfile)", @@ -193,14 +182,10 @@ public class TenantProfileController extends BaseController { @ResponseStatus(value = HttpStatus.OK) public void deleteTenantProfile(@ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { - try { - checkParameter("tenantProfileId", strTenantProfileId); - TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); - TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE); - tbTenantProfileService.delete(getTenantId(), profile); - } catch (Exception e) { - throw handleException(e); - } + checkParameter("tenantProfileId", strTenantProfileId); + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE); + tbTenantProfileService.delete(getTenantId(), profile); } @ApiOperation(value = "Make tenant profile default (setDefaultTenantProfile)", @@ -212,14 +197,10 @@ public class TenantProfileController extends BaseController { @ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { checkParameter("tenantProfileId", strTenantProfileId); - try { - TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); - TenantProfile tenantProfile = checkTenantProfileId(tenantProfileId, Operation.WRITE); - tenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfileId); - return tenantProfile; - } catch (Exception e) { - throw handleException(e); - } + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + TenantProfile tenantProfile = checkTenantProfileId(tenantProfileId, Operation.WRITE); + tenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfileId); + return tenantProfile; } @ApiOperation(value = "Get Tenant Profiles (getTenantProfiles)", notes = "Returns a page of tenant profiles registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @@ -237,12 +218,8 @@ public class TenantProfileController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(tenantProfileService.findTenantProfiles(getTenantId(), pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantProfileService.findTenantProfiles(getTenantId(), pageLink)); } @ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. " @@ -261,11 +238,15 @@ public class TenantProfileController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink)); - } catch (Exception e) { - throw handleException(e); - } + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink)); + } + + @GetMapping(value = "/tenantProfiles", params = {"ids"}) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + public List getTenantProfilesByIds(@RequestParam("ids") UUID[] ids) { + return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids); } + + } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 5aad5c9503..b07b1eb70b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 5ce46e324e..4a5bcb3d81 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.EmailTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig; @@ -39,7 +40,6 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; -import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.system.SystemSecurityService; diff --git a/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java index 321ff8da5c..a40d3b85af 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java b/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java new file mode 100644 index 0000000000..a7d4250b65 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.UsageInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.dao.usage.UsageInfoService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@Slf4j +public class UsageInfoController extends BaseController { + + @Autowired + private UsageInfoService usageInfoService; + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/usage", method = RequestMethod.GET) + @ResponseBody + public UsageInfo getTenantUsageInfo() throws ThingsboardException { + return checkNotNull(usageInfoService.getUsageInfo(getCurrentUser().getTenantId())); + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 86e4456638..d49b2b04df 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,20 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; -import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -32,21 +36,38 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserEmailInfo; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.common.data.settings.UserDashboardAction; +import org.thingsboard.server.common.data.settings.UserDashboardsInfo; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsType; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; -import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.service.query.EntityQueryService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -55,9 +76,17 @@ import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; @@ -84,11 +113,11 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class UserController extends BaseController { public static final String USER_ID = "userId"; + public static final String PATHS = "paths"; public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s"; @Value("${security.user_token_access_enabled}") - @Getter private boolean userTokenAccessEnabled; private final MailService mailService; @@ -97,6 +126,12 @@ public class UserController extends BaseController { private final ApplicationEventPublisher eventPublisher; private final TbUserService tbUserService; + @Autowired + private EntityQueryService entityQueryService; + + @Autowired + private EntityService entityService; + @ApiOperation(value = "Get User (getUserById)", notes = "Fetch the User object based on the provided User Id. " + "If the user has the authority of 'SYS_ADMIN', the server does not perform additional checks. " + @@ -109,22 +144,18 @@ public class UserController extends BaseController { @ApiParam(value = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId) throws ThingsboardException { checkParameter(USER_ID, strUserId); - try { - UserId userId = new UserId(toUUID(strUserId)); - User user = checkUserId(userId, Operation.READ); - if (user.getAdditionalInfo().isObject()) { - ObjectNode additionalInfo = (ObjectNode) user.getAdditionalInfo(); - processDashboardIdFromAdditionalInfo(additionalInfo, DEFAULT_DASHBOARD); - processDashboardIdFromAdditionalInfo(additionalInfo, HOME_DASHBOARD); - UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); - if (userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) { - additionalInfo.put("userCredentialsEnabled", true); - } + UserId userId = new UserId(toUUID(strUserId)); + User user = checkUserId(userId, Operation.READ); + if (user.getAdditionalInfo().isObject()) { + ObjectNode additionalInfo = (ObjectNode) user.getAdditionalInfo(); + processDashboardIdFromAdditionalInfo(additionalInfo, DEFAULT_DASHBOARD); + processDashboardIdFromAdditionalInfo(additionalInfo, HOME_DASHBOARD); + UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); + if (userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) { + additionalInfo.put("userCredentialsEnabled", true); } - return user; - } catch (Exception e) { - throw handleException(e); } + return user; } @ApiOperation(value = "Check Token Access Enabled (isUserTokenAccessEnabled)", @@ -149,21 +180,17 @@ public class UserController extends BaseController { @ApiParam(value = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId) throws ThingsboardException { checkParameter(USER_ID, strUserId); - try { - if (!userTokenAccessEnabled) { - throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION, - ThingsboardErrorCode.PERMISSION_DENIED); - } - UserId userId = new UserId(toUUID(strUserId)); - SecurityUser authUser = getCurrentUser(); - User user = checkUserId(userId, Operation.READ); - UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); - UserCredentials credentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), userId); - SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); - return tokenFactory.createTokenPair(securityUser); - } catch (Exception e) { - throw handleException(e); + if (!userTokenAccessEnabled) { + throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION, + ThingsboardErrorCode.PERMISSION_DENIED); } + UserId userId = new UserId(toUUID(strUserId)); + SecurityUser authUser = getCurrentUser(); + User user = checkUserId(userId, Operation.READ); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + UserCredentials credentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), userId); + SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); + return tokenFactory.createTokenPair(securityUser); } @ApiOperation(value = "Save Or update User (saveUser)", @@ -198,23 +225,19 @@ public class UserController extends BaseController { @ApiParam(value = "Email of the user", required = true) @RequestParam(value = "email") String email, HttpServletRequest request) throws ThingsboardException { - try { - User user = checkNotNull(userService.findUserByEmail(getCurrentUser().getTenantId(), email)); + User user = checkNotNull(userService.findUserByEmail(getCurrentUser().getTenantId(), email)); - accessControlService.checkPermission(getCurrentUser(), Resource.USER, Operation.READ, - user.getId(), user); + accessControlService.checkPermission(getCurrentUser(), Resource.USER, Operation.READ, + user.getId(), user); - UserCredentials userCredentials = userService.findUserCredentialsByUserId(getCurrentUser().getTenantId(), user.getId()); - if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) { - String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request); - String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl, - userCredentials.getActivateToken()); - mailService.sendActivationEmail(activateUrl, email); - } else { - throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - } catch (Exception e) { - throw handleException(e); + UserCredentials userCredentials = userService.findUserCredentialsByUserId(getCurrentUser().getTenantId(), user.getId()); + if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) { + String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request); + String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl, + userCredentials.getActivateToken()); + mailService.sendActivationEmail(activateUrl, email); + } else { + throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } } @@ -229,21 +252,17 @@ public class UserController extends BaseController { @PathVariable(USER_ID) String strUserId, HttpServletRequest request) throws ThingsboardException { checkParameter(USER_ID, strUserId); - try { - UserId userId = new UserId(toUUID(strUserId)); - User user = checkUserId(userId, Operation.READ); - SecurityUser authUser = getCurrentUser(); - UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), user.getId()); - if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) { - String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request); - String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl, - userCredentials.getActivateToken()); - return activateUrl; - } else { - throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - } catch (Exception e) { - throw handleException(e); + UserId userId = new UserId(toUUID(strUserId)); + User user = checkUserId(userId, Operation.READ); + SecurityUser authUser = getCurrentUser(); + UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), user.getId()); + if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) { + String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request); + String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl, + userCredentials.getActivateToken()); + return activateUrl; + } else { + throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } } @@ -282,19 +301,53 @@ public class UserController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - SecurityUser currentUser = getCurrentUser(); - if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) { - return checkNotNull(userService.findUsersByTenantId(currentUser.getTenantId(), pageLink)); - } else { - return checkNotNull(userService.findCustomerUsers(currentUser.getTenantId(), currentUser.getCustomerId(), pageLink)); - } - } catch (Exception e) { - throw handleException(e); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + SecurityUser currentUser = getCurrentUser(); + if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) { + return checkNotNull(userService.findUsersByTenantId(currentUser.getTenantId(), pageLink)); + } else { + return checkNotNull(userService.findCustomerUsers(currentUser.getTenantId(), currentUser.getCustomerId(), pageLink)); } } + @ApiOperation(value = "Find users by query (findUsersByQuery)", + notes = "Returns page of user data objects. Search is been executed by email, firstName and " + + "lastName fields. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/users/info", method = RequestMethod.GET) + @ResponseBody + public PageData findUsersByQuery( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = USER_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = USER_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + SecurityUser securityUser = getCurrentUser(); + + EntityTypeFilter entityFilter = new EntityTypeFilter(); + entityFilter.setEntityType(EntityType.USER); + EntityDataPageLink pageLink = new EntityDataPageLink(pageSize, page, textSearch, createEntityDataSortOrder(sortProperty, sortOrder)); + List entityFields = Arrays.asList(new EntityKey(ENTITY_FIELD, "firstName"), + new EntityKey(ENTITY_FIELD, "lastName"), + new EntityKey(ENTITY_FIELD, "email")); + + EntityDataQuery query = new EntityDataQuery(entityFilter, pageLink, entityFields, null, null); + + return entityQueryService.findEntityDataByQuery(securityUser, query).mapData(entityData -> + { + Map fieldValues = entityData.getLatest().get(ENTITY_FIELD); + return new UserEmailInfo(UserId.fromString(entityData.getEntityId().getId().toString()), + fieldValues.get("email").getValue(), + fieldValues.get("firstName").getValue(), + fieldValues.get("lastName").getValue()); + }); + } + @ApiOperation(value = "Get Tenant Users (getTenantAdmins)", notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @@ -314,13 +367,9 @@ public class UserController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("tenantId", strTenantId); - try { - TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(userService.findTenantAdmins(tenantId, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(userService.findTenantAdmins(tenantId, pageLink)); } @ApiOperation(value = "Get Customer Users (getCustomerUsers)", @@ -342,15 +391,11 @@ public class UserController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); - try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(userService.findCustomerUsers(tenantId, customerId, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(userService.findCustomerUsers(tenantId, customerId, pageLink)); } @ApiOperation(value = "Enable/Disable User credentials (setUserCredentialsEnabled)", @@ -361,21 +406,191 @@ public class UserController extends BaseController { public void setUserCredentialsEnabled( @ApiParam(value = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId, - @ApiParam(value = "Disable (\"true\") or enable (\"false\") the credentials.", defaultValue = "true") + @ApiParam(value = "Enable (\"true\") or disable (\"false\") the credentials.", defaultValue = "true") @RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException { checkParameter(USER_ID, strUserId); - try { - UserId userId = new UserId(toUUID(strUserId)); - User user = checkUserId(userId, Operation.WRITE); - TenantId tenantId = getCurrentUser().getTenantId(); - userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); + UserId userId = new UserId(toUUID(strUserId)); + User user = checkUserId(userId, Operation.WRITE); + TenantId tenantId = getCurrentUser().getTenantId(); + userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); + + if (!userCredentialsEnabled) { + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId)); + } + } - if (!userCredentialsEnabled) { - eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId)); + @ApiOperation(value = "Get usersForAssign (getUsersForAssign)", + notes = "Returns page of user data objects that can be assigned to provided alarmId. " + + "Search is been executed by email, firstName and lastName fields. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getUsersForAssign( + @ApiParam(value = ALARM_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("alarmId") String strAlarmId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = USER_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = USER_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + checkParameter("alarmId", strAlarmId); + AlarmId alarmEntityId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmEntityId, Operation.READ); + SecurityUser currentUser = getCurrentUser(); + TenantId tenantId = currentUser.getTenantId(); + CustomerId originatorCustomerId = entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator()).get(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData pageData; + if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) { + if (alarm.getCustomerId() == null) { + pageData = userService.findTenantAdmins(tenantId, pageLink); + } else { + ArrayList customerIds = new ArrayList<>(Collections.singletonList(new CustomerId(CustomerId.NULL_UUID))); + if (!CustomerId.NULL_UUID.equals(originatorCustomerId.getId())) { + customerIds.add(originatorCustomerId); + } + pageData = userService.findUsersByCustomerIds(tenantId, customerIds, pageLink); + } + } else { + pageData = userService.findCustomerUsers(tenantId, alarm.getCustomerId(), pageLink); } + return pageData.mapData(user -> new UserEmailInfo(user.getId(), user.getEmail(), user.getFirstName(), user.getLastName())); } catch (Exception e) { throw handleException(e); } } + @ApiOperation(value = "Save user settings (saveUserSettings)", + notes = "Save user settings represented in json format for authorized user. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PostMapping(value = "/user/settings") + public JsonNode saveUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + + UserSettings userSettings = new UserSettings(); + userSettings.setType(UserSettingsType.GENERAL); + userSettings.setSettings(settings); + userSettings.setUserId(currentUser.getId()); + return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings(); + } + + @ApiOperation(value = "Update user settings (saveUserSettings)", + notes = "Update user settings for authorized user. Only specified json elements will be updated." + + "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + + "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PutMapping(value = "/user/settings") + public void putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, settings); + } + + @ApiOperation(value = "Get user settings (getUserSettings)", + notes = "Fetch the User settings based on authorized user. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/settings") + public JsonNode getUserSettings() throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + + UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL); + return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); + } + + @ApiOperation(value = "Delete user settings (deleteUserSettings)", + notes = "Delete user settings by specifying list of json element xpaths. \n " + + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/user/settings/{paths}", method = RequestMethod.DELETE) + public void deleteUserSettings(@ApiParam(value = PATHS) + @PathVariable(PATHS) String paths) throws ThingsboardException { + checkParameter(USER_ID, paths); + + SecurityUser currentUser = getCurrentUser(); + userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, Arrays.asList(paths.split(","))); + } + + @ApiOperation(value = "Update user settings (saveUserSettings)", + notes = "Update user settings for authorized user. Only specified json elements will be updated." + + "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + + "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PutMapping(value = "/user/settings/{type}") + public void putUserSettings(@ApiParam(value = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") + @PathVariable("type") String strType, @RequestBody JsonNode settings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf); + checkNotReserved(strType, type); + userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), type, settings); + } + + @ApiOperation(value = "Get user settings (getUserSettings)", + notes = "Fetch the User settings based on authorized user. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/settings/{type}") + public JsonNode getUserSettings(@ApiParam(value = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") + @PathVariable("type") String strType) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf); + checkNotReserved(strType, type); + UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId(), type); + return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); + } + + @ApiOperation(value = "Delete user settings (deleteUserSettings)", + notes = "Delete user settings by specifying list of json element xpaths. \n " + + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/user/settings/{type}/{paths}", method = RequestMethod.DELETE) + public void deleteUserSettings(@ApiParam(value = PATHS) + @PathVariable(PATHS) String paths, + @ApiParam(value = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") + @PathVariable("type") String strType) throws ThingsboardException { + checkParameter(USER_ID, paths); + UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf); + checkNotReserved(strType, type); + SecurityUser currentUser = getCurrentUser(); + userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), type, Arrays.asList(paths.split(","))); + } + + @ApiOperation(value = "Get information about last visited and starred dashboards (getLastVisitedDashboards)", + notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/dashboards") + public UserDashboardsInfo getUserDashboardsInfo() throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + return userSettingsService.findUserDashboardsInfo(currentUser.getTenantId(), currentUser.getId()); + } + + @ApiOperation(value = "Report action of User over the dashboard (reportUserDashboardAction)", + notes = "Report action of User over the dashboard. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/user/dashboards/{dashboardId}/{action}", method = RequestMethod.GET) + @ResponseBody + public UserDashboardsInfo reportUserDashboardAction( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DashboardController.DASHBOARD_ID) String strDashboardId, + @ApiParam(value = "Dashboard action, one of: \"visit\", \"star\" or \"unstar\".") + @PathVariable("action") String strAction) throws ThingsboardException { + checkParameter(DashboardController.DASHBOARD_ID, strDashboardId); + checkParameter("action", strAction); + UserDashboardAction action = checkEnumParameter("Action", strAction, UserDashboardAction::valueOf); + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + checkDashboardInfoId(dashboardId, Operation.READ); + SecurityUser currentUser = getCurrentUser(); + return userSettingsService.reportUserDashboardAction(currentUser.getTenantId(), currentUser.getId(), dashboardId, action); + } + + private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException { + if (type.isReserved()) { + throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index 725d8ca2b4..d1ecd0441a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,12 +70,8 @@ public class WidgetTypeController extends AutoCommitController { @ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { checkParameter("widgetTypeId", strWidgetTypeId); - try { - WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); - return checkWidgetTypeId(widgetTypeId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); + return checkWidgetTypeId(widgetTypeId, Operation.READ); } @ApiOperation(value = "Create Or Update Widget Type (saveWidgetType)", @@ -93,32 +89,28 @@ public class WidgetTypeController extends AutoCommitController { @ResponseBody public WidgetTypeDetails saveWidgetType( @ApiParam(value = "A JSON value representing the Widget Type Details.", required = true) - @RequestBody WidgetTypeDetails widgetTypeDetails) throws ThingsboardException { - try { - var currentUser = getCurrentUser(); - if (Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { - widgetTypeDetails.setTenantId(TenantId.SYS_TENANT_ID); - } else { - widgetTypeDetails.setTenantId(currentUser.getTenantId()); - } + @RequestBody WidgetTypeDetails widgetTypeDetails) throws Exception { + var currentUser = getCurrentUser(); + if (Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { + widgetTypeDetails.setTenantId(TenantId.SYS_TENANT_ID); + } else { + widgetTypeDetails.setTenantId(currentUser.getTenantId()); + } - checkEntity(widgetTypeDetails.getId(), widgetTypeDetails, Resource.WIDGET_TYPE); - WidgetTypeDetails savedWidgetTypeDetails = widgetTypeService.saveWidgetType(widgetTypeDetails); + checkEntity(widgetTypeDetails.getId(), widgetTypeDetails, Resource.WIDGET_TYPE); + WidgetTypeDetails savedWidgetTypeDetails = widgetTypeService.saveWidgetType(widgetTypeDetails); - if (!Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { - WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(widgetTypeDetails.getTenantId(), widgetTypeDetails.getBundleAlias()); - if (widgetsBundle != null) { - autoCommit(currentUser, widgetsBundle.getId()); - } + if (!Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { + WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(widgetTypeDetails.getTenantId(), widgetTypeDetails.getBundleAlias()); + if (widgetsBundle != null) { + autoCommit(currentUser, widgetsBundle.getId()); } + } - sendEntityNotificationMsg(getTenantId(), savedWidgetTypeDetails.getId(), - widgetTypeDetails.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + sendEntityNotificationMsg(getTenantId(), savedWidgetTypeDetails.getId(), + widgetTypeDetails.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); - return checkNotNull(savedWidgetTypeDetails); - } catch (Exception e) { - throw handleException(e); - } + return checkNotNull(savedWidgetTypeDetails); } @ApiOperation(value = "Delete widget type (deleteWidgetType)", @@ -128,26 +120,21 @@ public class WidgetTypeController extends AutoCommitController { @ResponseStatus(value = HttpStatus.OK) public void deleteWidgetType( @ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { + @PathVariable("widgetTypeId") String strWidgetTypeId) throws Exception { checkParameter("widgetTypeId", strWidgetTypeId); - try { - var currentUser = getCurrentUser(); - WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); - WidgetTypeDetails wtd = checkWidgetTypeId(widgetTypeId, Operation.DELETE); - widgetTypeService.deleteWidgetType(currentUser.getTenantId(), widgetTypeId); - - if (wtd != null && !Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { - WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(wtd.getTenantId(), wtd.getBundleAlias()); - if (widgetsBundle != null) { - autoCommit(currentUser, widgetsBundle.getId()); - } + var currentUser = getCurrentUser(); + WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); + WidgetTypeDetails wtd = checkWidgetTypeId(widgetTypeId, Operation.DELETE); + widgetTypeService.deleteWidgetType(currentUser.getTenantId(), widgetTypeId); + + if (wtd != null && !Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { + WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(wtd.getTenantId(), wtd.getBundleAlias()); + if (widgetsBundle != null) { + autoCommit(currentUser, widgetsBundle.getId()); } - - sendEntityNotificationMsg(getTenantId(), widgetTypeId, EdgeEventActionType.DELETED); - - } catch (Exception e) { - throw handleException(e); } + + sendEntityNotificationMsg(getTenantId(), widgetTypeId, EdgeEventActionType.DELETED); } @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", @@ -160,17 +147,13 @@ public class WidgetTypeController extends AutoCommitController { @RequestParam boolean isSystem, @ApiParam(value = "Widget Bundle alias", required = true) @RequestParam String bundleAlias) throws ThingsboardException { - try { - TenantId tenantId; - if (isSystem) { - tenantId = TenantId.SYS_TENANT_ID; - } else { - tenantId = getCurrentUser().getTenantId(); - } - return checkNotNull(widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(tenantId, bundleAlias)); - } catch (Exception e) { - throw handleException(e); + TenantId tenantId; + if (isSystem) { + tenantId = TenantId.SYS_TENANT_ID; + } else { + tenantId = getCurrentUser().getTenantId(); } + return checkNotNull(widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(tenantId, bundleAlias)); } @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypes)", @@ -183,17 +166,13 @@ public class WidgetTypeController extends AutoCommitController { @RequestParam boolean isSystem, @ApiParam(value = "Widget Bundle alias", required = true) @RequestParam String bundleAlias) throws ThingsboardException { - try { - TenantId tenantId; - if (isSystem) { - tenantId = TenantId.SYS_TENANT_ID; - } else { - tenantId = getCurrentUser().getTenantId(); - } - return checkNotNull(widgetTypeService.findWidgetTypesDetailsByTenantIdAndBundleAlias(tenantId, bundleAlias)); - } catch (Exception e) { - throw handleException(e); + TenantId tenantId; + if (isSystem) { + tenantId = TenantId.SYS_TENANT_ID; + } else { + tenantId = getCurrentUser().getTenantId(); } + return checkNotNull(widgetTypeService.findWidgetTypesDetailsByTenantIdAndBundleAlias(tenantId, bundleAlias)); } @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfos)", @@ -206,17 +185,13 @@ public class WidgetTypeController extends AutoCommitController { @RequestParam boolean isSystem, @ApiParam(value = "Widget Bundle alias", required = true) @RequestParam String bundleAlias) throws ThingsboardException { - try { - TenantId tenantId; - if (isSystem) { - tenantId = TenantId.SYS_TENANT_ID; - } else { - tenantId = getCurrentUser().getTenantId(); - } - return checkNotNull(widgetTypeService.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId, bundleAlias)); - } catch (Exception e) { - throw handleException(e); + TenantId tenantId; + if (isSystem) { + tenantId = TenantId.SYS_TENANT_ID; + } else { + tenantId = getCurrentUser().getTenantId(); } + return checkNotNull(widgetTypeService.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId, bundleAlias)); } @ApiOperation(value = "Get Widget Type (getWidgetType)", @@ -231,20 +206,16 @@ public class WidgetTypeController extends AutoCommitController { @RequestParam String bundleAlias, @ApiParam(value = "Widget Type alias", required = true) @RequestParam String alias) throws ThingsboardException { - try { - TenantId tenantId; - if (isSystem) { - tenantId = TenantId.fromUUID(ModelConstants.NULL_UUID); - } else { - tenantId = getCurrentUser().getTenantId(); - } - WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdBundleAliasAndAlias(tenantId, bundleAlias, alias); - checkNotNull(widgetType); - accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, Operation.READ, widgetType.getId(), widgetType); - return widgetType; - } catch (Exception e) { - throw handleException(e); + TenantId tenantId; + if (isSystem) { + tenantId = TenantId.fromUUID(ModelConstants.NULL_UUID); + } else { + tenantId = getCurrentUser().getTenantId(); } + WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdBundleAliasAndAlias(tenantId, bundleAlias, alias); + checkNotNull(widgetType); + accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, Operation.READ, widgetType.getId(), widgetType); + return widgetType; } } diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index 23ec498d0e..737e6d54d5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,12 +74,8 @@ public class WidgetsBundleController extends BaseController { @ApiParam(value = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { checkParameter("widgetsBundleId", strWidgetsBundleId); - try { - WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); - return checkWidgetsBundleId(widgetsBundleId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); + return checkWidgetsBundleId(widgetsBundleId, Operation.READ); } @ApiOperation(value = "Create Or Update Widget Bundle (saveWidgetsBundle)", @@ -121,7 +117,7 @@ public class WidgetsBundleController extends BaseController { checkParameter("widgetsBundleId", strWidgetsBundleId); WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); WidgetsBundle widgetsBundle = checkWidgetsBundleId(widgetsBundleId, Operation.DELETE); - tbWidgetsBundleService.delete(widgetsBundle); + tbWidgetsBundleService.delete(widgetsBundle, getCurrentUser()); } @ApiOperation(value = "Get Widget Bundles (getWidgetsBundles)", @@ -141,16 +137,12 @@ public class WidgetsBundleController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { - return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(getTenantId(), pageLink)); - } else { - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink)); - } - } catch (Exception e) { - throw handleException(e); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { + return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(getTenantId(), pageLink)); + } else { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink)); } } @@ -160,15 +152,11 @@ public class WidgetsBundleController extends BaseController { @RequestMapping(value = "/widgetsBundles", method = RequestMethod.GET) @ResponseBody public List getWidgetsBundles() throws ThingsboardException { - try { - if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { - return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId())); - } else { - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId)); - } - } catch (Exception e) { - throw handleException(e); + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { + return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId())); + } else { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId)); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 432b35fd80..bc51c5fdd1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.BeanCreationNotAllowedException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.web.socket.CloseStatus; @@ -27,21 +28,24 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.adapter.NativeWebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; -import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.msg.tools.TbRateLimits; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.config.WebSocketConfiguration; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; -import org.thingsboard.server.service.telemetry.SessionEvent; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketMsgEndpoint; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; +import org.thingsboard.server.service.ws.SessionEvent; +import org.thingsboard.server.service.ws.WebSocketMsgEndpoint; +import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.WebSocketSessionType; import javax.websocket.RemoteEndpoint; import javax.websocket.SendHandler; @@ -50,42 +54,48 @@ import javax.websocket.Session; import java.io.IOException; import java.net.URI; import java.security.InvalidParameterException; +import java.util.Optional; import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; -import static org.thingsboard.server.service.telemetry.DefaultTelemetryWebSocketService.NUMBER_OF_PING_ATTEMPTS; +import static org.thingsboard.server.service.ws.DefaultWebSocketService.NUMBER_OF_PING_ATTEMPTS; @Service @TbCoreComponent @Slf4j -public class TbWebSocketHandler extends TextWebSocketHandler implements TelemetryWebSocketMsgEndpoint { +public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocketMsgEndpoint { - private static final ConcurrentMap internalSessionMap = new ConcurrentHashMap<>(); - private static final ConcurrentMap externalSessionMap = new ConcurrentHashMap<>(); + private final ConcurrentMap internalSessionMap = new ConcurrentHashMap<>(); + private final ConcurrentMap externalSessionMap = new ConcurrentHashMap<>(); - @Autowired - private TelemetryWebSocketService webSocketService; + @Autowired @Lazy + private WebSocketService webSocketService; @Autowired private TbTenantProfileCache tenantProfileCache; + @Autowired + private RateLimitService rateLimitService; + @Value("${server.ws.send_timeout:5000}") private long sendTimeout; @Value("${server.ws.ping_timeout:30000}") private long pingTimeout; + @Value("${server.ws.max_queue_messages_per_session:1000}") + private int wsMaxQueueMessagesPerSession; - private ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); - private ConcurrentMap perSessionUpdateLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); - private ConcurrentMap> tenantSessionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> customerSessionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> regularUserSessionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> publicUserSessionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> tenantSessionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> customerSessionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> regularUserSessionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> publicUserSessionsMap = new ConcurrentHashMap<>(); @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { @@ -130,15 +140,18 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } } String internalSessionId = session.getId(); - TelemetryWebSocketSessionRef sessionRef = toRef(session); + WebSocketSessionRef sessionRef = toRef(session); String externalSessionId = sessionRef.getSessionId(); if (!checkLimits(session, sessionRef)) { return; } - var tenantProfileConfiguration = tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration(); - internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef, tenantProfileConfiguration.getWsMsgQueueLimitPerSession() > 0 ? - tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : 500)); + var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); + int wsTenantProfileQueueLimit = tenantProfileConfiguration != null ? + tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : wsMaxQueueMessagesPerSession; + internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef, + (wsTenantProfileQueueLimit > 0 && wsTenantProfileQueueLimit < wsMaxQueueMessagesPerSession) ? + wsTenantProfileQueueLimit : wsMaxQueueMessagesPerSession)); externalSessionMap.put(externalSessionId, internalSessionId); processInWebSocketService(sessionRef, SessionEvent.onEstablished()); @@ -178,7 +191,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } } - private void processInWebSocketService(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) { + private void processInWebSocketService(WebSocketSessionRef sessionRef, SessionEvent event) { try { webSocketService.handleWebSocketSessionEvent(sessionRef, event); } catch (BeanCreationNotAllowedException e) { @@ -186,7 +199,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } } - private TelemetryWebSocketSessionRef toRef(WebSocketSession session) throws IOException { + private WebSocketSessionRef toRef(WebSocketSession session) throws IOException { URI sessionUri = session.getUri(); String path = sessionUri.getPath(); path = path.substring(WebSocketConfiguration.WS_PLUGIN_PREFIX.length()); @@ -195,25 +208,30 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } String[] pathElements = path.split("/"); String serviceToken = pathElements[0]; - if (!"telemetry".equalsIgnoreCase(serviceToken)) { - throw new InvalidParameterException("Can't find plugin with specified token!"); - } else { - SecurityUser currentUser = (SecurityUser) ((Authentication) session.getPrincipal()).getPrincipal(); - return new TelemetryWebSocketSessionRef(UUID.randomUUID().toString(), currentUser, session.getLocalAddress(), session.getRemoteAddress()); - } + WebSocketSessionType sessionType = WebSocketSessionType.forName(serviceToken) + .orElseThrow(() -> new InvalidParameterException("Can't find plugin with specified token!")); + + SecurityUser currentUser = (SecurityUser) ((Authentication) session.getPrincipal()).getPrincipal(); + return WebSocketSessionRef.builder() + .sessionId(UUID.randomUUID().toString()) + .securityCtx(currentUser) + .localAddress(session.getLocalAddress()) + .remoteAddress(session.getRemoteAddress()) + .sessionType(sessionType) + .build(); } private class SessionMetaData implements SendHandler { private final WebSocketSession session; private final RemoteEndpoint.Async asyncRemote; - private final TelemetryWebSocketSessionRef sessionRef; + private final WebSocketSessionRef sessionRef; - private volatile boolean isSending = false; + private final AtomicBoolean isSending = new AtomicBoolean(false); private final Queue> msgQueue; private volatile long lastActivityTime; - SessionMetaData(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef, int maxMsgQueuePerSession) { + SessionMetaData(WebSocketSession session, WebSocketSessionRef sessionRef, int maxMsgQueuePerSession) { super(); this.session = session; Session nativeSession = ((NativeWebSocketSession) session).getNativeSession(Session.class); @@ -255,7 +273,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } synchronized void sendMsg(TbWebSocketMsg msg) { - if (isSending) { + if (isSending.compareAndSet(false, true)) { + sendMsgInternal(msg); + } else { try { msgQueue.add(msg); } catch (RuntimeException e) { @@ -266,9 +286,6 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!")); } - } else { - isSending = true; - sendMsgInternal(msg); } } @@ -303,35 +320,30 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr if (msg != null) { sendMsgInternal(msg); } else { - isSending = false; + isSending.set(false); } } } @Override - public void send(TelemetryWebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException { + public void send(WebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException { String externalId = sessionRef.getSessionId(); log.debug("[{}] Processing {}", externalId, msg); String internalId = externalSessionMap.get(externalId); if (internalId != null) { SessionMetaData sessionMd = internalSessionMap.get(internalId); if (sessionMd != null) { - var tenantProfileConfiguration = tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration(); - if (StringUtils.isNotEmpty(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())) { - TbRateLimits rateLimits = perSessionUpdateLimits.computeIfAbsent(sessionRef.getSessionId(), sid -> new TbRateLimits(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())); - if (!rateLimits.tryConsume()) { - if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) { - log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached" - , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); - sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"); - } - return; - } else { - log.debug("[{}][{}][{}] Session is no longer blacklisted.", sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); - blacklistedSessions.remove(externalId); + TenantId tenantId = sessionRef.getSecurityCtx().getTenantId(); + if (!rateLimitService.checkRateLimit(LimitedApi.WS_UPDATES_PER_SESSION, tenantId, (Object) sessionRef.getSessionId())) { + if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) { + log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached" + , tenantId, sessionRef.getSecurityCtx().getId(), externalId); + sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"); } + return; } else { - perSessionUpdateLimits.remove(sessionRef.getSessionId()); + log.debug("[{}][{}][{}] Session is no longer blacklisted.", tenantId, sessionRef.getSecurityCtx().getId(), externalId); + blacklistedSessions.remove(externalId); } sessionMd.sendMsg(msg); } else { @@ -343,7 +355,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } @Override - public void sendPing(TelemetryWebSocketSessionRef sessionRef, long currentTime) throws IOException { + public void sendPing(WebSocketSessionRef sessionRef, long currentTime) throws IOException { String externalId = sessionRef.getSessionId(); String internalId = externalSessionMap.get(externalId); if (internalId != null) { @@ -359,7 +371,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } @Override - public void close(TelemetryWebSocketSessionRef sessionRef, CloseStatus reason) throws IOException { + public void close(WebSocketSessionRef sessionRef, CloseStatus reason) throws IOException { String externalId = sessionRef.getSessionId(); log.debug("[{}] Processing close request", externalId); String internalId = externalSessionMap.get(externalId); @@ -375,9 +387,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } } - private boolean checkLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) throws Exception { - var tenantProfileConfiguration = - tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration(); + private boolean checkLimits(WebSocketSession session, WebSocketSessionRef sessionRef) throws Exception { + var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); if (tenantProfileConfiguration == null) { return true; } @@ -443,11 +454,12 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr return true; } - private void cleanupLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) { - var tenantProfileConfiguration = tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration(); + private void cleanupLimits(WebSocketSession session, WebSocketSessionRef sessionRef) { + var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); + if (tenantProfileConfiguration == null) return; String sessionId = session.getId(); - perSessionUpdateLimits.remove(sessionRef.getSessionId()); + rateLimitService.cleanUp(LimitedApi.WS_UPDATES_PER_SESSION, sessionRef.getSessionId()); blacklistedSessions.remove(sessionRef.getSessionId()); if (tenantProfileConfiguration.getMaxWsSessionsPerTenant() > 0) { Set tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); @@ -477,4 +489,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } } -} \ No newline at end of file + private DefaultTenantProfileConfiguration getTenantProfileConfiguration(WebSocketSessionRef sessionRef) { + return Optional.ofNullable(tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId())) + .map(TenantProfile::getDefaultProfileConfiguration).orElse(null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketMsg.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketMsg.java index a48b9c5bf4..22c3207f12 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketMsg.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketMsg.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketMsgType.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketMsgType.java index de2500d853..d529a1859d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketMsgType.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketMsgType.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketPingMsg.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketPingMsg.java index 2fbc6c16a0..a641e20d58 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketPingMsg.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketPingMsg.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketTextMsg.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketTextMsg.java index 54efca4da7..ee642bbaac 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketTextMsg.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketTextMsg.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/exception/AccessDeniedException.java b/application/src/main/java/org/thingsboard/server/exception/AccessDeniedException.java new file mode 100644 index 0000000000..a82d4b6234 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/AccessDeniedException.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class AccessDeniedException extends Exception implements ToErrorResponseEntity { + + public AccessDeniedException(String message) { + super(message); + } + + @Override + public ResponseEntity toErrorResponseEntity() { + return new ResponseEntity<>(getMessage(), HttpStatus.FORBIDDEN); + } +} diff --git a/application/src/main/java/org/thingsboard/server/exception/EntityNotFoundException.java b/application/src/main/java/org/thingsboard/server/exception/EntityNotFoundException.java new file mode 100644 index 0000000000..53fd323691 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/EntityNotFoundException.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class EntityNotFoundException extends Exception implements ToErrorResponseEntity { + + public EntityNotFoundException(String message) { + super(message); + } + + @Override + public ResponseEntity toErrorResponseEntity() { + return new ResponseEntity<>(getMessage(), HttpStatus.NOT_FOUND); + } +} diff --git a/application/src/main/java/org/thingsboard/server/exception/InternalErrorException.java b/application/src/main/java/org/thingsboard/server/exception/InternalErrorException.java new file mode 100644 index 0000000000..ffa0d0cb07 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/InternalErrorException.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class InternalErrorException extends Exception implements ToErrorResponseEntity { + + public InternalErrorException(String message) { + super(message); + } + + @Override + public ResponseEntity toErrorResponseEntity() { + return new ResponseEntity<>(getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/application/src/main/java/org/thingsboard/server/exception/InvalidParametersException.java b/application/src/main/java/org/thingsboard/server/exception/InvalidParametersException.java new file mode 100644 index 0000000000..ad41e75b3d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/InvalidParametersException.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class InvalidParametersException extends Exception implements ToErrorResponseEntity { + + public InvalidParametersException(String message) { + super(message); + } + + @Override + public ResponseEntity toErrorResponseEntity() { + return new ResponseEntity<>(getMessage(), HttpStatus.BAD_REQUEST); + } +} diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java index 208fdead93..46e7205824 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java index 7cf2ca87f1..73cc5b0902 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index 30e934f386..ed641c2d4f 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.exception; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -36,6 +35,7 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.util.WebUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; @@ -90,9 +90,6 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand return errorCodeToStatusMap.getOrDefault(errorCode, HttpStatus.INTERNAL_SERVER_ERROR); } - @Autowired - private ObjectMapper mapper; - @Override @ExceptionHandler(AccessDeniedException.class) public void handle(HttpServletRequest request, HttpServletResponse response, @@ -101,7 +98,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand if (!response.isCommitted()) { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpStatus.FORBIDDEN.value()); - mapper.writeValue(response.getWriter(), + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("You don't have permission to perform this operation!", ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN)); } @@ -129,7 +126,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand handleAuthenticationException((AuthenticationException) exception, response); } else { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(), + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(), ThingsboardErrorCode.GENERAL, HttpStatus.INTERNAL_SERVER_ERROR)); } } catch (IOException e) { @@ -154,26 +151,26 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand ThingsboardErrorCode errorCode = thingsboardException.getErrorCode(); HttpStatus status = errorCodeToStatus(errorCode); response.setStatus(status.value()); - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(thingsboardException.getMessage(), errorCode, status)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(thingsboardException.getMessage(), errorCode, status)); } private void handleRateLimitException(HttpServletResponse response, TbRateLimitsException exception) throws IOException { response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); String message = "Too many requests for current " + exception.getEntityType().name().toLowerCase() + "!"; - mapper.writeValue(response.getWriter(), + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(message, ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS)); } private void handleSubscriptionException(ThingsboardException subscriptionException, HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); - mapper.writeValue(response.getWriter(), - (new ObjectMapper()).readValue(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class)); + JacksonUtil.writeValue(response.getWriter(), + JacksonUtil.fromBytes(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class)); } private void handleAccessDeniedException(HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); - mapper.writeValue(response.getWriter(), + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("You don't have permission to perform this operation!", ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN)); @@ -182,21 +179,21 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.UNAUTHORIZED.value()); if (authenticationException instanceof BadCredentialsException || authenticationException instanceof UsernameNotFoundException) { - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof DisabledException) { - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof LockedException) { - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof JwtExpiredTokenException) { - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof AuthMethodNotSupportedException) { - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof UserPasswordExpiredException) { UserPasswordExpiredException expiredException = (UserPasswordExpiredException) authenticationException; String resetToken = expiredException.getResetToken(); - mapper.writeValue(response.getWriter(), ThingsboardCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken)); } else { - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } } diff --git a/application/src/main/java/org/thingsboard/server/exception/ToErrorResponseEntity.java b/application/src/main/java/org/thingsboard/server/exception/ToErrorResponseEntity.java new file mode 100644 index 0000000000..ce79f852f3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/ToErrorResponseEntity.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import org.springframework.http.ResponseEntity; + +import java.io.Serializable; + +public interface ToErrorResponseEntity extends Serializable { + + ResponseEntity toErrorResponseEntity(); + +} diff --git a/application/src/main/java/org/thingsboard/server/exception/UnauthorizedException.java b/application/src/main/java/org/thingsboard/server/exception/UnauthorizedException.java new file mode 100644 index 0000000000..2d3d8dbf8f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/UnauthorizedException.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +/** + * Created by ashvayka on 21.02.17. + */ +public class UnauthorizedException extends Exception implements ToErrorResponseEntity { + + public UnauthorizedException(String message) { + super(message); + } + + @Override + public ResponseEntity toErrorResponseEntity() { + return new ResponseEntity<>(getMessage(), HttpStatus.UNAUTHORIZED); + } +} diff --git a/application/src/main/java/org/thingsboard/server/exception/UncheckedApiException.java b/application/src/main/java/org/thingsboard/server/exception/UncheckedApiException.java new file mode 100644 index 0000000000..38f8a4a51f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/UncheckedApiException.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import org.springframework.http.ResponseEntity; + +import java.util.Objects; + +public class UncheckedApiException extends RuntimeException implements ToErrorResponseEntity { + + private final ToErrorResponseEntity cause; + + public UncheckedApiException(T cause) { + super(cause.getMessage(), Objects.requireNonNull(cause)); + this.cause = cause; + } + + @Override + public ResponseEntity toErrorResponseEntity() { + return cause.toErrorResponseEntity(); + } +} + diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java index 4338f4aa44..4daebe0b26 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallException.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallException.java index 81d9a7d22a..c57ba35a14 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallException.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallException.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 9facea6dde..51d7435f0e 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.install.DatabaseEntitiesUpgradeService; import org.thingsboard.server.service.install.DatabaseTsUpgradeService; import org.thingsboard.server.service.install.EntityDatabaseSchemaService; +import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.install.NoSqlKeyspaceService; import org.thingsboard.server.service.install.SystemDataLoaderService; import org.thingsboard.server.service.install.TsDatabaseSchemaService; @@ -35,6 +36,8 @@ import org.thingsboard.server.service.install.migrate.TsLatestMigrateService; import org.thingsboard.server.service.install.update.CacheCleanupService; import org.thingsboard.server.service.install.update.DataUpdateService; +import static org.thingsboard.server.service.install.update.DefaultDataUpdateService.getEnv; + @Service @Profile("install") @Slf4j @@ -49,6 +52,9 @@ public class ThingsboardInstallService { @Value("${install.load_demo:false}") private Boolean loadDemo; + @Value("${state.persistToTelemetry:false}") + private boolean persistToTelemetry; + @Autowired private EntityDatabaseSchemaService entityDatabaseSchemaService; @@ -88,6 +94,9 @@ public class ThingsboardInstallService { @Autowired(required = false) private TsLatestMigrateService latestMigrateService; + @Autowired + private InstallScripts installScripts; + public void performInstall() { try { if (isUpgrade) { @@ -233,16 +242,34 @@ public class ThingsboardInstallService { log.info("Upgrading ThingsBoard from version 3.4.1 to 3.4.2 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.4.1"); dataUpdateService.updateData("3.4.1"); - log.info("Updating system data..."); - systemDataLoaderService.updateSystemWidgets(); + case "3.4.2": + log.info("Upgrading ThingsBoard from version 3.4.2 to 3.4.3 ..."); + case "3.4.3": + log.info("Upgrading ThingsBoard from version 3.4.3 to 3.4.4 ..."); + case "3.4.4": + log.info("Upgrading ThingsBoard from version 3.4.4 to 3.5.0 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.4.4"); + if (!getEnv("SKIP_DEFAULT_NOTIFICATION_CONFIGS_CREATION", false)) { + systemDataLoaderService.createDefaultNotificationConfigs(); + } else { + log.info("Skipping default notification configs creation"); + } + case "3.5.0": + log.info("Upgrading ThingsBoard from version 3.5.0 to 3.5.1 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.5.0"); + case "3.5.1": + log.info("Upgrading ThingsBoard from version 3.5.1 to 3.5.2 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.5.1"); + //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; - - //TODO update CacheCleanupService on the next version upgrade - default: throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); - } + entityDatabaseSchemaService.createOrUpdateViewsAndFunctions(); + entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry); + log.info("Updating system data..."); + systemDataLoaderService.updateSystemWidgets(); + installScripts.loadSystemLwm2mResources(); } log.info("Upgrade finished successfully!"); @@ -254,6 +281,9 @@ public class ThingsboardInstallService { entityDatabaseSchemaService.createDatabaseSchema(); + entityDatabaseSchemaService.createOrUpdateViewsAndFunctions(); + entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry); + log.info("Installing DataBase schema for timeseries..."); if (noSqlKeyspaceService != null) { @@ -277,8 +307,11 @@ public class ThingsboardInstallService { systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.createOAuth2Templates(); systemDataLoaderService.createQueues(); + systemDataLoaderService.createDefaultNotificationConfigs(); + // systemDataLoaderService.loadSystemPlugins(); // systemDataLoaderService.loadSystemRules(); + installScripts.loadSystemLwm2mResources(); if (loadDemo) { log.info("Loading demo data..."); @@ -297,3 +330,4 @@ public class ThingsboardInstallService { } } + diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index 7057f903be..623b95b40e 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.action; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; @@ -29,6 +28,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.CustomerId; @@ -40,7 +40,10 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger; +import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger; import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import java.util.List; import java.util.Map; @@ -52,8 +55,7 @@ import java.util.stream.Collectors; public class EntityActionService { private final TbClusterService tbClusterService; private final AuditLogService auditLogService; - - private static final ObjectMapper json = new ObjectMapper(); + private final NotificationRuleProcessor notificationRuleProcessor; public void pushEntityActionToRuleEngine(EntityId entityId, HasName entity, TenantId tenantId, CustomerId customerId, ActionType actionType, User user, Object... additionalInfo) { @@ -86,9 +88,21 @@ public class EntityActionService { case ALARM_CLEAR: msgType = DataConstants.ALARM_CLEAR; break; + case ALARM_ASSIGNED: + msgType = DataConstants.ALARM_ASSIGNED; + break; + case ALARM_UNASSIGNED: + msgType = DataConstants.ALARM_UNASSIGNED; + break; case ALARM_DELETE: msgType = DataConstants.ALARM_DELETE; break; + case ADDED_COMMENT: + msgType = DataConstants.COMMENT_CREATED; + break; + case UPDATED_COMMENT: + msgType = DataConstants.COMMENT_UPDATED; + break; case ASSIGNED_FROM_TENANT: msgType = DataConstants.ENTITY_ASSIGNED_FROM_TENANT; break; @@ -129,6 +143,13 @@ public class EntityActionService { if (user != null) { metaData.putValue("userId", user.getId().toString()); metaData.putValue("userName", user.getName()); + metaData.putValue("userEmail", user.getEmail()); + if (user.getFirstName() != null) { + metaData.putValue("userFirstName", user.getFirstName()); + } + if (user.getLastName() != null) { + metaData.putValue("userLastName", user.getLastName()); + } } if (customerId != null && !customerId.isNullUid()) { metaData.putValue("customerId", customerId.toString()); @@ -163,15 +184,20 @@ public class EntityActionService { String strEdgeName = extractParameter(String.class, 2, additionalInfo); metaData.putValue("unassignedEdgeId", strEdgeId); metaData.putValue("unassignedEdgeName", strEdgeName); + } else if (actionType == ActionType.ADDED_COMMENT || actionType == ActionType.UPDATED_COMMENT) { + AlarmComment comment = extractParameter(AlarmComment.class, 0, additionalInfo); + metaData.putValue("comment", JacksonUtil.toString(comment)); } ObjectNode entityNode; if (entity != null) { - entityNode = json.valueToTree(entity); + entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(entity); if (entityId.getEntityType() == EntityType.DASHBOARD) { entityNode.put("configuration", ""); } + metaData.putValue("entityName", entity.getName()); + metaData.putValue("entityType", entityId.getEntityType().toString()); } else { - entityNode = json.createObjectNode(); + entityNode = JacksonUtil.newObjectNode(); if (actionType == ActionType.ATTRIBUTES_UPDATED) { String scope = extractParameter(String.class, 0, additionalInfo); @SuppressWarnings("unchecked") @@ -205,15 +231,31 @@ public class EntityActionService { entityNode.put("startTs", extractParameter(Long.class, 1, additionalInfo)); entityNode.put("endTs", extractParameter(Long.class, 2, additionalInfo)); } else if (ActionType.RELATION_ADD_OR_UPDATE.equals(actionType) || ActionType.RELATION_DELETED.equals(actionType)) { - entityNode = json.valueToTree(extractParameter(EntityRelation.class, 0, additionalInfo)); + entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(extractParameter(EntityRelation.class, 0, additionalInfo)); } } - TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode)); + if (tenantId == null || tenantId.isNullUid()) { if (entity instanceof HasTenantId) { tenantId = ((HasTenantId) entity).getTenantId(); } } + if (tenantId != null && !tenantId.isSysTenantId()) { + if (actionType == ActionType.ADDED) { + notificationRuleProcessor.process(EntitiesLimitTrigger.builder() + .tenantId(tenantId) + .entityType(entityId.getEntityType()) + .build()); + } + notificationRuleProcessor.process(EntityActionTrigger.builder() + .tenantId(tenantId) + .entityId(entityId) + .entity(entity) + .actionType(actionType) + .user(user) + .build()); + } + TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null); } catch (Exception e) { log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e); @@ -222,7 +264,7 @@ public class EntityActionService { } public void logEntityAction(User user, I entityId, E entity, CustomerId customerId, - ActionType actionType, Exception e, Object... additionalInfo) { + ActionType actionType, Exception e, Object... additionalInfo) { if (customerId == null || customerId.isNullUid()) { customerId = user.getCustomerId(); } @@ -253,7 +295,7 @@ public class EntityActionService { Map> groupedTelemetry = timeseries.stream() .collect(Collectors.groupingBy(TsKvEntry::getTs)); for (Map.Entry> entry : groupedTelemetry.entrySet()) { - ObjectNode element = json.createObjectNode(); + ObjectNode element = JacksonUtil.newObjectNode(); element.put("ts", entry.getKey()); ObjectNode values = element.putObject("values"); for (TsKvEntry tsKvEntry : entry.getValue()) { diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java index 44e9c32864..1215bef0bf 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,10 @@ */ package org.thingsboard.server.service.apiusage; +import lombok.Builder; +import lombok.Data; import lombok.Getter; -import org.springframework.data.util.Pair; +import lombok.Setter; import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; @@ -26,17 +28,17 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.tools.SchedulerUtils; -import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public abstract class BaseApiUsageState { private final Map currentCycleValues = new ConcurrentHashMap<>(); private final Map currentHourValues = new ConcurrentHashMap<>(); + private final Map> lastGaugesByServiceId = new HashMap<>(); + private final Map gaugesReportCycles = new HashMap<>(); + @Getter private final ApiUsageState apiUsageState; @Getter @@ -46,6 +48,9 @@ public abstract class BaseApiUsageState { @Getter private volatile long currentHourTs; + @Setter + private long gaugeReportInterval; + public BaseApiUsageState(ApiUsageState apiUsageState) { this.apiUsageState = apiUsageState; this.currentCycleTs = SchedulerUtils.getStartOfCurrentMonth(); @@ -53,43 +58,81 @@ public abstract class BaseApiUsageState { this.currentHourTs = SchedulerUtils.getStartOfCurrentHour(); } - public void put(ApiUsageRecordKey key, Long value) { - currentCycleValues.put(key, value); + public StatsCalculationResult calculate(ApiUsageRecordKey key, long value, String serviceId) { + long currentValue = get(key); + long currentHourlyValue = getHourly(key); + + StatsCalculationResult result; + if (key.isCounter()) { + result = StatsCalculationResult.builder() + .newValue(currentValue + value).valueChanged(true) + .newHourlyValue(currentHourlyValue + value).hourlyValueChanged(true) + .build(); + } else { + Long newGaugeValue = calculateGauge(key, value, serviceId); + long newValue = newGaugeValue != null ? newGaugeValue : currentValue; + long newHourlyValue = newGaugeValue != null ? Math.max(newGaugeValue, currentHourlyValue) : currentHourlyValue; + result = StatsCalculationResult.builder() + .newValue(newValue).valueChanged(newValue != currentValue || !currentCycleValues.containsKey(key)) + .newHourlyValue(newHourlyValue).hourlyValueChanged(newHourlyValue != currentHourlyValue || !currentHourValues.containsKey(key)) + .build(); + } + + set(key, result.getNewValue()); + setHourly(key, result.getNewHourlyValue()); + return result; } - public void putHourly(ApiUsageRecordKey key, Long value) { - currentHourValues.put(key, value); + private Long calculateGauge(ApiUsageRecordKey key, long value, String serviceId) { + Map lastByServiceId = lastGaugesByServiceId.computeIfAbsent(key, k -> { + gaugesReportCycles.put(key, System.currentTimeMillis()); + return new HashMap<>(); + }); + lastByServiceId.put(serviceId, value); + + Long gaugeReportCycle = gaugesReportCycles.get(key); + if (gaugeReportCycle <= System.currentTimeMillis() - gaugeReportInterval) { + long newValue = lastByServiceId.values().stream().mapToLong(Long::longValue).sum(); + lastGaugesByServiceId.remove(key); + gaugesReportCycles.remove(key); + return newValue; + } else { + return null; + } } - public long add(ApiUsageRecordKey key, long value) { - long result = currentCycleValues.getOrDefault(key, 0L) + value; - currentCycleValues.put(key, result); - return result; + public void set(ApiUsageRecordKey key, Long value) { + currentCycleValues.put(key, value); } public long get(ApiUsageRecordKey key) { return currentCycleValues.getOrDefault(key, 0L); } - public long addToHourly(ApiUsageRecordKey key, long value) { - long result = currentHourValues.getOrDefault(key, 0L) + value; - currentHourValues.put(key, result); - return result; + public void setHourly(ApiUsageRecordKey key, Long value) { + currentHourValues.put(key, value); + } + + public long getHourly(ApiUsageRecordKey key) { + return currentHourValues.getOrDefault(key, 0L); } public void setHour(long currentHourTs) { this.currentHourTs = currentHourTs; - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - currentHourValues.put(key, 0L); - } + currentHourValues.clear(); + lastGaugesByServiceId.clear(); + gaugesReportCycles.clear(); } public void setCycles(long currentCycleTs, long nextCycleTs) { this.currentCycleTs = currentCycleTs; this.nextCycleTs = nextCycleTs; - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - currentCycleValues.put(key, 0L); - } + currentCycleValues.clear(); + } + + public void onRepartitionEvent() { + lastGaugesByServiceId.clear(); + gaugesReportCycles.clear(); } public ApiUsageStateValue getFeatureValue(ApiFeature feature) { @@ -150,4 +193,14 @@ public abstract class BaseApiUsageState { public EntityId getEntityId() { return getApiUsageState().getEntityId(); } + + @Data + @Builder + public static class StatsCalculationResult { + private final long newValue; + private final boolean valueChanged; + private final long newHourlyValue; + private final boolean hourlyValueChanged; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/CustomerApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/CustomerApiUsageState.java index 00722425d6..6e93a1bab1 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/CustomerApiUsageState.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/CustomerApiUsageState.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java deleted file mode 100644 index 525386d884..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.apiusage; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.msg.tools.TbRateLimits; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; - -@Service -@RequiredArgsConstructor -public class DefaultRateLimitService implements RateLimitService { - - private final TbTenantProfileCache tenantProfileCache; - - private final Map> rateLimits = new ConcurrentHashMap<>(); - - @Override - public boolean checkEntityExportLimit(TenantId tenantId) { - return checkLimit(tenantId, "entityExport", DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit); - } - - @Override - public boolean checkEntityImportLimit(TenantId tenantId) { - return checkLimit(tenantId, "entityImport", DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit); - } - - private boolean checkLimit(TenantId tenantId, String rateLimitsKey, Function rateLimitConfigExtractor) { - String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration() - .map(rateLimitConfigExtractor).orElse(null); - - Map rateLimits = this.rateLimits.get(rateLimitsKey); - if (StringUtils.isEmpty(rateLimitConfig)) { - if (rateLimits != null) { - rateLimits.remove(tenantId); - if (rateLimits.isEmpty()) { - this.rateLimits.remove(rateLimitsKey); - } - } - return true; - } - - if (rateLimits == null) { - rateLimits = new ConcurrentHashMap<>(); - this.rateLimits.put(rateLimitsKey, rateLimits); - } - TbRateLimits rateLimit = rateLimits.get(tenantId); - if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) { - rateLimit = new TbRateLimits(rateLimitConfig); - rateLimits.put(tenantId, rateLimit); - } - - return rateLimit.tryConsume(); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index b8ca8b37c0..9e17641c69 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,19 +17,19 @@ package org.thingsboard.server.service.apiusage; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageRecordState; import org.thingsboard.server.common.data.ApiUsageState; -import org.thingsboard.server.common.data.ApiUsageStateMailMessage; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -60,7 +61,10 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; +import org.thingsboard.server.service.apiusage.BaseApiUsageState.StatsCalculationResult; import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.mail.MailExecutorService; import org.thingsboard.server.service.partition.AbstractPartitionBasedService; import org.thingsboard.server.service.telemetry.InternalTelemetryService; @@ -77,8 +81,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -86,6 +88,7 @@ import java.util.stream.Collectors; @Slf4j @Service +@RequiredArgsConstructor public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService implements TbApiUsageStateService { public static final String HOURLY = "Hourly"; @@ -105,7 +108,9 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService private final ApiUsageStateService apiUsageStateService; private final TbTenantProfileCache tenantProfileCache; private final MailService mailService; + private final NotificationRuleProcessor notificationRuleProcessor; private final DbCallbackExecutorService dbExecutor; + private final MailExecutorService mailExecutor; @Lazy @Autowired @@ -124,28 +129,10 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService @Value("${usage.stats.check.cycle:60000}") private long nextCycleCheckInterval; - private final Lock updateLock = new ReentrantLock(); + @Value("${usage.stats.gauge_report_interval:180000}") + private long gaugeReportInterval; - private final ExecutorService mailExecutor; - - public DefaultTbApiUsageStateService(TbClusterService clusterService, - PartitionService partitionService, - TenantService tenantService, - TimeseriesService tsService, - ApiUsageStateService apiUsageStateService, - TbTenantProfileCache tenantProfileCache, - MailService mailService, - DbCallbackExecutorService dbExecutor) { - this.clusterService = clusterService; - this.partitionService = partitionService; - this.tenantService = tenantService; - this.tsService = tsService; - this.apiUsageStateService = apiUsageStateService; - this.tenantProfileCache = tenantProfileCache; - this.mailService = mailService; - this.mailExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("api-usage-svc-mail")); - this.dbExecutor = dbExecutor; - } + private final Lock updateLock = new ReentrantLock(); @PostConstruct public void init() { @@ -172,19 +159,19 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService ToUsageStatsServiceMsg statsMsg = msg.getValue(); TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); - EntityId entityId; + EntityId ownerId; if (statsMsg.getCustomerIdMSB() != 0 && statsMsg.getCustomerIdLSB() != 0) { - entityId = new CustomerId(new UUID(statsMsg.getCustomerIdMSB(), statsMsg.getCustomerIdLSB())); + ownerId = new CustomerId(new UUID(statsMsg.getCustomerIdMSB(), statsMsg.getCustomerIdLSB())); } else { - entityId = tenantId; + ownerId = tenantId; } - processEntityUsageStats(tenantId, entityId, statsMsg.getValuesList()); + processEntityUsageStats(tenantId, ownerId, statsMsg.getValuesList(), statsMsg.getServiceId()); callback.onSuccess(); } - private void processEntityUsageStats(TenantId tenantId, EntityId entityId, List values) { - if (deletedEntities.contains(entityId)) return; + private void processEntityUsageStats(TenantId tenantId, EntityId ownerId, List values, String serviceId) { + if (deletedEntities.contains(ownerId)) return; BaseApiUsageState usageState; List updatedEntries; @@ -192,7 +179,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService updateLock.lock(); try { - usageState = getOrFetchState(tenantId, entityId); + usageState = getOrFetchState(tenantId, ownerId); long ts = usageState.getCurrentCycleTs(); long hourTs = usageState.getCurrentHourTs(); long newHourTs = SchedulerUtils.getStartOfCurrentHour(); @@ -201,13 +188,21 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length); Set apiFeatures = new HashSet<>(); - for (UsageStatsKVProto kvProto : values) { - ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(kvProto.getKey()); - long newValue = usageState.add(recordKey, kvProto.getValue()); - updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue))); - long newHourlyValue = usageState.addToHourly(recordKey, kvProto.getValue()); - updatedEntries.add(new BasicTsKvEntry(newHourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue))); - apiFeatures.add(recordKey.getApiFeature()); + for (UsageStatsKVProto statsItem : values) { + ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(statsItem.getKey()); + + StatsCalculationResult calculationResult = usageState.calculate(recordKey, statsItem.getValue(), serviceId); + if (calculationResult.isValueChanged()) { + long newValue = calculationResult.getNewValue(); + updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue))); + } + if (calculationResult.isHourlyValueChanged()) { + long newHourlyValue = calculationResult.getNewHourlyValue(); + updatedEntries.add(new BasicTsKvEntry(newHourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue))); + } + if (recordKey.getApiFeature() != null) { + apiFeatures.add(recordKey.getApiFeature()); + } } if (usageState.getEntityType() == EntityType.TENANT && !usageState.getEntityId().equals(TenantId.SYS_TENANT_ID)) { result = ((TenantApiUsageState) usageState).checkStateUpdatedDueToThreshold(apiFeatures); @@ -356,30 +351,34 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService if (state.getEntityType() == EntityType.TENANT && !state.getEntityId().equals(TenantId.SYS_TENANT_ID)) { String email = tenantService.findTenantById(state.getTenantId()).getEmail(); - if (StringUtils.isNotEmpty(email)) { - result.forEach((apiFeature, stateValue) -> { + result.forEach((apiFeature, stateValue) -> { + ApiUsageRecordState recordState = createApiUsageRecordState((TenantApiUsageState) state, apiFeature, stateValue); + notificationRuleProcessor.process(ApiUsageLimitTrigger.builder() + .tenantId(state.getTenantId()) + .state(recordState) + .status(stateValue) + .build()); + if (StringUtils.isNotEmpty(email)) { mailExecutor.submit(() -> { try { - mailService.sendApiFeatureStateEmail(apiFeature, stateValue, email, createStateMailMessage((TenantApiUsageState) state, apiFeature, stateValue)); + mailService.sendApiFeatureStateEmail(apiFeature, stateValue, email, recordState); } catch (ThingsboardException e) { log.warn("[{}] Can't send update of the API state to tenant with provided email [{}]", state.getTenantId(), email, e); } }); - }); - } else { - log.warn("[{}] Can't send update of the API state to tenant with empty email!", state.getTenantId()); - } + } + }); } } - private ApiUsageStateMailMessage createStateMailMessage(TenantApiUsageState state, ApiFeature apiFeature, ApiUsageStateValue stateValue) { + private ApiUsageRecordState createApiUsageRecordState(TenantApiUsageState state, ApiFeature apiFeature, ApiUsageStateValue stateValue) { StateChecker checker = getStateChecker(stateValue); for (ApiUsageRecordKey apiUsageRecordKey : ApiUsageRecordKey.getKeys(apiFeature)) { long threshold = state.getProfileThreshold(apiUsageRecordKey); long warnThreshold = state.getProfileWarnThreshold(apiUsageRecordKey); long value = state.get(apiUsageRecordKey); if (checker.check(threshold, warnThreshold, value)) { - return new ApiUsageStateMailMessage(apiUsageRecordKey, threshold, value); + return new ApiUsageRecordState(apiFeature, apiUsageRecordKey, threshold, value); } } return null; @@ -391,7 +390,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } else if (ApiUsageStateValue.WARNING.equals(stateValue)) { return (t, wt, v) -> v < t && v >= wt; } else { - return (t, wt, v) -> v >= t; + return (t, wt, v) -> t > 0 && v >= t; } } @@ -431,26 +430,26 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), counts, VOID_CALLBACK); } - BaseApiUsageState getOrFetchState(TenantId tenantId, EntityId entityId) { - if (entityId == null || entityId.isNullUid()) { - entityId = tenantId; + BaseApiUsageState getOrFetchState(TenantId tenantId, EntityId ownerId) { + if (ownerId == null || ownerId.isNullUid()) { + ownerId = tenantId; } - BaseApiUsageState state = myUsageStates.get(entityId); + BaseApiUsageState state = myUsageStates.get(ownerId); if (state != null) { return state; } - ApiUsageState storedState = apiUsageStateService.findApiUsageStateByEntityId(entityId); + ApiUsageState storedState = apiUsageStateService.findApiUsageStateByEntityId(ownerId); if (storedState == null) { try { - storedState = apiUsageStateService.createDefaultApiUsageState(tenantId, entityId); + storedState = apiUsageStateService.createDefaultApiUsageState(tenantId, ownerId); } catch (Exception e) { - storedState = apiUsageStateService.findApiUsageStateByEntityId(entityId); + storedState = apiUsageStateService.findApiUsageStateByEntityId(ownerId); } } - if (entityId.getEntityType() == EntityType.TENANT) { - if (!entityId.equals(TenantId.SYS_TENANT_ID)) { - state = new TenantApiUsageState(tenantProfileCache.get((TenantId) entityId), storedState); + if (ownerId.getEntityType() == EntityType.TENANT) { + if (!ownerId.equals(TenantId.SYS_TENANT_ID)) { + state = new TenantApiUsageState(tenantProfileCache.get((TenantId) ownerId), storedState); } else { state = new TenantApiUsageState(storedState); } @@ -469,26 +468,27 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService cycleEntryFound = true; boolean oldCount = tsKvEntry.getTs() == state.getCurrentCycleTs(); - state.put(key, oldCount ? tsKvEntry.getLongValue().get() : 0L); + state.set(key, oldCount ? tsKvEntry.getLongValue().get() : 0L); if (!oldCount) { newCounts.add(key); } } else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) { hourlyEntryFound = true; - state.putHourly(key, tsKvEntry.getTs() == state.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L); + state.setHourly(key, tsKvEntry.getTs() == state.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L); } if (cycleEntryFound && hourlyEntryFound) { break; } } } - log.debug("[{}] Initialized state: {}", entityId, storedState); - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); + state.setGaugeReportInterval(gaugeReportInterval); + log.debug("[{}] Initialized state: {}", ownerId, storedState); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, ownerId); if (tpi.isMyPartition()) { addEntityState(tpi, state); } else { - otherUsageStates.put(entityId, state.getApiUsageState()); + otherUsageStates.put(ownerId, state.getApiUsageState()); } saveNewCounts(state, newCounts); } catch (InterruptedException | ExecutionException e) { @@ -502,6 +502,12 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService protected void onRepartitionEvent() { otherUsageStates.entrySet().removeIf(entry -> partitionService.resolve(ServiceType.TB_CORE, entry.getValue().getTenantId(), entry.getKey()).isMyPartition()); + updateLock.lock(); + try { + myUsageStates.values().forEach(BaseApiUsageState::onRepartitionEvent); + } finally { + updateLock.unlock(); + } } @Override @@ -543,8 +549,5 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService @PreDestroy private void destroy() { super.stop(); - if (mailExecutor != null) { - mailExecutor.shutdownNow(); - } } } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java deleted file mode 100644 index d3d4244ca1..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.apiusage; - -import org.thingsboard.server.common.data.id.TenantId; - -public interface RateLimitService { - - boolean checkEntityExportLimit(TenantId tenantId); - - boolean checkEntityImportLimit(TenantId tenantId); - -} diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java index 5f01d34877..9c5e9ae9b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java index 0229d95c18..2920331353 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,6 +55,10 @@ public class TenantApiUsageState extends BaseApiUsageState { return tenantProfileData.getConfiguration().getProfileThreshold(key); } + public boolean getProfileFeatureEnabled(ApiUsageRecordKey key) { + return tenantProfileData.getConfiguration().getProfileFeatureEnabled(key); + } + public long getProfileWarnThreshold(ApiUsageRecordKey key) { return tenantProfileData.getConfiguration().getWarnThreshold(key); } @@ -63,13 +67,18 @@ public class TenantApiUsageState extends BaseApiUsageState { ApiUsageStateValue featureValue = ApiUsageStateValue.ENABLED; for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.getKeys(feature)) { long value = get(recordKey); - long threshold = getProfileThreshold(recordKey); - long warnThreshold = getProfileWarnThreshold(recordKey); + boolean featureEnabled = getProfileFeatureEnabled(recordKey); ApiUsageStateValue tmpValue; - if (threshold == 0 || value == 0 || value < warnThreshold) { - tmpValue = ApiUsageStateValue.ENABLED; - } else if (value < threshold) { - tmpValue = ApiUsageStateValue.WARNING; + if (featureEnabled) { + long threshold = getProfileThreshold(recordKey); + long warnThreshold = getProfileWarnThreshold(recordKey); + if (threshold == 0 || value == 0 || value < warnThreshold) { + tmpValue = ApiUsageStateValue.ENABLED; + } else if (value < threshold) { + tmpValue = ApiUsageStateValue.WARNING; + } else { + tmpValue = ApiUsageStateValue.DISABLED; + } } else { tmpValue = ApiUsageStateValue.DISABLED; } diff --git a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java index 5142311da9..feca584258 100644 --- a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index 7a95bf78f9..1f5e41aeed 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.thingsboard.server.service.component; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +26,7 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeDefinition; import org.thingsboard.rule.engine.api.RuleNode; @@ -69,8 +69,6 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe private Map> edgeComponentsMap = new HashMap<>(); - private ObjectMapper mapper = new ObjectMapper(); - private boolean isInstall() { return environment.acceptsProfiles(Profiles.of("install")); } @@ -155,9 +153,10 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setScope(ruleNodeAnnotation.scope()); + scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode()); NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); - ObjectNode configurationDescriptor = mapper.createObjectNode(); - JsonNode node = mapper.valueToTree(nodeDefinition); + ObjectNode configurationDescriptor = JacksonUtil.newObjectNode(); + JsonNode node = JacksonUtil.valueToTree(nodeDefinition); configurationDescriptor.set("nodeDefinition", node); scannedComponent.setConfigurationDescriptor(configurationDescriptor); scannedComponent.setClazz(clazzName); @@ -194,7 +193,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe Class configClazz = nodeAnnotation.configClazz(); NodeConfiguration config = configClazz.getDeclaredConstructor().newInstance(); NodeConfiguration defaultConfiguration = config.defaultConfiguration(); - nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration)); + nodeDefinition.setDefaultConfiguration(JacksonUtil.valueToTree(defaultConfiguration)); nodeDefinition.setUiResources(nodeAnnotation.uiResources()); nodeDefinition.setConfigDirective(nodeAnnotation.configDirective()); nodeDefinition.setIcon(nodeAnnotation.icon()); diff --git a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java index 05dd8aa560..731009575d 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java index a397a4d0e9..ebc0fd0d24 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.device; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -67,7 +66,6 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { private static final String CLAIM_ATTRIBUTE_NAME = "claimingAllowed"; private static final String CLAIM_DATA_ATTRIBUTE_NAME = "claimingData"; - private static final ObjectMapper mapper = new ObjectMapper(); @Autowired private TbClusterService clusterService; @@ -90,35 +88,32 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { @Override public ListenableFuture registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs) { - ListenableFuture deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); - return Futures.transformAsync(deviceFuture, device -> { - Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); - List key = constructCacheKey(device.getId()); - - if (isAllowedClaimingByDefault) { - if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { - persistInCache(secretKey, durationMs, cache, key); - return Futures.immediateFuture(null); + Device device = deviceService.findDeviceById(tenantId, deviceId); + Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); + List key = constructCacheKey(device.getId()); + if (isAllowedClaimingByDefault) { + if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + persistInCache(secretKey, durationMs, cache, key); + return Futures.immediateFuture(null); + } + log.warn("The device [{}] has been already claimed!", device.getName()); + return Futures.immediateFailedFuture(new IllegalArgumentException()); + } else { + ListenableFuture> claimingAllowedFuture = attributesService.find(tenantId, device.getId(), + DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME)); + return Futures.transform(claimingAllowedFuture, list -> { + if (list != null && !list.isEmpty()) { + Optional claimingAllowedOptional = list.get(0).getBooleanValue(); + if (claimingAllowedOptional.isPresent() && claimingAllowedOptional.get() + && device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + persistInCache(secretKey, durationMs, cache, key); + return null; + } } - log.warn("The device [{}] has been already claimed!", device.getName()); + log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName()); throw new IllegalArgumentException(); - } else { - ListenableFuture> claimingAllowedFuture = attributesService.find(tenantId, device.getId(), - DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME)); - return Futures.transform(claimingAllowedFuture, list -> { - if (list != null && !list.isEmpty()) { - Optional claimingAllowedOptional = list.get(0).getBooleanValue(); - if (claimingAllowedOptional.isPresent() && claimingAllowedOptional.get() - && device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { - persistInCache(secretKey, durationMs, cache, key); - return null; - } - } - log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName()); - throw new IllegalArgumentException(); - }, MoreExecutors.directExecutor()); - } - }, MoreExecutors.directExecutor()); + }, MoreExecutors.directExecutor()); + } } private ListenableFuture getClaimData(Cache cache, Device device) { diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 7337497194..509e0fdbef 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfilePr import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration; import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -104,7 +105,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { protected Device saveEntity(SecurityUser user, Device entity, Map fields) { DeviceCredentials deviceCredentials; try { - deviceCredentials = createDeviceCredentials(fields); + deviceCredentials = createDeviceCredentials(entity.getTenantId(), entity.getId(), fields); deviceCredentialsService.formatCredentials(deviceCredentials); } catch (Exception e) { throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage()); @@ -136,7 +137,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { } @SneakyThrows - private DeviceCredentials createDeviceCredentials(Map fields) { + private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, Map fields) { DeviceCredentials credentials = new DeviceCredentials(); if (fields.containsKey(BulkImportColumnType.LWM2M_CLIENT_ENDPOINT)) { credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); @@ -147,7 +148,9 @@ public class DeviceBulkImportService extends AbstractBulkImportService { } else if (CollectionUtils.containsAny(fields.keySet(), EnumSet.of(BulkImportColumnType.MQTT_CLIENT_ID, BulkImportColumnType.MQTT_USER_NAME, BulkImportColumnType.MQTT_PASSWORD))) { credentials.setCredentialsType(DeviceCredentialsType.MQTT_BASIC); setUpBasicMqttCredentials(fields, credentials); - } else { + } else if (deviceId != null && !fields.containsKey(BulkImportColumnType.ACCESS_TOKEN)) { + credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceId); + } else { credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); setUpAccessTokenCredentials(fields, credentials); } diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index c88cbd8156..beecebe2ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,15 +20,16 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -36,15 +37,16 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.device.DeviceCredentialsService; -import org.thingsboard.server.dao.device.DeviceDao; -import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionFailedException; @@ -59,12 +61,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.state.DeviceStateService; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Service @@ -77,35 +80,56 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { private static final String DEVICE_PROVISION_STATE = "provisionState"; private static final String PROVISIONED_STATE = "provisioned"; - @Autowired - TbClusterService clusterService; + private final TbClusterService clusterService; + private final DeviceProfileService deviceProfileService; + private final DeviceService deviceService; + private final DeviceCredentialsService deviceCredentialsService; + private final AttributesService attributesService; + private final AuditLogService auditLogService; + private final PartitionService partitionService; - @Autowired - DeviceDao deviceDao; - - @Autowired - DeviceProfileDao deviceProfileDao; - - @Autowired - DeviceService deviceService; - - @Autowired - DeviceCredentialsService deviceCredentialsService; - - @Autowired - AttributesService attributesService; - - @Autowired - DeviceStateService deviceStateService; - - @Autowired - AuditLogService auditLogService; - - @Autowired - PartitionService partitionService; - - public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) { + public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider, TbClusterService clusterService, DeviceProfileService deviceProfileService, DeviceService deviceService, DeviceCredentialsService deviceCredentialsService, AttributesService attributesService, AuditLogService auditLogService, PartitionService partitionService) { ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); + this.clusterService = clusterService; + this.deviceProfileService = deviceProfileService; + this.deviceService = deviceService; + this.deviceCredentialsService = deviceCredentialsService; + this.attributesService = attributesService; + this.auditLogService = auditLogService; + this.partitionService = partitionService; + } + + @Override + public ProvisionResponse provisionDeviceViaX509Chain(DeviceProfile targetProfile, ProvisionRequest provisionRequest) throws ProvisionFailedException { + if (targetProfile == null) { + throw new ProvisionFailedException("Device profile is not specified!"); + } + if (!DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(targetProfile.getProfileData().getProvisionConfiguration().getType())) { + throw new ProvisionFailedException("Device profile provision strategy is not X509_CERTIFICATE_CHAIN!"); + } + X509CertificateChainProvisionConfiguration configuration = (X509CertificateChainProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration(); + String certificateValue = provisionRequest.getCredentialsData().getX509CertHash(); + String certificateRegEx = configuration.getCertificateRegExPattern(); + String commonName = getCNFromX509Certificate(targetProfile, certificateValue); + String deviceName = extractDeviceNameFromCNByRegEx(targetProfile, commonName, certificateRegEx); + provisionRequest.setDeviceName(deviceName); + Device targetDevice = deviceService.findDeviceByTenantIdAndName(targetProfile.getTenantId(), provisionRequest.getDeviceName()); + X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration(); + if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(targetDevice.getTenantId(), targetDevice.getId()); + if (DeviceCredentialsType.X509_CERTIFICATE.equals(deviceCredentials.getCredentialsType())) { + String updatedDeviceCertificateValue = provisionRequest.getCredentialsData().getX509CertHash(); + deviceCredentials = updateDeviceCredentials(targetDevice.getTenantId(), deviceCredentials, + updatedDeviceCertificateValue, DeviceCredentialsType.X509_CERTIFICATE); + } + return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS); + } else if (x509Configuration.isAllowCreateNewDevicesByX509Certificate()) { + return createDevice(provisionRequest, targetProfile); + } else { + log.warn("[{}][{}] Device with name {} doesn't exist and cannot be created due incorrect configuration for X509CertificateChainProvisionConfiguration", + targetProfile.getTenantId(), targetProfile.getId(), provisionRequest.getDeviceName()); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } } @Override @@ -124,14 +148,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); } - DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey); + DeviceProfile targetProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequestKey); if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null || targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) { throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); } - Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null); + Device targetDevice = deviceService.findDeviceByTenantIdAndName(targetProfile.getTenantId(), provisionRequest.getDeviceName()); switch (targetProfile.getProvisionType()) { case ALLOW_CREATE_NEW_DEVICES: @@ -155,6 +179,8 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { } } break; + case X509_CERTIFICATE_CHAIN: + throw new ProvisionFailedException("Invalid provision strategy type!"); } throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); } @@ -209,6 +235,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { } } + private DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials, String certificateValue, + DeviceCredentialsType credentialsType) { + log.trace("Updating device credentials [{}] with certificate value [{}]", deviceCredentials, certificateValue); + deviceCredentials.setCredentialsValue(certificateValue); + deviceCredentials.setCredentialsType(credentialsType); + return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + } + private ListenableFuture> saveProvisionStateAttribute(Device device) { return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), @@ -257,4 +291,27 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE; auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest); } + + private String getCNFromX509Certificate(DeviceProfile profile, String x509Value) { + try { + return SslUtil.parseCommonName(SslUtil.readCertFile(x509Value)); + } catch (Exception e) { + log.trace("[{}][{}] Failed to parse CN from X509 certificate {}", profile.getTenantId(), profile.getId(), x509Value); + return null; + } + } + + public String extractDeviceNameFromCNByRegEx(DeviceProfile profile, String commonName, String regex) throws ProvisionFailedException { + try { + log.trace("Extract device name from CN [{}] by regex pattern [{}]", commonName, regex); + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(commonName); + if (matcher.find()) { + return matcher.group(1); + } + } catch (Exception ignored) {} + log.trace("[{}][{}] Failed to match device name using [{}] from CN: [{}]", profile.getTenantId(), profile.getId(), regex, commonName); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index 1bb30d6960..110760acbc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.thingsboard.server.service.edge; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -23,6 +24,7 @@ import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EdgeUtils; @@ -39,22 +41,22 @@ import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.AlarmEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.AssetEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.AssetProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.CustomerEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.DashboardEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.DeviceEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.DeviceProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.EntityViewEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.OtaPackageEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.QueueEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.RelationEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.RuleChainEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.UserEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.WidgetBundleEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.WidgetTypeEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.asset.AssetProfileEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.customer.CustomerEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.device.DeviceProfileEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.edge.EdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -67,6 +69,8 @@ import java.util.concurrent.Executors; @Slf4j public class DefaultEdgeNotificationService implements EdgeNotificationService { + public static final String EDGE_IS_ROOT_BODY_KEY = "isRoot"; + @Autowired private EdgeService edgeService; @@ -142,7 +146,9 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { public Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws Exception { edge.setRootRuleChainId(ruleChainId); Edge savedEdge = edgeService.saveEdge(edge); - saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN, EdgeEventActionType.UPDATED, ruleChainId, null).get(); + ObjectNode isRootBody = JacksonUtil.newObjectNode(); + isRootBody.put(EDGE_IS_ROOT_BODY_KEY, Boolean.TRUE); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN, EdgeEventActionType.UPDATED, ruleChainId, isRootBody).get(); return savedEdge; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java index f8c23ece1e..03e249ca26 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 4adb73ebbb..474c7ba233 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,24 +40,24 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; import org.thingsboard.server.service.edge.rpc.constructor.EdgeMsgConstructor; -import org.thingsboard.server.service.edge.rpc.processor.AdminSettingsEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.AlarmEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.AssetEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.AssetProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.CustomerEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.DashboardEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.DeviceEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.DeviceProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.EntityViewEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.OtaPackageEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.QueueEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.RelationEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.RuleChainEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.TelemetryEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.UserEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.WidgetBundleEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.WidgetTypeEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.asset.AssetProfileEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.customer.CustomerEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.device.DeviceProfileEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.edge.EdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.settings.AdminSettingsEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.GrpcCallbackExecutorService; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java index ee36c80454..91de140959 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java new file mode 100644 index 0000000000..2c4a14635d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.instructions; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.InstallScripts; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class DefaultEdgeInstallService implements EdgeInstallService { + + private static final String EDGE_DIR = "edge"; + + private static final String EDGE_INSTALL_INSTRUCTIONS_DIR = "install_instructions"; + + private final InstallScripts installScripts; + + @Value("${edges.rpc.port}") + private int rpcPort; + + @Value("${edges.rpc.ssl.enabled}") + private boolean sslEnabled; + + @Value("${app.version:unknown}") + private String appVersion; + + @Override + public EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request) { + String dockerInstallInstructions = readFile(resolveFile("docker", "instructions.md")); + String baseUrl = request.getServerName(); + if (baseUrl.contains("localhost") || baseUrl.contains("127.0.0.1")) { + String localhostWarning = readFile(resolveFile("docker", "localhost_warning.md")); + dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", localhostWarning); + dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", "!!!REPLACE_ME_TO_HOST_IP_ADDRESS!!!"); + } else { + dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", ""); + dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl); + } + String edgeVersion = appVersion + "EDGE"; + edgeVersion = edgeVersion.replace("-SNAPSHOT", ""); + dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); + dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey()); + dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret()); + dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_PORT}", Integer.toString(rpcPort)); + dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_SSL_ENABLED}", Boolean.toString(sslEnabled)); + return new EdgeInstallInstructions(dockerInstallInstructions); + } + + private String readFile(Path file) { + try { + return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + } catch (IOException e) { + log.warn("Failed to read file: {}", file, e); + throw new RuntimeException(e); + } + } + + private Path resolveFile(String subDir, String... subDirs) { + return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs)); + } + + private Path getEdgeInstallInstructionsDir() { + return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, EDGE_INSTALL_INSTRUCTIONS_DIR); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java new file mode 100644 index 0000000000..2a030fc4fa --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.instructions; + +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; +import org.thingsboard.server.common.data.id.TenantId; + +import javax.servlet.http.HttpServletRequest; + +public interface EdgeInstallService { + + EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java index 81c3ca644b..1830cfe9d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index fea89d6336..bda510a762 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.edge.rpc; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import io.grpc.Server; @@ -25,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; @@ -35,6 +37,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; @@ -66,6 +71,10 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; +import static org.thingsboard.server.common.data.DataConstants.CONNECT_EVENT; +import static org.thingsboard.server.common.data.DataConstants.DISCONNECT_EVENT; +import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; + @Service @Slf4j @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @@ -89,11 +98,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private String privateKeyResource; @Value("${edges.state.persistToTelemetry:false}") private boolean persistToTelemetry; - @Value("${edges.rpc.client_max_keep_alive_time_sec}") + @Value("${edges.rpc.client_max_keep_alive_time_sec:1}") private int clientMaxKeepAliveTimeSec; @Value("${edges.rpc.max_inbound_message_size:4194304}") private int maxInboundMessageSize; - + @Value("${edges.rpc.keep_alive_time_sec:10}") + private int keepAliveTimeSec; + @Value("${edges.rpc.keep_alive_timeout_sec:5}") + private int keepAliveTimeoutSec; @Value("${edges.scheduler_pool_size}") private int schedulerPoolSize; @@ -122,6 +134,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i log.info("Initializing Edge RPC service!"); NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort) .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) + .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS) + .keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS) + .permitKeepAliveWithoutCalls(true) .maxInboundMessageSize(maxInboundMessageSize) .addService(this); if (sslEnabled) { @@ -174,7 +189,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @Override public StreamObserver handleMsgs(StreamObserver outputStream) { - return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService).getInputStream(); + return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService, this.maxInboundMessageSize).getInputStream(); } @Override @@ -261,7 +276,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i newEventLock.unlock(); } save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); - save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, System.currentTimeMillis()); + long lastConnectTs = System.currentTimeMillis(); + save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); + pushRuleEngineMessage(edgeGrpcSession.getEdge().getTenantId(), edgeId, lastConnectTs, CONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); scheduleEdgeEventsCheck(edgeGrpcSession); } @@ -271,7 +288,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i if (session != null) { boolean success = false; if (session.isConnected()) { - session.startSyncProcess(tenantId, edgeId, true); + session.startSyncProcess(true); success = true; } clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success)); @@ -365,7 +382,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void onEdgeDisconnect(EdgeId edgeId) { log.info("[{}] edge disconnected!", edgeId); - sessions.remove(edgeId); + EdgeGrpcSession removed = sessions.remove(edgeId); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); try { @@ -374,7 +391,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i newEventLock.unlock(); } save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); - save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, System.currentTimeMillis()); + long lastDisconnectTs = System.currentTimeMillis(); + save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs); + pushRuleEngineMessage(removed.getEdge().getTenantId(), edgeId, lastDisconnectTs, DISCONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); } @@ -423,4 +442,26 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i log.warn("[{}] Failed to update attribute [{}] with value [{}]", edgeId, key, value, t); } } + + private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, String msgType) { + try { + ObjectNode edgeState = JacksonUtil.newObjectNode(); + if (msgType.equals(CONNECT_EVENT)) { + edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true); + edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts); + } else { + edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, false); + edgeState.put(DefaultDeviceStateService.LAST_DISCONNECT_TIME, ts); + } + String data = JacksonUtil.toString(edgeState); + TbMsgMetaData md = new TbMsgMetaData(); + if (!persistToTelemetry) { + md.putValue(DataConstants.SCOPE, SERVER_SCOPE); + } + TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data); + clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push {}", tenantId, edgeId, msgType, e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 9571319226..33aed92682 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,16 +105,20 @@ public final class EdgeGrpcSession implements Closeable { private EdgeVersion edgeVersion; + private int maxInboundMessageSize; + private int clientMaxInboundMessageSize; + private ScheduledExecutorService sendDownlinkExecutorService; EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, BiConsumer sessionOpenListener, - Consumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService) { + Consumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) { this.sessionId = UUID.randomUUID(); this.ctx = ctx; this.outputStream = outputStream; this.sessionOpenListener = sessionOpenListener; this.sessionCloseListener = sessionCloseListener; this.sendDownlinkExecutorService = sendDownlinkExecutorService; + this.maxInboundMessageSize = maxInboundMessageSize; initInputStream(); } @@ -130,6 +134,10 @@ public final class EdgeGrpcSession implements Closeable { if (ConnectResponseCode.ACCEPTED != responseMsg.getResponseCode()) { outputStream.onError(new RuntimeException(responseMsg.getErrorMsg())); } else { + if (requestMsg.getConnectRequestMsg().hasMaxInboundMessageSize()) { + log.debug("[{}] Client max inbound message size: {}", sessionId, requestMsg.getConnectRequestMsg().getMaxInboundMessageSize()); + clientMaxInboundMessageSize = requestMsg.getConnectRequestMsg().getMaxInboundMessageSize(); + } connected = true; } } @@ -140,7 +148,7 @@ public final class EdgeGrpcSession implements Closeable { if (requestMsg.getSyncRequestMsg().hasFullSync()) { fullSync = requestMsg.getSyncRequestMsg().getFullSync(); } - startSyncProcess(edge.getTenantId(), edge.getId(), fullSync); + startSyncProcess(fullSync); } else { syncCompleted = true; } @@ -184,17 +192,19 @@ public final class EdgeGrpcSession implements Closeable { }; } - public void startSyncProcess(TenantId tenantId, EdgeId edgeId, boolean fullSync) { - log.trace("[{}][{}] Staring edge sync process", tenantId, edgeId); + public void startSyncProcess(boolean fullSync) { + log.trace("[{}][{}][{}] Staring edge sync process", edge.getTenantId(), edge.getId(), this.sessionId); syncCompleted = false; - interruptGeneralProcessingOnSync(tenantId, edgeId); + interruptGeneralProcessingOnSync(); doSync(new EdgeSyncCursor(ctx, edge, fullSync)); } private void doSync(EdgeSyncCursor cursor) { if (cursor.hasNext()) { - log.info("[{}][{}] starting sync process, cursor current idx = {}", edge.getTenantId(), edge.getId(), cursor.getCurrentIdx()); - ListenableFuture uuidListenableFuture = startProcessingEdgeEvents(cursor.getNext()); + EdgeEventFetcher next = cursor.getNext(); + log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}", + edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); + ListenableFuture uuidListenableFuture = startProcessingEdgeEvents(next); Futures.addCallback(uuidListenableFuture, new FutureCallback<>() { @Override public void onSuccess(@Nullable UUID result) { @@ -211,9 +221,9 @@ public final class EdgeGrpcSession implements Closeable { .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) .build(); - Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback() { + Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() { @Override - public void onSuccess(Void result) { + public void onSuccess(Boolean isInterrupted) { syncCompleted = true; ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId()); } @@ -262,7 +272,7 @@ public final class EdgeGrpcSession implements Closeable { } if (sessionState.getPendingMsgsMap().isEmpty()) { log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey()); - stopCurrentSendDownlinkMsgsTask(null); + stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e); @@ -357,14 +367,19 @@ public final class EdgeGrpcSession implements Closeable { if (isConnected() && !pageData.getData().isEmpty()) { log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); - Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback() { + Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { @Override - public void onSuccess(@Nullable Void tmp) { - if (isConnected() && pageData.hasNext()) { - processEdgeEvents(fetcher, pageLink.nextPageLink(), result); + public void onSuccess(@Nullable Boolean isInterrupted) { + if (Boolean.TRUE.equals(isInterrupted)) { + log.debug("[{}][{}][{}] Send downlink messages task was interrupted", edge.getTenantId(), edge.getId(), sessionId); + result.set(null); } else { - UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); - result.set(ifOffset); + if (isConnected() && pageData.hasNext()) { + processEdgeEvents(fetcher, pageLink.nextPageLink(), result); + } else { + UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); + result.set(ifOffset); + } } } @@ -384,7 +399,7 @@ public final class EdgeGrpcSession implements Closeable { } } - private ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { + private ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { interruptPreviousSendDownlinkMsgsTask(); sessionState.setSendDownlinkMsgsFuture(SettableFuture.create()); @@ -406,22 +421,31 @@ public final class EdgeGrpcSession implements Closeable { } log.trace("[{}] [{}] downlink msg(s) are going to be send.", this.sessionId, copy.size()); for (DownlinkMsg downlinkMsg : copy) { - sendDownlinkMsg(ResponseMsg.newBuilder() - .setDownlinkMsg(downlinkMsg) - .build()); + if (this.clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > this.clientMaxInboundMessageSize) { + log.error("[{}][{}][{}] Downlink msg size [{}] exceeds client max inbound message size [{}]. Skipping this message. " + + "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it." + + "Message {}", edge.getTenantId(), edge.getId(), this.sessionId, downlinkMsg.getSerializedSize(), + this.clientMaxInboundMessageSize, downlinkMsg); + sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); + } else { + sendDownlinkMsg(ResponseMsg.newBuilder() + .setDownlinkMsg(downlinkMsg) + .build()); + } } if (attempt < MAX_DOWNLINK_ATTEMPTS) { scheduleDownlinkMsgsPackSend(attempt + 1); } else { log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); - stopCurrentSendDownlinkMsgsTask(null); + stopCurrentSendDownlinkMsgsTask(false); } } else { - stopCurrentSendDownlinkMsgsTask(null); + stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { - stopCurrentSendDownlinkMsgsTask(e); + log.warn("[{}] Failed to send downlink msgs. Error msg {}", this.sessionId, e.getMessage(), e); + stopCurrentSendDownlinkMsgsTask(true); } }; @@ -455,7 +479,6 @@ public final class EdgeGrpcSession implements Closeable { case ASSIGNED_TO_CUSTOMER: case UNASSIGNED_FROM_CUSTOMER: case CREDENTIALS_REQUEST: - case ENTITY_MERGE_REQUEST: case RPC_CALL: downlinkMsg = convertEntityEventToDownlink(edgeEvent); log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg); @@ -524,7 +547,7 @@ public final class EdgeGrpcSession implements Closeable { case CUSTOMER: return ctx.getCustomerProcessor().convertCustomerEventToDownlink(edgeEvent); case RULE_CHAIN: - return ctx.getRuleChainProcessor().convertRuleChainEventToDownlink(edge, edgeEvent); + return ctx.getRuleChainProcessor().convertRuleChainEventToDownlink(edgeEvent); case RULE_CHAIN_METADATA: return ctx.getRuleChainProcessor().convertRuleChainMetadataEventToDownlink(edgeEvent, this.edgeVersion); case ALARM: @@ -554,27 +577,27 @@ public final class EdgeGrpcSession implements Closeable { try { if (uplinkMsg.getEntityDataCount() > 0) { for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) { - result.addAll(ctx.getTelemetryProcessor().processTelemetryFromEdge(edge.getTenantId(), entityData)); + result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData)); } } if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceFromEdge(edge.getTenantId(), edge, deviceUpdateMsg)); + result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg)); } } if (uplinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg : uplinkMsg.getDeviceCredentialsUpdateMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceCredentialsFromEdge(edge.getTenantId(), deviceCredentialsUpdateMsg)); + result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsg(edge.getTenantId(), deviceCredentialsUpdateMsg)); } } if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { - result.add(ctx.getAlarmProcessor().processAlarmFromEdge(edge.getTenantId(), alarmUpdateMsg)); + result.add(ctx.getAlarmProcessor().processAlarmMsg(edge.getTenantId(), alarmUpdateMsg)); } } if (uplinkMsg.getRelationUpdateMsgCount() > 0) { for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { - result.add(ctx.getRelationProcessor().processRelationFromEdge(edge.getTenantId(), relationUpdateMsg)); + result.add(ctx.getRelationProcessor().processRelationMsg(edge.getTenantId(), relationUpdateMsg)); } } if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { @@ -636,7 +659,9 @@ public final class EdgeGrpcSession implements Closeable { return ConnectResponseMsg.newBuilder() .setResponseCode(ConnectResponseCode.ACCEPTED) .setErrorMsg("") - .setConfiguration(ctx.getEdgeMsgConstructor().constructEdgeConfiguration(edge)).build(); + .setConfiguration(ctx.getEdgeMsgConstructor().constructEdgeConfiguration(edge)) + .setMaxInboundMessageSize(maxInboundMessageSize) + .build(); } return ConnectResponseMsg.newBuilder() .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) @@ -668,23 +693,18 @@ public final class EdgeGrpcSession implements Closeable { } private void interruptPreviousSendDownlinkMsgsTask() { - String msg = String.format("[%s] Previous send downlink future was not properly completed, stopping it now!", this.sessionId); - stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); + log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId); + stopCurrentSendDownlinkMsgsTask(true); } - private void interruptGeneralProcessingOnSync(TenantId tenantId, EdgeId edgeId) { - String msg = String.format("[%s][%s] Sync process started. General processing interrupted!", tenantId, edgeId); - stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); + private void interruptGeneralProcessingOnSync() { + log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", edge.getTenantId(), edge.getId(), this.sessionId); + stopCurrentSendDownlinkMsgsTask(true); } - public void stopCurrentSendDownlinkMsgsTask(Exception e) { + public void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) { if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) { - if (e != null) { - log.warn(e.getMessage(), e); - sessionState.getSendDownlinkMsgsFuture().setException(e); - } else { - sessionState.getSendDownlinkMsgsFuture().set(null); - } + sessionState.getSendDownlinkMsgsFuture().set(isInterrupted); } if (sessionState.getScheduledSendDownlinkTask() != null) { sessionState.getScheduledSendDownlinkTask().cancel(true); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java index 6c5515337e..d5315ae650 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java index 5a3d8dc0f8..09a7c5add8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,6 @@ import java.util.concurrent.ScheduledFuture; public class EdgeSessionState { private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); - private SettableFuture sendDownlinkMsgsFuture; + private SettableFuture sendDownlinkMsgsFuture; private ScheduledFuture scheduledSendDownlinkTask; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index e9232584be..bf3e804f7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.edge.rpc; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.edge.EdgeContextComponent; @@ -41,9 +42,9 @@ import java.util.NoSuchElementException; public class EdgeSyncCursor { - List fetchers = new LinkedList<>(); + private final List fetchers = new LinkedList<>(); - int currentIdx = 0; + private int currentIdx = 0; public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) { if (fullSync) { @@ -53,8 +54,10 @@ public class EdgeSyncCursor { fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); + Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); + fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { - fetchers.add(new CustomerEdgeEventFetcher()); + fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId())); fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java index ee33dd687b..79754b867c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package org.thingsboard.server.service.edge.rpc.constructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java index e8f70e56c4..69a83da0a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.thingsboard.server.service.edge.rpc.constructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; @@ -25,7 +26,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java index f8cff82897..86ffc6dd5c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java index ec71217955..f5b921a32b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,10 @@ public class AssetProfileMsgConstructor { if (assetProfile.getImage() != null) { builder.setImage(ByteString.copyFrom(assetProfile.getImage().getBytes(StandardCharsets.UTF_8))); } + if (assetProfile.getDefaultEdgeRuleChainId() != null) { + builder.setDefaultRuleChainIdMSB(assetProfile.getDefaultEdgeRuleChainId().getId().getMostSignificantBits()) + .setDefaultRuleChainIdLSB(assetProfile.getDefaultEdgeRuleChainId().getId().getLeastSignificantBits()); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java index 38eb35782c..0465dd5a94 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java index c61c5eb260..edfdefb603 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java index 522bcd3a4e..78b785a5d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ public class DeviceMsgConstructor { @Autowired private DataDecodingEncodingService dataDecodingEncodingService; - public DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device, String conflictName) { + public DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device) { DeviceUpdateMsg.Builder builder = DeviceUpdateMsg.newBuilder() .setMsgType(msgType) .setIdMSB(device.getId().getId().getMostSignificantBits()) @@ -66,8 +66,9 @@ public class DeviceMsgConstructor { builder.setFirmwareIdMSB(device.getFirmwareId().getId().getMostSignificantBits()) .setFirmwareIdLSB(device.getFirmwareId().getId().getLeastSignificantBits()); } - if (conflictName != null) { - builder.setConflictName(conflictName); + if (device.getSoftwareId() != null) { + builder.setSoftwareIdMSB(device.getSoftwareId().getId().getMostSignificantBits()) + .setSoftwareIdLSB(device.getSoftwareId().getId().getLeastSignificantBits()); } if (device.getDeviceData() != null) { builder.setDeviceDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(device.getDeviceData()))); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java index 7865adb292..d8e19ee854 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,18 @@ public class DeviceProfileMsgConstructor { builder.setFirmwareIdMSB(deviceProfile.getFirmwareId().getId().getMostSignificantBits()) .setFirmwareIdLSB(deviceProfile.getFirmwareId().getId().getLeastSignificantBits()); } + if (deviceProfile.getSoftwareId() != null) { + builder.setSoftwareIdMSB(deviceProfile.getSoftwareId().getId().getMostSignificantBits()) + .setSoftwareIdLSB(deviceProfile.getSoftwareId().getId().getLeastSignificantBits()); + } + if (deviceProfile.getDefaultEdgeRuleChainId() != null) { + builder.setDefaultRuleChainIdMSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getMostSignificantBits()) + .setDefaultRuleChainIdLSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getLeastSignificantBits()); + } + if (deviceProfile.getDefaultDashboardId() != null) { + builder.setDefaultDashboardIdMSB(deviceProfile.getDefaultDashboardId().getId().getMostSignificantBits()) + .setDefaultDashboardIdLSB(deviceProfile.getDefaultDashboardId().getId().getLeastSignificantBits()); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EdgeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EdgeMsgConstructor.java index 5501217578..3416240b2c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EdgeMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EdgeMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java index e2d3735910..242a15ba56 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java index d5aa79700d..235199d75e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/OtaPackageMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/OtaPackageMsgConstructor.java index 35a1a6e0ac..253305a5ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/OtaPackageMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/OtaPackageMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/QueueMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/QueueMsgConstructor.java index 85362ff70b..2644a5409b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/QueueMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/QueueMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java index c445dea37d..24786a4f7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java index 35ac760faa..8f91f5d19e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,13 +35,13 @@ import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMetadat @TbCoreComponent public class RuleChainMsgConstructor { - public RuleChainUpdateMsg constructRuleChainUpdatedMsg(RuleChainId edgeRootRuleChainId, UpdateMsgType msgType, RuleChain ruleChain) { + public RuleChainUpdateMsg constructRuleChainUpdatedMsg(UpdateMsgType msgType, RuleChain ruleChain, boolean isRoot) { RuleChainUpdateMsg.Builder builder = RuleChainUpdateMsg.newBuilder() .setMsgType(msgType) .setIdMSB(ruleChain.getId().getId().getMostSignificantBits()) .setIdLSB(ruleChain.getId().getId().getLeastSignificantBits()) .setName(ruleChain.getName()) - .setRoot(ruleChain.getId().equals(edgeRootRuleChainId)) + .setRoot(isRoot) .setDebugMode(ruleChain.isDebugMode()) .setConfiguration(JacksonUtil.toString(ruleChain.getConfiguration())); if (ruleChain.getFirstRuleNodeId() != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java index 625b555e24..fbd3b31688 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java index ef95577d0d..a574b8bb4b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java index 33ce6e613d..36e43b9579 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java index 8fb32926f5..28c1b1c458 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructor.java index 014023cb60..51b7380e48 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorFactory.java index 5fc686f86a..1c581267c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorFactory.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV330.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV330.java index 3172ee9277..5e1310a5a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV330.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV330.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV340.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV340.java index 9acb2ae9f5..6fe39942ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV340.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV340.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java index b7c30fde64..cefeb4c46a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java index efade1b235..d2189b22a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java index ead87638fb..8aeff9954d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java index 5d7a5cb231..0ec5878695 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.SortOrder; import java.util.ArrayList; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java index 6791ba69a6..0049426f02 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java index 709c438739..05e7427121 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java index de88c2f05b..ace25dc7f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -33,6 +34,8 @@ import java.util.List; @AllArgsConstructor public class CustomerEdgeEventFetcher implements EdgeEventFetcher { + private final CustomerId customerId; + @Override public PageLink getPageLink(int pageSize) { return null; @@ -42,7 +45,7 @@ public class CustomerEdgeEventFetcher implements EdgeEventFetcher { public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { List result = new ArrayList<>(); result.add(EdgeUtils.constructEdgeEvent(edge.getTenantId(), edge.getId(), - EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, edge.getCustomerId(), null)); + EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null)); // @voba - returns PageData object to be in sync with other fetchers return new PageData<>(result, 1, result.size(), false); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java index 2cc923d08a..4159cdc0f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java index a6e7b3307c..81f508cd44 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java index ef4a5ac9ea..8c25f973f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java index fe896dbb79..90decaec26 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EdgeEventFetcher.java index f2a026f4c2..23d2a866fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java index 3bc4befdbe..5ec9c52f19 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java index ed5e039b62..327184e6a9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java index 14b8bac3f4..a2a25ada15 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/QueuesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/QueuesEdgeEventFetcher.java index a47dfa50a1..76e09d2e5f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/QueuesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/QueuesEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java index 8d2a8adc51..2caa8fde3b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,10 @@ */ package org.thingsboard.server.service.edge.rpc.fetch; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; @@ -28,6 +30,8 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.rule.RuleChainService; +import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY; + @Slf4j @AllArgsConstructor public class RuleChainsEdgeEventFetcher extends BasePageableEdgeEventFetcher { @@ -41,7 +45,13 @@ public class RuleChainsEdgeEventFetcher extends BasePageableEdgeEventFetcher processAlarmFromEdge(TenantId tenantId, AlarmUpdateMsg alarmUpdateMsg) { - log.trace("[{}] onAlarmUpdate [{}]", tenantId, alarmUpdateMsg); - EntityId originatorId = getAlarmOriginator(tenantId, alarmUpdateMsg.getOriginatorName(), - EntityType.valueOf(alarmUpdateMsg.getOriginatorType())); - if (originatorId == null) { - log.warn("Originator not found for the alarm msg {}", alarmUpdateMsg); - return Futures.immediateFuture(null); - } - try { - Alarm existentAlarm = alarmService.findLatestByOriginatorAndType(tenantId, originatorId, alarmUpdateMsg.getType()).get(); - switch (alarmUpdateMsg.getMsgType()) { - case ENTITY_CREATED_RPC_MESSAGE: - case ENTITY_UPDATED_RPC_MESSAGE: - if (existentAlarm == null || existentAlarm.getStatus().isCleared()) { - existentAlarm = new Alarm(); - existentAlarm.setTenantId(tenantId); - existentAlarm.setType(alarmUpdateMsg.getName()); - existentAlarm.setOriginator(originatorId); - existentAlarm.setSeverity(AlarmSeverity.valueOf(alarmUpdateMsg.getSeverity())); - existentAlarm.setStartTs(alarmUpdateMsg.getStartTs()); - existentAlarm.setClearTs(alarmUpdateMsg.getClearTs()); - existentAlarm.setPropagate(alarmUpdateMsg.getPropagate()); - } - existentAlarm.setStatus(AlarmStatus.valueOf(alarmUpdateMsg.getStatus())); - existentAlarm.setAckTs(alarmUpdateMsg.getAckTs()); - existentAlarm.setEndTs(alarmUpdateMsg.getEndTs()); - existentAlarm.setDetails(JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails())); - alarmService.createOrUpdateAlarm(existentAlarm); - break; - case ALARM_ACK_RPC_MESSAGE: - if (existentAlarm != null) { - alarmService.ackAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); - } - break; - case ALARM_CLEAR_RPC_MESSAGE: - if (existentAlarm != null) { - alarmService.clearAlarm(tenantId, existentAlarm.getId(), - JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails()), alarmUpdateMsg.getAckTs()); - } - break; - case ENTITY_DELETED_RPC_MESSAGE: - if (existentAlarm != null) { - alarmService.deleteAlarm(tenantId, existentAlarm.getId()); - } - break; - } - return Futures.immediateFuture(null); - } catch (Exception e) { - log.error("Failed to process alarm update msg [{}]", alarmUpdateMsg, e); - return Futures.immediateFailedFuture(new RuntimeException("Failed to process alarm update msg", e)); - } - } - - private EntityId getAlarmOriginator(TenantId tenantId, String entityName, EntityType entityType) { - switch (entityType) { - case DEVICE: - return deviceService.findDeviceByTenantIdAndName(tenantId, entityName).getId(); - case ASSET: - return assetService.findAssetByTenantIdAndName(tenantId, entityName).getId(); - case ENTITY_VIEW: - return entityViewService.findEntityViewByTenantIdAndName(tenantId, entityName).getId(); - default: - return null; - } - } - - public DownlinkMsg convertAlarmEventToDownlink(EdgeEvent edgeEvent) { - AlarmId alarmId = new AlarmId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ALARM_ACK: - case ALARM_CLEAR: - try { - Alarm alarm = alarmService.findAlarmByIdAsync(edgeEvent.getTenantId(), alarmId).get(); - if (alarm != null) { - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addAlarmUpdateMsg(alarmMsgConstructor.constructAlarmUpdatedMsg(edgeEvent.getTenantId(), msgType, alarm)) - .build(); - } - } catch (Exception e) { - log.error("Can't process alarm msg [{}] [{}]", edgeEvent, msgType, e); - } - break; - case DELETED: - Alarm alarm = JacksonUtil.OBJECT_MAPPER.convertValue(edgeEvent.getBody(), Alarm.class); - AlarmUpdateMsg alarmUpdateMsg = - alarmMsgConstructor.constructAlarmUpdatedMsg(edgeEvent.getTenantId(), msgType, alarm); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addAlarmUpdateMsg(alarmUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processAlarmNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) throws JsonProcessingException { - EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); - AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); - switch (actionType) { - case DELETED: - EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); - Alarm deletedAlarm = JacksonUtil.OBJECT_MAPPER.readValue(edgeNotificationMsg.getBody(), Alarm.class); - return saveEdgeEvent(tenantId, edgeId, EdgeEventType.ALARM, actionType, alarmId, JacksonUtil.OBJECT_MAPPER.valueToTree(deletedAlarm)); - default: - ListenableFuture alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId); - return Futures.transformAsync(alarmFuture, alarm -> { - if (alarm == null) { - return Futures.immediateFuture(null); - } - EdgeEventType type = EdgeUtils.getEdgeEventTypeByEntityType(alarm.getOriginator().getEntityType()); - if (type == null) { - return Futures.immediateFuture(null); - } - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; - List> futures = new ArrayList<>(); - do { - pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, alarm.getOriginator(), pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - for (EdgeId relatedEdgeId : pageData.getData()) { - futures.add(saveEdgeEvent(tenantId, - relatedEdgeId, - EdgeEventType.ALARM, - EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()), - alarmId, - null)); - } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } - } while (pageData != null && pageData.hasNext()); - return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); - }, dbCallbackExecutorService); - } - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetEdgeProcessor.java deleted file mode 100644 index 8f03523034..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetEdgeProcessor.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.asset.AssetProfile; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class AssetEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent) { - AssetId assetId = new AssetId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: - Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId); - if (asset != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - AssetUpdateMsg assetUpdateMsg = - assetMsgConstructor.constructAssetUpdatedMsg(msgType, asset); - DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addAssetUpdateMsg(assetUpdateMsg); - if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { - AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); - builder.addAssetProfileUpdateMsg(assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile)); - } - downlinkMsg = builder.build(); - } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: - AssetUpdateMsg assetUpdateMsg = - assetMsgConstructor.constructAssetDeleteMsg(assetId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addAssetUpdateMsg(assetUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processAssetNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetProfileEdgeProcessor.java deleted file mode 100644 index ce17c050b3..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetProfileEdgeProcessor.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.asset.AssetProfile; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class AssetProfileEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent) { - AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId); - if (assetProfile != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - AssetProfileUpdateMsg assetProfileUpdateMsg = - assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addAssetProfileUpdateMsg(assetProfileUpdateMsg) - .build(); - } - break; - case DELETED: - AssetProfileUpdateMsg assetProfileUpdateMsg = - assetProfileMsgConstructor.constructAssetProfileDeleteMsg(assetProfileId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addAssetProfileUpdateMsg(assetProfileUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processAssetProfileNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 3b39520606..ac7c791338 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,10 +97,14 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; @Slf4j public abstract class BaseEdgeProcessor { + protected static final Lock deviceCreationLock = new ReentrantLock(); + protected static final int DEFAULT_PAGE_SIZE = 100; @Autowired @@ -306,6 +310,12 @@ public abstract class BaseEdgeProcessor { return futures; } + protected ListenableFuture handleUnsupportedMsgType(UpdateMsgType msgType) { + String errMsg = String.format("Unsupported msg type %s", msgType); + log.error(errMsg); + return Futures.immediateFailedFuture(new RuntimeException(errMsg)); + } + protected UpdateMsgType getUpdateMsgType(EdgeEventActionType actionType) { switch (actionType) { case UPDATED: @@ -462,4 +472,40 @@ public abstract class BaseEdgeProcessor { return null; } } + + protected UUID safeGetUUID(long mSB, long lSB) { + return mSB != 0 && lSB != 0 ? new UUID(mSB, lSB) : null; + } + + protected CustomerId safeGetCustomerId(long mSB, long lSB) { + CustomerId customerId = null; + UUID customerUUID = safeGetUUID(mSB, lSB); + if (customerUUID != null) { + customerId = new CustomerId(customerUUID); + } + return customerId; + } + + protected boolean isEntityExists(TenantId tenantId, EntityId entityId) { + switch (entityId.getEntityType()) { + case TENANT: + return tenantService.findTenantById(tenantId) != null; + case DEVICE: + return deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())) != null; + case ASSET: + return assetService.findAssetById(tenantId, new AssetId(entityId.getId())) != null; + case ENTITY_VIEW: + return entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())) != null; + case CUSTOMER: + return customerService.findCustomerById(tenantId, new CustomerId(entityId.getId())) != null; + case USER: + return userService.findUserById(tenantId, new UserId(entityId.getId())) != null; + case DASHBOARD: + return dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId())) != null; + case EDGE: + return edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())) != null; + default: + return false; + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/CustomerEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/CustomerEdgeProcessor.java deleted file mode 100644 index 7d720fa671..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/CustomerEdgeProcessor.java +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Component -@Slf4j -@TbCoreComponent -public class CustomerEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertCustomerEventToDownlink(EdgeEvent edgeEvent) { - CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - Customer customer = customerService.findCustomerById(edgeEvent.getTenantId(), customerId); - if (customer != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - CustomerUpdateMsg customerUpdateMsg = - customerMsgConstructor.constructCustomerUpdatedMsg(msgType, customer); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addCustomerUpdateMsg(customerUpdateMsg) - .build(); - } - break; - case DELETED: - CustomerUpdateMsg customerUpdateMsg = - customerMsgConstructor.constructCustomerDeleteMsg(customerId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addCustomerUpdateMsg(customerUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processCustomerNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); - EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); - UUID uuid = new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()); - CustomerId customerId = new CustomerId(EntityIdFactory.getByEdgeEventTypeAndUuid(type, uuid).getId()); - switch (actionType) { - case UPDATED: - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; - List> futures = new ArrayList<>(); - do { - pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - for (Edge edge : pageData.getData()) { - futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null)); - } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } - } while (pageData != null && pageData.hasNext()); - return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); - case DELETED: - EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); - return saveEdgeEvent(tenantId, edgeId, type, actionType, customerId, null); - default: - return Futures.immediateFuture(null); - } - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DashboardEdgeProcessor.java deleted file mode 100644 index 283df6954e..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DashboardEdgeProcessor.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class DashboardEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertDashboardEventToDownlink(EdgeEvent edgeEvent) { - DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: - Dashboard dashboard = dashboardService.findDashboardById(edgeEvent.getTenantId(), dashboardId); - if (dashboard != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - DashboardUpdateMsg dashboardUpdateMsg = - dashboardMsgConstructor.constructDashboardUpdatedMsg(msgType, dashboard); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDashboardUpdateMsg(dashboardUpdateMsg) - .build(); - } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: - DashboardUpdateMsg dashboardUpdateMsg = - dashboardMsgConstructor.constructDashboardDeleteMsg(dashboardId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDashboardUpdateMsg(dashboardUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processDashboardNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java deleted file mode 100644 index 81e7970611..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ /dev/null @@ -1,502 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.datastax.oss.driver.api.core.uuid.Uuids; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.device.data.DeviceData; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.rpc.RpcError; -import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.data.security.DeviceCredentialsType; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgDataType; -import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; -import org.thingsboard.server.common.msg.session.SessionMsgType; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; -import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; -import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.server.queue.TbQueueMsgMetadata; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; - -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.locks.ReentrantLock; - -@Component -@Slf4j -@TbCoreComponent -public class DeviceEdgeProcessor extends BaseEdgeProcessor { - - @Autowired - private DataDecodingEncodingService dataDecodingEncodingService; - - private static final ReentrantLock deviceCreationLock = new ReentrantLock(); - - public ListenableFuture processDeviceFromEdge(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { - log.trace("[{}] onDeviceUpdate [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); - switch (deviceUpdateMsg.getMsgType()) { - case ENTITY_CREATED_RPC_MESSAGE: - String deviceName = deviceUpdateMsg.getName(); - Device device = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); - if (device != null) { - boolean deviceAlreadyExistsForThisEdge = isDeviceAlreadyExistsOnCloudForThisEdge(tenantId, edge, device); - if (deviceAlreadyExistsForThisEdge) { - log.info("[{}] Device with name '{}' already exists on the cloud, and related to this edge [{}]. " + - "deviceUpdateMsg [{}], Updating device", tenantId, deviceName, edge.getId(), deviceUpdateMsg); - return updateDevice(tenantId, edge, deviceUpdateMsg); - } else { - log.info("[{}] Device with name '{}' already exists on the cloud, but not related to this edge [{}]. deviceUpdateMsg [{}]." + - "Creating a new device with random prefix and relate to this edge", tenantId, deviceName, edge.getId(), deviceUpdateMsg); - String newDeviceName = deviceUpdateMsg.getName() + "_" + StringUtils.randomAlphabetic(15); - Device newDevice; - try { - newDevice = createDevice(tenantId, edge, deviceUpdateMsg, newDeviceName); - } catch (DataValidationException e) { - log.error("[{}] Device update msg can't be processed due to data validation [{}]", tenantId, deviceUpdateMsg, e); - return Futures.immediateFuture(null); - } - ObjectNode body = JacksonUtil.OBJECT_MAPPER.createObjectNode(); - body.put("conflictName", deviceName); - ListenableFuture input = saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.ENTITY_MERGE_REQUEST, newDevice.getId(), body); - return Futures.transformAsync(input, unused -> - saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, newDevice.getId(), null), - dbCallbackExecutorService); - } - } else { - log.info("[{}] Creating new device on the cloud [{}]", tenantId, deviceUpdateMsg); - try { - device = createDevice(tenantId, edge, deviceUpdateMsg, deviceUpdateMsg.getName()); - } catch (DataValidationException e) { - log.error("[{}] Device update msg can't be processed due to data validation [{}]", tenantId, deviceUpdateMsg, e); - return Futures.immediateFuture(null); - } - return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, device.getId(), null); - } - case ENTITY_UPDATED_RPC_MESSAGE: - return updateDevice(tenantId, edge, deviceUpdateMsg); - case ENTITY_DELETED_RPC_MESSAGE: - DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); - Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId); - if (deviceToDelete != null) { - deviceService.unassignDeviceFromEdge(tenantId, deviceId, edge.getId()); - } - return Futures.immediateFuture(null); - case UNRECOGNIZED: - default: - log.error("Unsupported msg type {}", deviceUpdateMsg.getMsgType()); - return Futures.immediateFailedFuture(new RuntimeException("Unsupported msg type " + deviceUpdateMsg.getMsgType())); - } - } - - private boolean isDeviceAlreadyExistsOnCloudForThisEdge(TenantId tenantId, Edge edge, Device device) { - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; - do { - pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, device.getId(), pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - if (pageData.getData().contains(edge.getId())) { - return true; - } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } - } while (pageData != null && pageData.hasNext()); - return false; - } - - public ListenableFuture processDeviceCredentialsFromEdge(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { - log.debug("Executing onDeviceCredentialsUpdate, deviceCredentialsUpdateMsg [{}]", deviceCredentialsUpdateMsg); - DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB())); - ListenableFuture deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); - return Futures.transform(deviceFuture, device -> { - if (device != null) { - log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", - device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); - try { - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); - deviceCredentials.setCredentialsType(DeviceCredentialsType.valueOf(deviceCredentialsUpdateMsg.getCredentialsType())); - deviceCredentials.setCredentialsId(deviceCredentialsUpdateMsg.getCredentialsId()); - if (deviceCredentialsUpdateMsg.hasCredentialsValue()) { - deviceCredentials.setCredentialsValue(deviceCredentialsUpdateMsg.getCredentialsValue()); - } - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); - } catch (Exception e) { - log.error("Can't update device credentials for device [{}], deviceCredentialsUpdateMsg [{}]", device.getName(), deviceCredentialsUpdateMsg, e); - throw new RuntimeException(e); - } - } - return null; - }, dbCallbackExecutorService); - } - - - private ListenableFuture updateDevice(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { - DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); - Device device = deviceService.findDeviceById(tenantId, deviceId); - if (device != null) { - device.setName(deviceUpdateMsg.getName()); - device.setType(deviceUpdateMsg.getType()); - if (deviceUpdateMsg.hasLabel()) { - device.setLabel(deviceUpdateMsg.getLabel()); - } - if (deviceUpdateMsg.hasAdditionalInfo()) { - device.setAdditionalInfo(JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo())); - } - if (deviceUpdateMsg.hasDeviceProfileIdMSB() && deviceUpdateMsg.hasDeviceProfileIdLSB()) { - DeviceProfileId deviceProfileId = new DeviceProfileId( - new UUID(deviceUpdateMsg.getDeviceProfileIdMSB(), - deviceUpdateMsg.getDeviceProfileIdLSB())); - device.setDeviceProfileId(deviceProfileId); - } - device.setCustomerId(getCustomerId(deviceUpdateMsg)); - Optional deviceDataOpt = - dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); - if (deviceDataOpt.isPresent()) { - device.setDeviceData(deviceDataOpt.get()); - } - Device savedDevice = deviceService.saveDevice(device); - tbClusterService.onDeviceUpdated(savedDevice, device, false); - return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null); - } else { - String errMsg = String.format("[%s] can't find device [%s], edge [%s]", tenantId, deviceUpdateMsg, edge.getId()); - log.warn(errMsg); - return Futures.immediateFailedFuture(new RuntimeException(errMsg)); - } - } - - private Device createDevice(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg, String deviceName) { - Device device; - deviceCreationLock.lock(); - try { - log.debug("[{}] Creating device entity [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); - DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); - device = deviceService.findDeviceById(tenantId, deviceId); - boolean created = false; - if (device == null) { - device = new Device(); - device.setTenantId(tenantId); - device.setCreatedTime(Uuids.unixTimestamp(deviceId.getId())); - created = true; - } - device.setName(deviceName); - device.setType(deviceUpdateMsg.getType()); - if (deviceUpdateMsg.hasLabel()) { - device.setLabel(deviceUpdateMsg.getLabel()); - } - if (deviceUpdateMsg.hasAdditionalInfo()) { - device.setAdditionalInfo(JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo())); - } - if (deviceUpdateMsg.hasDeviceProfileIdMSB() && deviceUpdateMsg.hasDeviceProfileIdLSB()) { - DeviceProfileId deviceProfileId = new DeviceProfileId( - new UUID(deviceUpdateMsg.getDeviceProfileIdMSB(), - deviceUpdateMsg.getDeviceProfileIdLSB())); - device.setDeviceProfileId(deviceProfileId); - } - device.setCustomerId(getCustomerId(deviceUpdateMsg)); - Optional deviceDataOpt = - dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); - if (deviceDataOpt.isPresent()) { - device.setDeviceData(deviceDataOpt.get()); - } - if (created) { - deviceValidator.validate(device, Device::getTenantId); - device.setId(deviceId); - } else { - deviceValidator.validate(device, Device::getTenantId); - } - Device savedDevice = deviceService.saveDevice(device, false); - tbClusterService.onDeviceUpdated(savedDevice, created ? null : device, false); - if (created) { - DeviceCredentials deviceCredentials = new DeviceCredentials(); - deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); - deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); - deviceCredentials.setCredentialsId(StringUtils.randomAlphanumeric(20)); - deviceCredentialsService.createDeviceCredentials(device.getTenantId(), deviceCredentials); - } - createRelationFromEdge(tenantId, edge.getId(), device.getId()); - pushDeviceCreatedEventToRuleEngine(tenantId, edge, device); - deviceService.assignDeviceToEdge(edge.getTenantId(), device.getId(), edge.getId()); - } finally { - deviceCreationLock.unlock(); - } - return device; - } - - private CustomerId getCustomerId(DeviceUpdateMsg deviceUpdateMsg) { - if (deviceUpdateMsg.hasCustomerIdMSB() && deviceUpdateMsg.hasCustomerIdLSB()) { - return new CustomerId(new UUID(deviceUpdateMsg.getCustomerIdMSB(), deviceUpdateMsg.getCustomerIdLSB())); - } else { - return null; - } - } - - private void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { - EntityRelation relation = new EntityRelation(); - relation.setFrom(edgeId); - relation.setTo(entityId); - relation.setTypeGroup(RelationTypeGroup.COMMON); - relation.setType(EntityRelation.EDGE_TYPE); - relationService.saveRelation(tenantId, relation); - } - - private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, Device device) { - try { - DeviceId deviceId = device.getId(); - ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device); - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, device.getCustomerId(), - getActionTbMsgMetaData(edge, device.getCustomerId()), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); - tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", device); - } - - @Override - public void onFailure(Throwable t) { - log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); - } - }); - } catch (JsonProcessingException | IllegalArgumentException e) { - log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e); - } - } - - private TbMsgMetaData getActionTbMsgMetaData(Edge edge, CustomerId customerId) { - TbMsgMetaData metaData = getTbMsgMetaData(edge); - if (customerId != null && !customerId.isNullUid()) { - metaData.putValue("customerId", customerId.toString()); - } - return metaData; - } - - private TbMsgMetaData getTbMsgMetaData(Edge edge) { - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("edgeId", edge.getId().toString()); - metaData.putValue("edgeName", edge.getName()); - return metaData; - } - - public ListenableFuture processDeviceRpcCallFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) { - log.trace("[{}] processDeviceRpcCallFromEdge [{}]", tenantId, deviceRpcCallMsg); - if (deviceRpcCallMsg.hasResponseMsg()) { - return processDeviceRpcResponseFromEdge(tenantId, deviceRpcCallMsg); - } else if (deviceRpcCallMsg.hasRequestMsg()) { - return processDeviceRpcRequestFromEdge(tenantId, edge, deviceRpcCallMsg); - } - return Futures.immediateFuture(null); - } - - private ListenableFuture processDeviceRpcResponseFromEdge(TenantId tenantId, DeviceRpcCallMsg deviceRpcCallMsg) { - SettableFuture futureToSet = SettableFuture.create(); - UUID requestUuid = new UUID(deviceRpcCallMsg.getRequestUuidMSB(), deviceRpcCallMsg.getRequestUuidLSB()); - DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB())); - - FromDeviceRpcResponse response; - if (!StringUtils.isEmpty(deviceRpcCallMsg.getResponseMsg().getError())) { - response = new FromDeviceRpcResponse(requestUuid, null, RpcError.valueOf(deviceRpcCallMsg.getResponseMsg().getError())); - } else { - response = new FromDeviceRpcResponse(requestUuid, deviceRpcCallMsg.getResponseMsg().getResponse(), null); - } - TbQueueCallback callback = new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - futureToSet.set(null); - } - - @Override - public void onFailure(Throwable t) { - log.error("Can't process push notification to core [{}]", deviceRpcCallMsg, t); - futureToSet.setException(t); - } - }; - FromDeviceRpcResponseActorMsg msg = - new FromDeviceRpcResponseActorMsg(deviceRpcCallMsg.getRequestId(), - tenantId, - deviceId, response); - tbClusterService.pushMsgToCore(msg, callback); - return futureToSet; - } - - private ListenableFuture processDeviceRpcRequestFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) { - DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB())); - try { - TbMsgMetaData metaData = new TbMsgMetaData(); - String requestId = Integer.toString(deviceRpcCallMsg.getRequestId()); - metaData.putValue("requestId", requestId); - metaData.putValue("serviceId", deviceRpcCallMsg.getServiceId()); - metaData.putValue("sessionId", deviceRpcCallMsg.getSessionId()); - metaData.putValue(DataConstants.EDGE_ID, edge.getId().toString()); - Device device = deviceService.findDeviceById(tenantId, deviceId); - if (device != null) { - metaData.putValue("deviceName", device.getName()); - metaData.putValue("deviceType", device.getType()); - metaData.putValue(DataConstants.DEVICE_ID, deviceId.getId().toString()); - } - ObjectNode data = JacksonUtil.OBJECT_MAPPER.createObjectNode(); - data.put("method", deviceRpcCallMsg.getRequestMsg().getMethod()); - data.put("params", deviceRpcCallMsg.getRequestMsg().getParams()); - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, null, metaData, - TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(data)); - tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - log.debug("Successfully send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", - device, deviceRpcCallMsg); - } - - @Override - public void onFailure(Throwable t) { - log.debug("Failed to send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", - device, deviceRpcCallMsg, t); - } - }); - } catch (JsonProcessingException | IllegalArgumentException e) { - log.warn("[{}] Failed to push TO_SERVER_RPC_REQUEST to rule engine. deviceRpcCallMsg {}", deviceId, deviceRpcCallMsg, e); - } - - return Futures.immediateFuture(null); - } - - public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent) { - DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: - Device device = deviceService.findDeviceById(edgeEvent.getTenantId(), deviceId); - if (device != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - DeviceUpdateMsg deviceUpdateMsg = - deviceMsgConstructor.constructDeviceUpdatedMsg(msgType, device, null); - DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceUpdateMsg(deviceUpdateMsg); - if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { - DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); - builder.addDeviceProfileUpdateMsg(deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); - } - downlinkMsg = builder.build(); - } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: - DeviceUpdateMsg deviceUpdateMsg = - deviceMsgConstructor.constructDeviceDeleteMsg(deviceId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceUpdateMsg(deviceUpdateMsg) - .build(); - break; - case CREDENTIALS_UPDATED: - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); - if (deviceCredentials != null) { - DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = - deviceMsgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg) - .build(); - } - break; - case RPC_CALL: - return convertRpcCallEventToDownlink(edgeEvent); - case CREDENTIALS_REQUEST: - return convertCredentialsRequestEventToDownlink(edgeEvent); - case ENTITY_MERGE_REQUEST: - return convertEntityMergeRequestEventToDownlink(edgeEvent); - } - return downlinkMsg; - } - - private DownlinkMsg convertRpcCallEventToDownlink(EdgeEvent edgeEvent) { - log.trace("Executing convertRpcCallEventToDownlink, edgeEvent [{}]", edgeEvent); - return DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceRpcCallMsg(deviceMsgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) - .build(); - } - - private DownlinkMsg convertCredentialsRequestEventToDownlink(EdgeEvent edgeEvent) { - DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); - DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = DeviceCredentialsRequestMsg.newBuilder() - .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) - .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) - .build(); - DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg); - return builder.build(); - } - - public DownlinkMsg convertEntityMergeRequestEventToDownlink(EdgeEvent edgeEvent) { - DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); - Device device = deviceService.findDeviceById(edgeEvent.getTenantId(), deviceId); - String conflictName = null; - if(edgeEvent.getBody() != null) { - conflictName = edgeEvent.getBody().get("conflictName").asText(); - } - DeviceUpdateMsg deviceUpdateMsg = deviceMsgConstructor - .constructDeviceUpdatedMsg(UpdateMsgType.ENTITY_MERGE_RPC_MESSAGE, device, conflictName); - return DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceUpdateMsg(deviceUpdateMsg) - .build(); - } - - public ListenableFuture processDeviceNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProfileEdgeProcessor.java deleted file mode 100644 index d5d36ee640..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProfileEdgeProcessor.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class DeviceProfileEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent) { - DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); - if (deviceProfile != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - DeviceProfileUpdateMsg deviceProfileUpdateMsg = - deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) - .build(); - } - break; - case DELETED: - DeviceProfileUpdateMsg deviceProfileUpdateMsg = - deviceProfileMsgConstructor.constructDeviceProfileDeleteMsg(deviceProfileId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processDeviceProfileNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java deleted file mode 100644 index d4f6998926..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Component -@Slf4j -@TbCoreComponent -public class EdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { - EdgeId edgeId = new EdgeId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: - Edge edge = edgeService.findEdgeById(edgeEvent.getTenantId(), edgeId); - if (edge != null) { - EdgeConfiguration edgeConfigMsg = - edgeMsgConstructor.constructEdgeConfiguration(edge); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .setEdgeConfiguration(edgeConfigMsg) - .build(); - } - break; - } - return downlinkMsg; - } - - public ListenableFuture processEdgeNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - try { - EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); - EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); - switch (actionType) { - case ASSIGNED_TO_CUSTOMER: - CustomerId customerId = JacksonUtil.OBJECT_MAPPER.readValue(edgeNotificationMsg.getBody(), CustomerId.class); - Edge edge = edgeService.findEdgeById(tenantId, edgeId); - if (edge == null || customerId.isNullUid()) { - return Futures.immediateFuture(null); - } - List> futures = new ArrayList<>(); - futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null)); - futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.EDGE, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, edgeId, null)); - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; - do { - pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - log.trace("[{}] [{}] user(s) are going to be added to edge.", edge.getId(), pageData.getData().size()); - for (User user : pageData.getData()) { - futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.USER, EdgeEventActionType.ADDED, user.getId(), null)); - } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } - } while (pageData != null && pageData.hasNext()); - return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); - case UNASSIGNED_FROM_CUSTOMER: - CustomerId customerIdToDelete = JacksonUtil.OBJECT_MAPPER.readValue(edgeNotificationMsg.getBody(), CustomerId.class); - edge = edgeService.findEdgeById(tenantId, edgeId); - if (edge == null || customerIdToDelete.isNullUid()) { - return Futures.immediateFuture(null); - } - return Futures.transformAsync(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.EDGE, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, edgeId, null), - voids -> saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.DELETED, customerIdToDelete, null), - dbCallbackExecutorService); - default: - return Futures.immediateFuture(null); - } - } catch (Exception e) { - log.error("Exception during processing edge event", e); - return Futures.immediateFailedFuture(e); - } - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EntityViewEdgeProcessor.java deleted file mode 100644 index b89ce15a5d..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EntityViewEdgeProcessor.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class EntityViewEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent) { - EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: - EntityView entityView = entityViewService.findEntityViewById(edgeEvent.getTenantId(), entityViewId); - if (entityView != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - EntityViewUpdateMsg entityViewUpdateMsg = - entityViewMsgConstructor.constructEntityViewUpdatedMsg(msgType, entityView); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addEntityViewUpdateMsg(entityViewUpdateMsg) - .build(); - } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: - EntityViewUpdateMsg entityViewUpdateMsg = - entityViewMsgConstructor.constructEntityViewDeleteMsg(entityViewId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addEntityViewUpdateMsg(entityViewUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processEntityViewNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/OtaPackageEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/OtaPackageEdgeProcessor.java deleted file mode 100644 index 37ae5142af..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/OtaPackageEdgeProcessor.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.OtaPackage; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.OtaPackageId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertOtaPackageEventToDownlink(EdgeEvent edgeEvent) { - OtaPackageId otaPackageId = new OtaPackageId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - OtaPackage otaPackage = otaPackageService.findOtaPackageById(edgeEvent.getTenantId(), otaPackageId); - if (otaPackage != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - OtaPackageUpdateMsg otaPackageUpdateMsg = - otaPackageMsgConstructor.constructOtaPackageUpdatedMsg(msgType, otaPackage); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addOtaPackageUpdateMsg(otaPackageUpdateMsg) - .build(); - } - break; - case DELETED: - OtaPackageUpdateMsg otaPackageUpdateMsg = - otaPackageMsgConstructor.constructOtaPackageDeleteMsg(otaPackageId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addOtaPackageUpdateMsg(otaPackageUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processOtaPackageNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/QueueEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/QueueEdgeProcessor.java deleted file mode 100644 index 6a7e66a58a..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/QueueEdgeProcessor.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.QueueId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.queue.Queue; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class QueueEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertQueueEventToDownlink(EdgeEvent edgeEvent) { - QueueId queueId = new QueueId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - Queue queue = queueService.findQueueById(edgeEvent.getTenantId(), queueId); - if (queue != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - QueueUpdateMsg queueUpdateMsg = - queueMsgConstructor.constructQueueUpdatedMsg(msgType, queue); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addQueueUpdateMsg(queueUpdateMsg) - .build(); - } - break; - case DELETED: - QueueUpdateMsg queueDeleteMsg = - queueMsgConstructor.constructQueueDeleteMsg(queueId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addQueueUpdateMsg(queueDeleteMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processQueueNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationEdgeProcessor.java deleted file mode 100644 index 2998001a0a..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationEdgeProcessor.java +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; -import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.UUID; - -@Component -@Slf4j -@TbCoreComponent -public class RelationEdgeProcessor extends BaseEdgeProcessor { - - public ListenableFuture processRelationFromEdge(TenantId tenantId, RelationUpdateMsg relationUpdateMsg) { - log.trace("[{}] onRelationUpdate [{}]", tenantId, relationUpdateMsg); - try { - EntityRelation entityRelation = new EntityRelation(); - - UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); - EntityId fromId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getFromEntityType()), fromUUID); - entityRelation.setFrom(fromId); - - UUID toUUID = new UUID(relationUpdateMsg.getToIdMSB(), relationUpdateMsg.getToIdLSB()); - EntityId toId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getToEntityType()), toUUID); - entityRelation.setTo(toId); - - entityRelation.setType(relationUpdateMsg.getType()); - if (relationUpdateMsg.hasTypeGroup()) { - entityRelation.setTypeGroup(RelationTypeGroup.valueOf(relationUpdateMsg.getTypeGroup())); - } - entityRelation.setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.readTree(relationUpdateMsg.getAdditionalInfo())); - switch (relationUpdateMsg.getMsgType()) { - case ENTITY_CREATED_RPC_MESSAGE: - case ENTITY_UPDATED_RPC_MESSAGE: - if (isEntityExists(tenantId, entityRelation.getTo()) - && isEntityExists(tenantId, entityRelation.getFrom())) { - relationService.saveRelationAsync(tenantId, entityRelation); - } - break; - case ENTITY_DELETED_RPC_MESSAGE: - relationService.deleteRelation(tenantId, entityRelation); - break; - case UNRECOGNIZED: - log.error("Unsupported msg type"); - } - return Futures.immediateFuture(null); - } catch (Exception e) { - log.error("Failed to process relation update msg [{}]", relationUpdateMsg, e); - return Futures.immediateFailedFuture(new RuntimeException("Failed to process relation update msg", e)); - } - } - - - private boolean isEntityExists(TenantId tenantId, EntityId entityId) throws ThingsboardException { - switch (entityId.getEntityType()) { - case DEVICE: - return deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())) != null; - case ASSET: - return assetService.findAssetById(tenantId, new AssetId(entityId.getId())) != null; - case ENTITY_VIEW: - return entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())) != null; - case CUSTOMER: - return customerService.findCustomerById(tenantId, new CustomerId(entityId.getId())) != null; - case USER: - return userService.findUserById(tenantId, new UserId(entityId.getId())) != null; - case DASHBOARD: - return dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId())) != null; - default: - throw new ThingsboardException("Unsupported entity type " + entityId.getEntityType(), ThingsboardErrorCode.INVALID_ARGUMENTS); - } - } - - public DownlinkMsg convertRelationEventToDownlink(EdgeEvent edgeEvent) { - EntityRelation entityRelation = JacksonUtil.OBJECT_MAPPER.convertValue(edgeEvent.getBody(), EntityRelation.class); - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - RelationUpdateMsg relationUpdateMsg = relationMsgConstructor.constructRelationUpdatedMsg(msgType, entityRelation); - return DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRelationUpdateMsg(relationUpdateMsg) - .build(); - } - - public ListenableFuture processRelationNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) throws JsonProcessingException { - EntityRelation relation = JacksonUtil.OBJECT_MAPPER.readValue(edgeNotificationMsg.getBody(), EntityRelation.class); - if (relation.getFrom().getEntityType().equals(EntityType.EDGE) || - relation.getTo().getEntityType().equals(EntityType.EDGE)) { - return Futures.immediateFuture(null); - } - - Set uniqueEdgeIds = new HashSet<>(); - uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getTo())); - uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getFrom())); - if (uniqueEdgeIds.isEmpty()) { - return Futures.immediateFuture(null); - } - List> futures = new ArrayList<>(); - for (EdgeId edgeId : uniqueEdgeIds) { - futures.add(saveEdgeEvent(tenantId, - edgeId, - EdgeEventType.RELATION, - EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()), - null, - JacksonUtil.OBJECT_MAPPER.valueToTree(relation))); - } - return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RuleChainEdgeProcessor.java deleted file mode 100644 index 5704e7d30d..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RuleChainEdgeProcessor.java +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.rule.RuleChainMetaData; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class RuleChainEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertRuleChainEventToDownlink(Edge edge, EdgeEvent edgeEvent) { - RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId); - if (ruleChain != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - RuleChainUpdateMsg ruleChainUpdateMsg = - ruleChainMsgConstructor.constructRuleChainUpdatedMsg(edge.getRootRuleChainId(), msgType, ruleChain); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRuleChainUpdateMsg(ruleChainUpdateMsg) - .build(); - } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRuleChainUpdateMsg(ruleChainMsgConstructor.constructRuleChainDeleteMsg(ruleChainId)) - .build(); - break; - } - return downlinkMsg; - } - - public DownlinkMsg convertRuleChainMetadataEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { - RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); - RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId); - DownlinkMsg downlinkMsg = null; - if (ruleChain != null) { - RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - ruleChainMsgConstructor.constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion); - if (ruleChainMetadataUpdateMsg != null) { - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg) - .build(); - } - } - return downlinkMsg; - } - - public ListenableFuture processRuleChainNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java deleted file mode 100644 index f4d713b40f..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java +++ /dev/null @@ -1,347 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; -import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.asset.AssetProfile; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.AttributeKey; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.common.msg.session.SessionMsgType; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; -import org.thingsboard.server.common.transport.util.JsonUtils; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.EntityDataProto; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.server.queue.TbQueueMsgMetadata; -import org.thingsboard.server.queue.TbQueueProducer; -import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.util.TbCoreComponent; - -import javax.annotation.Nullable; -import javax.annotation.PostConstruct; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -@Component -@Slf4j -@TbCoreComponent -public class TelemetryEdgeProcessor extends BaseEdgeProcessor { - - private final Gson gson = new Gson(); - - private TbQueueProducer> tbCoreMsgProducer; - - @PostConstruct - public void init() { - tbCoreMsgProducer = producerProvider.getTbCoreMsgProducer(); - } - - public List> processTelemetryFromEdge(TenantId tenantId, EntityDataProto entityData) { - log.trace("[{}] processTelemetryFromEdge [{}]", tenantId, entityData); - List> result = new ArrayList<>(); - EntityId entityId = constructEntityId(entityData.getEntityType(), entityData.getEntityIdMSB(), entityData.getEntityIdLSB()); - if ((entityData.hasPostAttributesMsg() || entityData.hasPostTelemetryMsg() || entityData.hasAttributesUpdatedMsg()) && entityId != null) { - Pair pair = getBaseMsgMetadataAndCustomerId(tenantId, entityId); - TbMsgMetaData metaData = pair.getKey(); - CustomerId customerId = pair.getValue(); - metaData.putValue(DataConstants.MSG_SOURCE_KEY, DataConstants.EDGE_MSG_SOURCE); - if (entityData.hasPostAttributesMsg()) { - result.add(processPostAttributes(tenantId, customerId, entityId, entityData.getPostAttributesMsg(), metaData)); - } - if (entityData.hasAttributesUpdatedMsg()) { - metaData.putValue("scope", entityData.getPostAttributeScope()); - result.add(processAttributesUpdate(tenantId, customerId, entityId, entityData.getAttributesUpdatedMsg(), metaData)); - } - if (entityData.hasPostTelemetryMsg()) { - result.add(processPostTelemetry(tenantId, customerId, entityId, entityData.getPostTelemetryMsg(), metaData)); - } - if (EntityType.DEVICE.equals(entityId.getEntityType())) { - DeviceId deviceId = new DeviceId(entityId.getId()); - - long currentTs = System.currentTimeMillis(); - - TransportProtos.DeviceActivityProto deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) - .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) - .setLastActivityTime(currentTs).build(); - - log.trace("[{}][{}] device activity time is going to be updated, ts {}", tenantId, deviceId, currentTs); - - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); - tbCoreMsgProducer.send(tpi, new TbProtoQueueMsg<>(deviceId.getId(), - TransportProtos.ToCoreMsg.newBuilder().setDeviceActivityMsg(deviceActivityMsg).build()), null); - } - } - if (entityData.hasAttributeDeleteMsg()) { - result.add(processAttributeDeleteMsg(tenantId, entityId, entityData.getAttributeDeleteMsg(), entityData.getEntityType())); - } - return result; - } - - private Pair getBaseMsgMetadataAndCustomerId(TenantId tenantId, EntityId entityId) { - TbMsgMetaData metaData = new TbMsgMetaData(); - CustomerId customerId = null; - switch (entityId.getEntityType()) { - case DEVICE: - Device device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); - if (device != null) { - customerId = device.getCustomerId(); - metaData.putValue("deviceName", device.getName()); - metaData.putValue("deviceType", device.getType()); - } - break; - case ASSET: - Asset asset = assetService.findAssetById(tenantId, new AssetId(entityId.getId())); - if (asset != null) { - customerId = asset.getCustomerId(); - metaData.putValue("assetName", asset.getName()); - metaData.putValue("assetType", asset.getType()); - } - break; - case ENTITY_VIEW: - EntityView entityView = entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())); - if (entityView != null) { - customerId = entityView.getCustomerId(); - metaData.putValue("entityViewName", entityView.getName()); - metaData.putValue("entityViewType", entityView.getType()); - } - break; - case EDGE: - Edge edge = edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())); - if (edge != null) { - customerId = edge.getCustomerId(); - metaData.putValue("edgeName", edge.getName()); - metaData.putValue("edgeType", edge.getType()); - } - break; - default: - log.debug("Using empty metadata for entityId [{}]", entityId); - break; - } - return new ImmutablePair<>(metaData, customerId != null ? customerId : new CustomerId(ModelConstants.NULL_UUID)); - } - - private ListenableFuture processPostTelemetry(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostTelemetryMsg msg, TbMsgMetaData metaData) { - SettableFuture futureToSet = SettableFuture.create(); - for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { - JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); - metaData.putValue("ts", tsKv.getTs() + ""); - var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); - tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - futureToSet.set(null); - } - - @Override - public void onFailure(Throwable t) { - log.error("Can't process post telemetry [{}]", msg, t); - futureToSet.setException(t); - } - }); - } - return futureToSet; - } - - private Pair getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) { - RuleChainId ruleChainId = null; - String queueName = null; - if (EntityType.DEVICE.equals(entityId.getEntityType())) { - DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); - if (deviceProfile == null) { - log.warn("[{}] Device profile is null!", entityId); - } else { - ruleChainId = deviceProfile.getDefaultRuleChainId(); - queueName = deviceProfile.getDefaultQueueName(); - } - } else if (EntityType.ASSET.equals(entityId.getEntityType())) { - AssetProfile assetProfile = assetProfileCache.get(tenantId, new AssetId(entityId.getId())); - if (assetProfile == null) { - log.warn("[{}] Asset profile is null!", entityId); - } else { - ruleChainId = assetProfile.getDefaultRuleChainId(); - queueName = assetProfile.getDefaultQueueName(); - } - } - return new ImmutablePair<>(queueName, ruleChainId); - } - - private ListenableFuture processPostAttributes(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) { - SettableFuture futureToSet = SettableFuture.create(); - JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); - var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); - tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - futureToSet.set(null); - } - - @Override - public void onFailure(Throwable t) { - log.error("Can't process post attributes [{}]", msg, t); - futureToSet.setException(t); - } - }); - return futureToSet; - } - - private ListenableFuture processAttributesUpdate(TenantId tenantId, - CustomerId customerId, - EntityId entityId, - TransportProtos.PostAttributeMsg msg, - TbMsgMetaData metaData) { - SettableFuture futureToSet = SettableFuture.create(); - JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(json)); - String scope = metaData.getValue("scope"); - tsSubService.saveAndNotify(tenantId, entityId, scope, attributes, new FutureCallback() { - @Override - public void onSuccess(@Nullable Void tmp) { - var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), DataConstants.ATTRIBUTES_UPDATED, entityId, - customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); - tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - futureToSet.set(null); - } - - @Override - public void onFailure(Throwable t) { - log.error("Can't process attributes update [{}]", msg, t); - futureToSet.setException(t); - } - }); - } - - @Override - public void onFailure(Throwable t) { - log.error("Can't process attributes update [{}]", msg, t); - futureToSet.setException(t); - } - }); - return futureToSet; - } - - private ListenableFuture processAttributeDeleteMsg(TenantId tenantId, EntityId entityId, AttributeDeleteMsg attributeDeleteMsg, - String entityType) { - SettableFuture futureToSet = SettableFuture.create(); - String scope = attributeDeleteMsg.getScope(); - List attributeNames = attributeDeleteMsg.getAttributeNamesList(); - attributesService.removeAll(tenantId, entityId, scope, attributeNames); - if (EntityType.DEVICE.name().equals(entityType)) { - tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete( - tenantId, (DeviceId) entityId, scope, attributeNames), new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - futureToSet.set(null); - } - - @Override - public void onFailure(Throwable t) { - log.error("Can't process attribute delete msg [{}]", attributeDeleteMsg, t); - futureToSet.setException(t); - } - }); - } - return futureToSet; - } - - public DownlinkMsg convertTelemetryEventToDownlink(EdgeEvent edgeEvent) throws JsonProcessingException { - EntityId entityId; - switch (edgeEvent.getType()) { - case DEVICE: - entityId = new DeviceId(edgeEvent.getEntityId()); - break; - case ASSET: - entityId = new AssetId(edgeEvent.getEntityId()); - break; - case ENTITY_VIEW: - entityId = new EntityViewId(edgeEvent.getEntityId()); - break; - case DASHBOARD: - entityId = new DashboardId(edgeEvent.getEntityId()); - break; - case TENANT: - entityId = TenantId.fromUUID(edgeEvent.getEntityId()); - break; - case CUSTOMER: - entityId = new CustomerId(edgeEvent.getEntityId()); - break; - case EDGE: - entityId = new EdgeId(edgeEvent.getEntityId()); - break; - default: - log.warn("Unsupported edge event type [{}]", edgeEvent); - return null; - } - return constructEntityDataProtoMsg(entityId, edgeEvent.getAction(), - JsonUtils.parse(JacksonUtil.OBJECT_MAPPER.writeValueAsString(edgeEvent.getBody()))); - } - - private DownlinkMsg constructEntityDataProtoMsg(EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { - EntityDataProto entityDataProto = entityDataMsgConstructor.constructEntityDataMsg(entityId, actionType, entityData); - return DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addEntityData(entityDataProto) - .build(); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/UserEdgeProcessor.java deleted file mode 100644 index 390fafe464..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/UserEdgeProcessor.java +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class UserEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertUserEventToDownlink(EdgeEvent edgeEvent) { - UserId userId = new UserId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - User user = userService.findUserById(edgeEvent.getTenantId(), userId); - if (user != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserUpdateMsg(userMsgConstructor.constructUserUpdatedMsg(msgType, user)) - .build(); - } - break; - case DELETED: - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserUpdateMsg(userMsgConstructor.constructUserDeleteMsg(userId)) - .build(); - break; - case CREDENTIALS_UPDATED: - UserCredentials userCredentialsByUserId = userService.findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); - if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { - UserCredentialsUpdateMsg userCredentialsUpdateMsg = - userMsgConstructor.constructUserCredentialsUpdatedMsg(userCredentialsByUserId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) - .build(); - } - } - return downlinkMsg; - } - - public ListenableFuture processUserNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetBundleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetBundleEdgeProcessor.java deleted file mode 100644 index b74e3c1cec..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetBundleEdgeProcessor.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertWidgetsBundleEventToDownlink(EdgeEvent edgeEvent) { - WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(edgeEvent.getTenantId(), widgetsBundleId); - if (widgetsBundle != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = - widgetsBundleMsgConstructor.constructWidgetsBundleUpdateMsg(msgType, widgetsBundle); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) - .build(); - } - break; - case DELETED: - WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = - widgetsBundleMsgConstructor.constructWidgetsBundleDeleteMsg(widgetsBundleId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processWidgetsBundleNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetTypeEdgeProcessor.java deleted file mode 100644 index 10807b7b5c..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetTypeEdgeProcessor.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.processor; - -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetTypeId; -import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@Slf4j -@TbCoreComponent -public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { - - public DownlinkMsg convertWidgetTypeEventToDownlink(EdgeEvent edgeEvent) { - WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); - DownlinkMsg downlinkMsg = null; - switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(edgeEvent.getTenantId(), widgetTypeId); - if (widgetTypeDetails != null) { - UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - WidgetTypeUpdateMsg widgetTypeUpdateMsg = - widgetTypeMsgConstructor.constructWidgetTypeUpdateMsg(msgType, widgetTypeDetails); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) - .build(); - } - break; - case DELETED: - WidgetTypeUpdateMsg widgetTypeUpdateMsg = - widgetTypeMsgConstructor.constructWidgetTypeDeleteMsg(widgetTypeId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) - .build(); - break; - } - return downlinkMsg; - } - - public ListenableFuture processWidgetTypeNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java new file mode 100644 index 0000000000..189d69e68d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.alarm; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Component +@Slf4j +@TbCoreComponent +public class AlarmEdgeProcessor extends BaseAlarmProcessor { + + public DownlinkMsg convertAlarmEventToDownlink(EdgeEvent edgeEvent) { + AlarmUpdateMsg alarmUpdateMsg = + convertAlarmEventToAlarmMsg(edgeEvent.getTenantId(), edgeEvent.getEntityId(), edgeEvent.getAction(), edgeEvent.getBody()); + if (alarmUpdateMsg != null) { + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAlarmUpdateMsg(alarmUpdateMsg) + .build(); + } + return null; + } + + public ListenableFuture processAlarmNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) throws JsonProcessingException { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + switch (actionType) { + case DELETED: + EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); + Alarm deletedAlarm = JacksonUtil.OBJECT_MAPPER.readValue(edgeNotificationMsg.getBody(), Alarm.class); + return saveEdgeEvent(tenantId, edgeId, EdgeEventType.ALARM, actionType, alarmId, JacksonUtil.OBJECT_MAPPER.valueToTree(deletedAlarm)); + default: + ListenableFuture alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId); + return Futures.transformAsync(alarmFuture, alarm -> { + if (alarm == null) { + return Futures.immediateFuture(null); + } + EdgeEventType type = EdgeUtils.getEdgeEventTypeByEntityType(alarm.getOriginator().getEntityType()); + if (type == null) { + return Futures.immediateFuture(null); + } + PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); + PageData pageData; + List> futures = new ArrayList<>(); + do { + pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, alarm.getOriginator(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (EdgeId relatedEdgeId : pageData.getData()) { + futures.add(saveEdgeEvent(tenantId, + relatedEdgeId, + EdgeEventType.ALARM, + EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()), + alarmId, + null)); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); + }, dbCallbackExecutorService); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java new file mode 100644 index 0000000000..be64f475a2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -0,0 +1,137 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.alarm; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; + +@Slf4j +public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { + + public ListenableFuture processAlarmMsg(TenantId tenantId, AlarmUpdateMsg alarmUpdateMsg) { + log.trace("[{}] processAlarmMsg [{}]", tenantId, alarmUpdateMsg); + EntityId originatorId = getAlarmOriginator(tenantId, alarmUpdateMsg.getOriginatorName(), + EntityType.valueOf(alarmUpdateMsg.getOriginatorType())); + AlarmId alarmId = new AlarmId(new UUID(alarmUpdateMsg.getIdMSB(), alarmUpdateMsg.getIdLSB())); + if (originatorId == null) { + log.warn("Originator not found for the alarm msg {}", alarmUpdateMsg); + return Futures.immediateFuture(null); + } + try { + switch (alarmUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + Alarm alarm = new Alarm(); + alarm.setId(alarmId); + alarm.setTenantId(tenantId); + alarm.setType(alarmUpdateMsg.getName()); + alarm.setOriginator(originatorId); + alarm.setSeverity(AlarmSeverity.valueOf(alarmUpdateMsg.getSeverity())); + alarm.setStartTs(alarmUpdateMsg.getStartTs()); + var alarmStatus = AlarmStatus.valueOf(alarmUpdateMsg.getStatus()); + alarm.setClearTs(alarmUpdateMsg.getClearTs()); + alarm.setPropagate(alarmUpdateMsg.getPropagate()); + alarm.setCleared(alarmStatus.isCleared()); + alarm.setAcknowledged(alarmStatus.isAck()); + alarm.setAckTs(alarmUpdateMsg.getAckTs()); + alarm.setEndTs(alarmUpdateMsg.getEndTs()); + alarm.setDetails(JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails())); + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(alarmUpdateMsg.getMsgType())) { + alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(alarm, null, alarmId)); + } else { + alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(alarm)); + } + return Futures.immediateFuture(null); + case ALARM_ACK_RPC_MESSAGE: + Alarm alarmToAck = alarmService.findAlarmById(tenantId, alarmId); + if (alarmToAck != null) { + alarmService.acknowledgeAlarm(tenantId, alarmId, alarmUpdateMsg.getAckTs()); + } + return Futures.immediateFuture(null); + case ALARM_CLEAR_RPC_MESSAGE: + Alarm alarmToClear = alarmService.findAlarmById(tenantId, alarmId); + if (alarmToClear != null) { + alarmService.clearAlarm(tenantId, alarmId, alarmUpdateMsg.getClearTs(), + JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails())); + } + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + Alarm alarmToDelete = alarmService.findAlarmById(tenantId, alarmId); + if (alarmToDelete != null) { + alarmService.delAlarm(tenantId, alarmId); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(alarmUpdateMsg.getMsgType()); + } + } catch (Exception e) { + log.error("[{}] Failed to process alarm update msg [{}]", tenantId, alarmUpdateMsg, e); + return Futures.immediateFailedFuture(e); + } + } + + private EntityId getAlarmOriginator(TenantId tenantId, String entityName, EntityType entityType) { + switch (entityType) { + case DEVICE: + return deviceService.findDeviceByTenantIdAndName(tenantId, entityName).getId(); + case ASSET: + return assetService.findAssetByTenantIdAndName(tenantId, entityName).getId(); + case ENTITY_VIEW: + return entityViewService.findEntityViewByTenantIdAndName(tenantId, entityName).getId(); + default: + return null; + } + } + + public AlarmUpdateMsg convertAlarmEventToAlarmMsg(TenantId tenantId, UUID entityId, EdgeEventActionType actionType, JsonNode body) { + AlarmId alarmId = new AlarmId(entityId); + UpdateMsgType msgType = getUpdateMsgType(actionType); + switch (actionType) { + case ADDED: + case UPDATED: + case ALARM_ACK: + case ALARM_CLEAR: + Alarm alarm = alarmService.findAlarmById(tenantId, alarmId); + if (alarm != null) { + return alarmMsgConstructor.constructAlarmUpdatedMsg(tenantId, msgType, alarm); + } + break; + case DELETED: + Alarm deletedAlarm = JacksonUtil.OBJECT_MAPPER.convertValue(body, Alarm.class); + return alarmMsgConstructor.constructAlarmUpdatedMsg(tenantId, msgType, deletedAlarm); + } + return null; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java new file mode 100644 index 0000000000..2fb7320219 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.asset; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class AssetEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent) { + AssetId assetId = new AssetId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId); + if (asset != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + AssetUpdateMsg assetUpdateMsg = + assetMsgConstructor.constructAssetUpdatedMsg(msgType, asset); + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAssetUpdateMsg(assetUpdateMsg); + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { + AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); + builder.addAssetProfileUpdateMsg(assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile)); + } + downlinkMsg = builder.build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + AssetUpdateMsg assetUpdateMsg = + assetMsgConstructor.constructAssetDeleteMsg(assetId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAssetUpdateMsg(assetUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processAssetNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotification(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java new file mode 100644 index 0000000000..51c42e5764 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.asset; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class AssetProfileEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent) { + AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId); + if (assetProfile != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + AssetProfileUpdateMsg assetProfileUpdateMsg = + assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAssetProfileUpdateMsg(assetProfileUpdateMsg) + .build(); + } + break; + case DELETED: + AssetProfileUpdateMsg assetProfileUpdateMsg = + assetProfileMsgConstructor.constructAssetProfileDeleteMsg(assetProfileId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAssetProfileUpdateMsg(assetProfileUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processAssetProfileNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java new file mode 100644 index 0000000000..f553d6d5a0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java @@ -0,0 +1,108 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.customer; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Component +@Slf4j +@TbCoreComponent +public class CustomerEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertCustomerEventToDownlink(EdgeEvent edgeEvent) { + CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + Customer customer = customerService.findCustomerById(edgeEvent.getTenantId(), customerId); + if (customer != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + CustomerUpdateMsg customerUpdateMsg = + customerMsgConstructor.constructCustomerUpdatedMsg(msgType, customer); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addCustomerUpdateMsg(customerUpdateMsg) + .build(); + } + break; + case DELETED: + CustomerUpdateMsg customerUpdateMsg = + customerMsgConstructor.constructCustomerDeleteMsg(customerId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addCustomerUpdateMsg(customerUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processCustomerNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + UUID uuid = new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()); + CustomerId customerId = new CustomerId(EntityIdFactory.getByEdgeEventTypeAndUuid(type, uuid).getId()); + switch (actionType) { + case UPDATED: + PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); + PageData pageData; + List> futures = new ArrayList<>(); + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (Edge edge : pageData.getData()) { + futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null)); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); + case DELETED: + EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); + return saveEdgeEvent(tenantId, edgeId, type, actionType, customerId, null); + default: + return Futures.immediateFuture(null); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java new file mode 100644 index 0000000000..9651609bd3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.dashboard; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class DashboardEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertDashboardEventToDownlink(EdgeEvent edgeEvent) { + DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Dashboard dashboard = dashboardService.findDashboardById(edgeEvent.getTenantId(), dashboardId); + if (dashboard != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + DashboardUpdateMsg dashboardUpdateMsg = + dashboardMsgConstructor.constructDashboardUpdatedMsg(msgType, dashboard); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDashboardUpdateMsg(dashboardUpdateMsg) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + DashboardUpdateMsg dashboardUpdateMsg = + dashboardMsgConstructor.constructDashboardDeleteMsg(dashboardId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDashboardUpdateMsg(dashboardUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processDashboardNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotification(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java new file mode 100644 index 0000000000..742acdbcda --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.device; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { + + @Autowired + protected DataDecodingEncodingService dataDecodingEncodingService; + + protected Pair saveOrUpdateDevice(TenantId tenantId, DeviceId deviceId, DeviceUpdateMsg deviceUpdateMsg, CustomerId customerId) { + boolean created = false; + boolean deviceNameUpdated = false; + deviceCreationLock.lock(); + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + String deviceName = deviceUpdateMsg.getName(); + if (device == null) { + created = true; + device = new Device(); + device.setTenantId(tenantId); + device.setCreatedTime(Uuids.unixTimestamp(deviceId.getId())); + Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); + if (deviceByName != null) { + deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15); + log.warn("Device with name {} already exists. Renaming device name to {}", + deviceUpdateMsg.getName(), deviceName); + deviceNameUpdated = true; + } + } + device.setName(deviceName); + device.setType(deviceUpdateMsg.getType()); + device.setLabel(deviceUpdateMsg.hasLabel() ? deviceUpdateMsg.getLabel() : null); + device.setAdditionalInfo(deviceUpdateMsg.hasAdditionalInfo() + ? JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo()) : null); + + UUID deviceProfileUUID = safeGetUUID(deviceUpdateMsg.getDeviceProfileIdMSB(), deviceUpdateMsg.getDeviceProfileIdLSB()); + device.setDeviceProfileId(deviceProfileUUID != null ? new DeviceProfileId(deviceProfileUUID) : null); + + device.setCustomerId(customerId); + + Optional deviceDataOpt = + dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); + device.setDeviceData(deviceDataOpt.orElse(null)); + + UUID firmwareUUID = safeGetUUID(deviceUpdateMsg.getFirmwareIdMSB(), deviceUpdateMsg.getFirmwareIdLSB()); + device.setFirmwareId(firmwareUUID != null ? new OtaPackageId(firmwareUUID) : null); + + UUID softwareUUID = safeGetUUID(deviceUpdateMsg.getSoftwareIdMSB(), deviceUpdateMsg.getSoftwareIdLSB()); + device.setSoftwareId(softwareUUID != null ? new OtaPackageId(softwareUUID) : null); + deviceValidator.validate(device, Device::getTenantId); + if (created) { + device.setId(deviceId); + } + Device savedDevice = deviceService.saveDevice(device, false); + if (created) { + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); + deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + deviceCredentials.setCredentialsId(StringUtils.randomAlphanumeric(20)); + deviceCredentialsService.createDeviceCredentials(device.getTenantId(), deviceCredentials); + } + tbClusterService.onDeviceUpdated(savedDevice, created ? null : device, false); + } finally { + deviceCreationLock.unlock(); + } + return Pair.of(created, deviceNameUpdated); + } + + public ListenableFuture processDeviceCredentialsMsg(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { + log.debug("[{}] Executing processDeviceCredentialsMsg, deviceCredentialsUpdateMsg [{}]", tenantId, deviceCredentialsUpdateMsg); + DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB())); + return dbCallbackExecutorService.submit(() -> { + Device device = deviceService.findDeviceById(tenantId, deviceId); + if (device != null) { + log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", + device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); + try { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); + deviceCredentials.setCredentialsType(DeviceCredentialsType.valueOf(deviceCredentialsUpdateMsg.getCredentialsType())); + deviceCredentials.setCredentialsId(deviceCredentialsUpdateMsg.getCredentialsId()); + deviceCredentials.setCredentialsValue(deviceCredentialsUpdateMsg.hasCredentialsValue() + ? deviceCredentialsUpdateMsg.getCredentialsValue() : null); + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + } catch (Exception e) { + log.error("Can't update device credentials for device [{}], deviceCredentialsUpdateMsg [{}]", + device.getName(), deviceCredentialsUpdateMsg, e); + throw new RuntimeException(e); + } + } else { + log.warn("Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", deviceId, deviceCredentialsUpdateMsg); + } + return null; + }); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java new file mode 100644 index 0000000000..3e617de8c6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -0,0 +1,315 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.device; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rpc.RpcError; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; + +import java.util.UUID; + +@Component +@Slf4j +@TbCoreComponent +public class DeviceEdgeProcessor extends BaseDeviceProcessor { + + public ListenableFuture processDeviceMsgFromEdge(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { + log.trace("[{}] executing processDeviceMsgFromEdge [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); + DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); + try { + switch (deviceUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge); + return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null); + case ENTITY_DELETED_RPC_MESSAGE: + Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId); + if (deviceToDelete != null) { + deviceService.unassignDeviceFromEdge(tenantId, deviceId, edge.getId()); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(deviceUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed devices violated {}", tenantId, deviceUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } + } + + private void saveOrUpdateDevice(TenantId tenantId, DeviceId deviceId, DeviceUpdateMsg deviceUpdateMsg, Edge edge) { + CustomerId customerId = safeGetCustomerId(deviceUpdateMsg.getCustomerIdMSB(), deviceUpdateMsg.getCustomerIdLSB()); + Pair resultPair = super.saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, customerId); + Boolean created = resultPair.getFirst(); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), deviceId); + pushDeviceCreatedEventToRuleEngine(tenantId, edge, deviceId); + deviceService.assignDeviceToEdge(tenantId, deviceId, edge.getId()); + } + Boolean deviceNameUpdated = resultPair.getSecond(); + if (deviceNameUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.UPDATED, deviceId, null); + } + } + + private void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { + EntityRelation relation = new EntityRelation(); + relation.setFrom(edgeId); + relation.setTo(entityId); + relation.setTypeGroup(RelationTypeGroup.COMMON); + relation.setType(EntityRelation.EDGE_TYPE); + relationService.saveRelation(tenantId, relation); + } + + private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceId deviceId) { + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device); + TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, device.getCustomerId(), + getActionTbMsgMetaData(edge, device.getCustomerId()), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", device); + } + + @Override + public void onFailure(Throwable t) { + log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); + } + }); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push device action to rule engine: {}", deviceId, DataConstants.ENTITY_CREATED, e); + } + } + + private TbMsgMetaData getActionTbMsgMetaData(Edge edge, CustomerId customerId) { + TbMsgMetaData metaData = getTbMsgMetaData(edge); + if (customerId != null && !customerId.isNullUid()) { + metaData.putValue("customerId", customerId.toString()); + } + return metaData; + } + + private TbMsgMetaData getTbMsgMetaData(Edge edge) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("edgeId", edge.getId().toString()); + metaData.putValue("edgeName", edge.getName()); + return metaData; + } + + public ListenableFuture processDeviceRpcCallFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) { + log.trace("[{}] processDeviceRpcCallFromEdge [{}]", tenantId, deviceRpcCallMsg); + if (deviceRpcCallMsg.hasResponseMsg()) { + return processDeviceRpcResponseFromEdge(tenantId, deviceRpcCallMsg); + } else if (deviceRpcCallMsg.hasRequestMsg()) { + return processDeviceRpcRequestFromEdge(tenantId, edge, deviceRpcCallMsg); + } + return Futures.immediateFuture(null); + } + + private ListenableFuture processDeviceRpcResponseFromEdge(TenantId tenantId, DeviceRpcCallMsg deviceRpcCallMsg) { + SettableFuture futureToSet = SettableFuture.create(); + UUID requestUuid = new UUID(deviceRpcCallMsg.getRequestUuidMSB(), deviceRpcCallMsg.getRequestUuidLSB()); + DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB())); + + FromDeviceRpcResponse response; + if (!StringUtils.isEmpty(deviceRpcCallMsg.getResponseMsg().getError())) { + response = new FromDeviceRpcResponse(requestUuid, null, RpcError.valueOf(deviceRpcCallMsg.getResponseMsg().getError())); + } else { + response = new FromDeviceRpcResponse(requestUuid, deviceRpcCallMsg.getResponseMsg().getResponse(), null); + } + TbQueueCallback callback = new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process push notification to core [{}]", deviceRpcCallMsg, t); + futureToSet.setException(t); + } + }; + FromDeviceRpcResponseActorMsg msg = + new FromDeviceRpcResponseActorMsg(deviceRpcCallMsg.getRequestId(), + tenantId, + deviceId, response); + tbClusterService.pushMsgToCore(msg, callback); + return futureToSet; + } + + private ListenableFuture processDeviceRpcRequestFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) { + DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB())); + try { + TbMsgMetaData metaData = new TbMsgMetaData(); + String requestId = Integer.toString(deviceRpcCallMsg.getRequestId()); + metaData.putValue("requestId", requestId); + metaData.putValue("serviceId", deviceRpcCallMsg.getServiceId()); + metaData.putValue("sessionId", deviceRpcCallMsg.getSessionId()); + metaData.putValue(DataConstants.EDGE_ID, edge.getId().toString()); + Device device = deviceService.findDeviceById(tenantId, deviceId); + if (device != null) { + metaData.putValue("deviceName", device.getName()); + metaData.putValue("deviceType", device.getType()); + metaData.putValue(DataConstants.DEVICE_ID, deviceId.getId().toString()); + } + ObjectNode data = JacksonUtil.newObjectNode(); + data.put("method", deviceRpcCallMsg.getRequestMsg().getMethod()); + data.put("params", deviceRpcCallMsg.getRequestMsg().getParams()); + TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, null, metaData, + TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(data)); + tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", + device, deviceRpcCallMsg); + } + + @Override + public void onFailure(Throwable t) { + log.debug("Failed to send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", + device, deviceRpcCallMsg, t); + } + }); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push TO_SERVER_RPC_REQUEST to rule engine. deviceRpcCallMsg {}", deviceId, deviceRpcCallMsg, e); + } + + return Futures.immediateFuture(null); + } + + public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent) { + DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Device device = deviceService.findDeviceById(edgeEvent.getTenantId(), deviceId); + if (device != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + DeviceUpdateMsg deviceUpdateMsg = + deviceMsgConstructor.constructDeviceUpdatedMsg(msgType, device); + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDeviceUpdateMsg(deviceUpdateMsg); + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); + builder.addDeviceProfileUpdateMsg(deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); + } + downlinkMsg = builder.build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + DeviceUpdateMsg deviceUpdateMsg = + deviceMsgConstructor.constructDeviceDeleteMsg(deviceId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDeviceUpdateMsg(deviceUpdateMsg) + .build(); + break; + case CREDENTIALS_UPDATED: + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); + if (deviceCredentials != null) { + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = + deviceMsgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg) + .build(); + } + break; + case RPC_CALL: + return convertRpcCallEventToDownlink(edgeEvent); + case CREDENTIALS_REQUEST: + return convertCredentialsRequestEventToDownlink(edgeEvent); + } + return downlinkMsg; + } + + private DownlinkMsg convertRpcCallEventToDownlink(EdgeEvent edgeEvent) { + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDeviceRpcCallMsg(deviceMsgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) + .build(); + } + + private DownlinkMsg convertCredentialsRequestEventToDownlink(EdgeEvent edgeEvent) { + DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); + DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = DeviceCredentialsRequestMsg.newBuilder() + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .build(); + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg); + return builder.build(); + } + + public ListenableFuture processDeviceNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotification(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java new file mode 100644 index 0000000000..5ddfecfdb1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.device; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class DeviceProfileEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent) { + DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); + if (deviceProfile != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = + deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) + .build(); + } + break; + case DELETED: + DeviceProfileUpdateMsg deviceProfileUpdateMsg = + deviceProfileMsgConstructor.constructDeviceProfileDeleteMsg(deviceProfileId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processDeviceProfileNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java new file mode 100644 index 0000000000..123209afc1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.edge; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Component +@Slf4j +@TbCoreComponent +public class EdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + EdgeId edgeId = new EdgeId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Edge edge = edgeService.findEdgeById(edgeEvent.getTenantId(), edgeId); + if (edge != null) { + EdgeConfiguration edgeConfigMsg = + edgeMsgConstructor.constructEdgeConfiguration(edge); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .setEdgeConfiguration(edgeConfigMsg) + .build(); + } + break; + } + return downlinkMsg; + } + + public ListenableFuture processEdgeNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + try { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + switch (actionType) { + case ASSIGNED_TO_CUSTOMER: + CustomerId customerId = JacksonUtil.OBJECT_MAPPER.readValue(edgeNotificationMsg.getBody(), CustomerId.class); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null || customerId.isNullUid()) { + return Futures.immediateFuture(null); + } + List> futures = new ArrayList<>(); + futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null)); + futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.EDGE, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, edgeId, null)); + PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); + PageData pageData; + do { + pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] user(s) are going to be added to edge.", edge.getId(), pageData.getData().size()); + for (User user : pageData.getData()) { + futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.USER, EdgeEventActionType.ADDED, user.getId(), null)); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); + case UNASSIGNED_FROM_CUSTOMER: + CustomerId customerIdToDelete = JacksonUtil.OBJECT_MAPPER.readValue(edgeNotificationMsg.getBody(), CustomerId.class); + edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null || customerIdToDelete.isNullUid()) { + return Futures.immediateFuture(null); + } + return Futures.transformAsync(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.EDGE, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, edgeId, null), + voids -> saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.DELETED, customerIdToDelete, null), + dbCallbackExecutorService); + default: + return Futures.immediateFuture(null); + } + } catch (Exception e) { + log.error("Exception during processing edge event", e); + return Futures.immediateFailedFuture(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java new file mode 100644 index 0000000000..29965fcc69 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.entityview; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class EntityViewEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent) { + EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + EntityView entityView = entityViewService.findEntityViewById(edgeEvent.getTenantId(), entityViewId); + if (entityView != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + EntityViewUpdateMsg entityViewUpdateMsg = + entityViewMsgConstructor.constructEntityViewUpdatedMsg(msgType, entityView); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addEntityViewUpdateMsg(entityViewUpdateMsg) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + EntityViewUpdateMsg entityViewUpdateMsg = + entityViewMsgConstructor.constructEntityViewDeleteMsg(entityViewId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addEntityViewUpdateMsg(entityViewUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processEntityViewNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotification(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java new file mode 100644 index 0000000000..8206e0f1b0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.ota; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertOtaPackageEventToDownlink(EdgeEvent edgeEvent) { + OtaPackageId otaPackageId = new OtaPackageId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + OtaPackage otaPackage = otaPackageService.findOtaPackageById(edgeEvent.getTenantId(), otaPackageId); + if (otaPackage != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + OtaPackageUpdateMsg otaPackageUpdateMsg = + otaPackageMsgConstructor.constructOtaPackageUpdatedMsg(msgType, otaPackage); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOtaPackageUpdateMsg(otaPackageUpdateMsg) + .build(); + } + break; + case DELETED: + OtaPackageUpdateMsg otaPackageUpdateMsg = + otaPackageMsgConstructor.constructOtaPackageDeleteMsg(otaPackageId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOtaPackageUpdateMsg(otaPackageUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processOtaPackageNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java new file mode 100644 index 0000000000..cbacaf9276 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.queue; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class QueueEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertQueueEventToDownlink(EdgeEvent edgeEvent) { + QueueId queueId = new QueueId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + Queue queue = queueService.findQueueById(edgeEvent.getTenantId(), queueId); + if (queue != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + QueueUpdateMsg queueUpdateMsg = + queueMsgConstructor.constructQueueUpdatedMsg(msgType, queue); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addQueueUpdateMsg(queueUpdateMsg) + .build(); + } + break; + case DELETED: + QueueUpdateMsg queueDeleteMsg = + queueMsgConstructor.constructQueueDeleteMsg(queueId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addQueueUpdateMsg(queueDeleteMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processQueueNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java new file mode 100644 index 0000000000..df683bbbe2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.relation; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; + +@Slf4j +public abstract class BaseRelationProcessor extends BaseEdgeProcessor { + + public ListenableFuture processRelationMsg(TenantId tenantId, RelationUpdateMsg relationUpdateMsg) { + log.trace("[{}] processRelationFromEdge [{}]", tenantId, relationUpdateMsg); + try { + EntityRelation entityRelation = new EntityRelation(); + + UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); + EntityId fromId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getFromEntityType()), fromUUID); + entityRelation.setFrom(fromId); + + UUID toUUID = new UUID(relationUpdateMsg.getToIdMSB(), relationUpdateMsg.getToIdLSB()); + EntityId toId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getToEntityType()), toUUID); + entityRelation.setTo(toId); + + entityRelation.setType(relationUpdateMsg.getType()); + entityRelation.setTypeGroup(relationUpdateMsg.hasTypeGroup() + ? RelationTypeGroup.valueOf(relationUpdateMsg.getTypeGroup()) : RelationTypeGroup.COMMON); + entityRelation.setAdditionalInfo(JacksonUtil.toJsonNode(relationUpdateMsg.getAdditionalInfo())); + switch (relationUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + if (isEntityExists(tenantId, entityRelation.getTo()) + && isEntityExists(tenantId, entityRelation.getFrom())) { + return Futures.transform(relationService.saveRelationAsync(tenantId, entityRelation), + (result) -> null, dbCallbackExecutorService); + } else { + log.warn("Skipping relating update msg because from/to entity doesn't exists on edge, {}", relationUpdateMsg); + return Futures.immediateFuture(null); + } + case ENTITY_DELETED_RPC_MESSAGE: + return Futures.transform(relationService.deleteRelationAsync(tenantId, entityRelation), + (result) -> null, dbCallbackExecutorService); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(relationUpdateMsg.getMsgType()); + } + } catch (Exception e) { + log.error("[{}] Failed to process relation update msg [{}]", tenantId, relationUpdateMsg, e); + return Futures.immediateFailedFuture(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java new file mode 100644 index 0000000000..48192a3973 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.relation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Component +@Slf4j +@TbCoreComponent +public class RelationEdgeProcessor extends BaseRelationProcessor { + + public DownlinkMsg convertRelationEventToDownlink(EdgeEvent edgeEvent) { + EntityRelation entityRelation = JacksonUtil.OBJECT_MAPPER.convertValue(edgeEvent.getBody(), EntityRelation.class); + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + RelationUpdateMsg relationUpdateMsg = relationMsgConstructor.constructRelationUpdatedMsg(msgType, entityRelation); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addRelationUpdateMsg(relationUpdateMsg) + .build(); + } + + public ListenableFuture processRelationNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) throws JsonProcessingException { + EntityRelation relation = JacksonUtil.OBJECT_MAPPER.readValue(edgeNotificationMsg.getBody(), EntityRelation.class); + if (relation.getFrom().getEntityType().equals(EntityType.EDGE) || + relation.getTo().getEntityType().equals(EntityType.EDGE)) { + return Futures.immediateFuture(null); + } + + Set uniqueEdgeIds = new HashSet<>(); + uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getTo())); + uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getFrom())); + if (uniqueEdgeIds.isEmpty()) { + return Futures.immediateFuture(null); + } + List> futures = new ArrayList<>(); + for (EdgeId edgeId : uniqueEdgeIds) { + futures.add(saveEdgeEvent(tenantId, + edgeId, + EdgeEventType.RELATION, + EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()), + null, + JacksonUtil.OBJECT_MAPPER.valueToTree(relation))); + } + return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java new file mode 100644 index 0000000000..69d4f7ef64 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.rule; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY; + +@Component +@Slf4j +@TbCoreComponent +public class RuleChainEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertRuleChainEventToDownlink(EdgeEvent edgeEvent) { + RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId); + if (ruleChain != null) { + boolean isRoot = false; + if (edgeEvent.getBody() != null && edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY) != null) { + try { + isRoot = Boolean.parseBoolean(edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY).asText()); + } catch (Exception ignored) {} + } + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + RuleChainUpdateMsg ruleChainUpdateMsg = + ruleChainMsgConstructor.constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addRuleChainUpdateMsg(ruleChainUpdateMsg) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addRuleChainUpdateMsg(ruleChainMsgConstructor.constructRuleChainDeleteMsg(ruleChainId)) + .build(); + break; + } + return downlinkMsg; + } + + public DownlinkMsg convertRuleChainMetadataEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); + RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId); + DownlinkMsg downlinkMsg = null; + if (ruleChain != null) { + RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + ruleChainMsgConstructor.constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion); + if (ruleChainMetadataUpdateMsg != null) { + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg) + .build(); + } + } + return downlinkMsg; + } + + public ListenableFuture processRuleChainNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotification(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java new file mode 100644 index 0000000000..6bd21e0796 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.settings; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertAdminSettingsEventToDownlink(EdgeEvent edgeEvent) { + AdminSettings adminSettings = JacksonUtil.OBJECT_MAPPER.convertValue(edgeEvent.getBody(), AdminSettings.class); + AdminSettingsUpdateMsg adminSettingsUpdateMsg = adminSettingsMsgConstructor.constructAdminSettingsUpdateMsg(adminSettings); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAdminSettingsUpdateMsg(adminSettingsUpdateMsg) + .build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java new file mode 100644 index 0000000000..fece86b92a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -0,0 +1,352 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.telemetry; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.util.JsonUtils; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.v1.EntityDataProto; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Slf4j +public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { + + private final Gson gson = new Gson(); + + private TbQueueProducer> tbCoreMsgProducer; + + @PostConstruct + public void init() { + tbCoreMsgProducer = producerProvider.getTbCoreMsgProducer(); + } + + abstract protected String getMsgSourceKey(); + + public List> processTelemetryMsg(TenantId tenantId, EntityDataProto entityData) { + log.trace("[{}] processTelemetryMsg [{}]", tenantId, entityData); + List> result = new ArrayList<>(); + EntityId entityId = constructEntityId(entityData.getEntityType(), entityData.getEntityIdMSB(), entityData.getEntityIdLSB()); + if (entityId != null && isEntityExists(tenantId, entityId)) { + if ((entityData.hasPostAttributesMsg() || entityData.hasPostTelemetryMsg() || entityData.hasAttributesUpdatedMsg())) { + Pair pair = getBaseMsgMetadataAndCustomerId(tenantId, entityId); + TbMsgMetaData metaData = pair.getKey(); + CustomerId customerId = pair.getValue(); + metaData.putValue(DataConstants.MSG_SOURCE_KEY, getMsgSourceKey()); + if (entityData.hasPostAttributesMsg()) { + result.add(processPostAttributes(tenantId, customerId, entityId, entityData.getPostAttributesMsg(), metaData)); + } + if (entityData.hasAttributesUpdatedMsg()) { + metaData.putValue("scope", entityData.getPostAttributeScope()); + result.add(processAttributesUpdate(tenantId, customerId, entityId, entityData.getAttributesUpdatedMsg(), metaData)); + } + if (entityData.hasPostTelemetryMsg()) { + result.add(processPostTelemetry(tenantId, customerId, entityId, entityData.getPostTelemetryMsg(), metaData)); + } + if (EntityType.DEVICE.equals(entityId.getEntityType())) { + DeviceId deviceId = new DeviceId(entityId.getId()); + + long currentTs = System.currentTimeMillis(); + + TransportProtos.DeviceActivityProto deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastActivityTime(currentTs).build(); + + log.trace("[{}][{}] device activity time is going to be updated, ts {}", tenantId, deviceId, currentTs); + + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); + tbCoreMsgProducer.send(tpi, new TbProtoQueueMsg<>(deviceId.getId(), + TransportProtos.ToCoreMsg.newBuilder().setDeviceActivityMsg(deviceActivityMsg).build()), null); + } + } + if (entityData.hasAttributeDeleteMsg()) { + result.add(processAttributeDeleteMsg(tenantId, entityId, entityData.getAttributeDeleteMsg(), entityData.getEntityType())); + } + } else { + log.warn("Skipping telemetry update msg because entity doesn't exists on edge, {}", entityData); + } + return result; + } + + private Pair getBaseMsgMetadataAndCustomerId(TenantId tenantId, EntityId entityId) { + TbMsgMetaData metaData = new TbMsgMetaData(); + CustomerId customerId = null; + switch (entityId.getEntityType()) { + case DEVICE: + Device device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); + if (device != null) { + customerId = device.getCustomerId(); + metaData.putValue("deviceName", device.getName()); + metaData.putValue("deviceType", device.getType()); + } + break; + case ASSET: + Asset asset = assetService.findAssetById(tenantId, new AssetId(entityId.getId())); + if (asset != null) { + customerId = asset.getCustomerId(); + metaData.putValue("assetName", asset.getName()); + metaData.putValue("assetType", asset.getType()); + } + break; + case ENTITY_VIEW: + EntityView entityView = entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())); + if (entityView != null) { + customerId = entityView.getCustomerId(); + metaData.putValue("entityViewName", entityView.getName()); + metaData.putValue("entityViewType", entityView.getType()); + } + break; + case EDGE: + Edge edge = edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())); + if (edge != null) { + customerId = edge.getCustomerId(); + metaData.putValue("edgeName", edge.getName()); + metaData.putValue("edgeType", edge.getType()); + } + break; + default: + log.debug("Using empty metadata for entityId [{}]", entityId); + break; + } + return new ImmutablePair<>(metaData, customerId != null ? customerId : new CustomerId(ModelConstants.NULL_UUID)); + } + + private ListenableFuture processPostTelemetry(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostTelemetryMsg msg, TbMsgMetaData metaData) { + SettableFuture futureToSet = SettableFuture.create(); + for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { + JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); + metaData.putValue("ts", tsKv.getTs() + ""); + var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process post telemetry [{}]", msg, t); + futureToSet.setException(t); + } + }); + } + return futureToSet; + } + + private Pair getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) { + RuleChainId ruleChainId = null; + String queueName = null; + if (EntityType.DEVICE.equals(entityId.getEntityType())) { + DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); + if (deviceProfile == null) { + log.warn("[{}] Device profile is null!", entityId); + } else { + ruleChainId = deviceProfile.getDefaultRuleChainId(); + queueName = deviceProfile.getDefaultQueueName(); + } + } else if (EntityType.ASSET.equals(entityId.getEntityType())) { + AssetProfile assetProfile = assetProfileCache.get(tenantId, new AssetId(entityId.getId())); + if (assetProfile == null) { + log.warn("[{}] Asset profile is null!", entityId); + } else { + ruleChainId = assetProfile.getDefaultRuleChainId(); + queueName = assetProfile.getDefaultQueueName(); + } + } + return new ImmutablePair<>(queueName, ruleChainId); + } + + private ListenableFuture processPostAttributes(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) { + SettableFuture futureToSet = SettableFuture.create(); + JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); + var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process post attributes [{}]", msg, t); + futureToSet.setException(t); + } + }); + return futureToSet; + } + + private ListenableFuture processAttributesUpdate(TenantId tenantId, + CustomerId customerId, + EntityId entityId, + TransportProtos.PostAttributeMsg msg, + TbMsgMetaData metaData) { + SettableFuture futureToSet = SettableFuture.create(); + JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); + List attributes = new ArrayList<>(JsonConverter.convertToAttributes(json)); + String scope = metaData.getValue("scope"); + tsSubService.saveAndNotify(tenantId, entityId, scope, attributes, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void tmp) { + var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), DataConstants.ATTRIBUTES_UPDATED, entityId, + customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attributes update [{}]", msg, t); + futureToSet.setException(t); + } + }); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attributes update [{}]", msg, t); + futureToSet.setException(t); + } + }); + return futureToSet; + } + + private ListenableFuture processAttributeDeleteMsg(TenantId tenantId, EntityId entityId, AttributeDeleteMsg attributeDeleteMsg, + String entityType) { + + String scope = attributeDeleteMsg.getScope(); + List attributeKeys = attributeDeleteMsg.getAttributeNamesList(); + ListenableFuture> removeAllFuture = attributesService.removeAll(tenantId, entityId, scope, attributeKeys); + return Futures.transformAsync(removeAllFuture, removeAttributes -> { + if (EntityType.DEVICE.name().equals(entityType)) { + SettableFuture futureToSet = SettableFuture.create(); + tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete( + tenantId, (DeviceId) entityId, scope, attributeKeys), new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attribute delete msg [{}]", attributeDeleteMsg, t); + futureToSet.setException(t); + } + }); + return futureToSet; + } else { + return Futures.immediateFuture(null); + } + }, dbCallbackExecutorService); + } + + public EntityDataProto convertTelemetryEventToEntityDataProto(EntityType entityType, + UUID entityUUID, + EdgeEventActionType actionType, + JsonNode body) throws JsonProcessingException { + EntityId entityId; + switch (entityType) { + case DEVICE: + entityId = new DeviceId(entityUUID); + break; + case ASSET: + entityId = new AssetId(entityUUID); + break; + case ENTITY_VIEW: + entityId = new EntityViewId(entityUUID); + break; + case DASHBOARD: + entityId = new DashboardId(entityUUID); + break; + case TENANT: + entityId = TenantId.fromUUID(entityUUID); + break; + case CUSTOMER: + entityId = new CustomerId(entityUUID); + break; + case USER: + entityId = new UserId(entityUUID); + break; + case EDGE: + entityId = new EdgeId(entityUUID); + break; + default: + log.warn("Unsupported edge event type [{}]", entityType); + return null; + } + JsonElement entityData = JsonParser.parseString(JacksonUtil.OBJECT_MAPPER.writeValueAsString(body)); + return entityDataMsgConstructor.constructEntityDataMsg(entityId, actionType, entityData); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java new file mode 100644 index 0000000000..66c5596545 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.telemetry; + +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EntityDataProto; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@Slf4j +@TbCoreComponent +public class TelemetryEdgeProcessor extends BaseTelemetryProcessor { + + @Override + protected String getMsgSourceKey() { + return DataConstants.EDGE_MSG_SOURCE; + } + + public DownlinkMsg convertTelemetryEventToDownlink(EdgeEvent edgeEvent) throws JsonProcessingException { + EntityType entityType = EntityType.valueOf(edgeEvent.getType().name()); + EntityDataProto entityDataProto = convertTelemetryEventToEntityDataProto(entityType, edgeEvent.getEntityId(), + edgeEvent.getAction(), edgeEvent.getBody()); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addEntityData(entityDataProto) + .build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java new file mode 100644 index 0000000000..9070686fd6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.user; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class UserEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertUserEventToDownlink(EdgeEvent edgeEvent) { + UserId userId = new UserId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + User user = userService.findUserById(edgeEvent.getTenantId(), userId); + if (user != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addUserUpdateMsg(userMsgConstructor.constructUserUpdatedMsg(msgType, user)) + .build(); + } + break; + case DELETED: + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addUserUpdateMsg(userMsgConstructor.constructUserDeleteMsg(userId)) + .build(); + break; + case CREDENTIALS_UPDATED: + UserCredentials userCredentialsByUserId = userService.findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); + if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { + UserCredentialsUpdateMsg userCredentialsUpdateMsg = + userMsgConstructor.constructUserCredentialsUpdatedMsg(userCredentialsByUserId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) + .build(); + } + } + return downlinkMsg; + } + + public ListenableFuture processUserNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java new file mode 100644 index 0000000000..a429816a31 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.widget; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertWidgetsBundleEventToDownlink(EdgeEvent edgeEvent) { + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(edgeEvent.getTenantId(), widgetsBundleId); + if (widgetsBundle != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = + widgetsBundleMsgConstructor.constructWidgetsBundleUpdateMsg(msgType, widgetsBundle); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) + .build(); + } + break; + case DELETED: + WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = + widgetsBundleMsgConstructor.constructWidgetsBundleDeleteMsg(widgetsBundleId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processWidgetsBundleNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java new file mode 100644 index 0000000000..3caad57081 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.widget; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertWidgetTypeEventToDownlink(EdgeEvent edgeEvent) { + WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED: + case UPDATED: + WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(edgeEvent.getTenantId(), widgetTypeId); + if (widgetTypeDetails != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + WidgetTypeUpdateMsg widgetTypeUpdateMsg = + widgetTypeMsgConstructor.constructWidgetTypeUpdateMsg(msgType, widgetTypeDetails); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) + .build(); + } + break; + case DELETED: + WidgetTypeUpdateMsg widgetTypeUpdateMsg = + widgetTypeMsgConstructor.constructWidgetTypeDeleteMsg(widgetTypeId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + + public ListenableFuture processWidgetTypeNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 9210d3ceec..700e0f3861 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -52,13 +53,10 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.asset.AssetProfileService; -import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.device.DeviceProfileService; -import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; @@ -93,24 +91,15 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { private AttributesService attributesService; @Autowired - private RelationService relationService; - + private TimeseriesService timeseriesService; + @Autowired - private DeviceService deviceService; - - @Autowired - private AssetService assetService; + private RelationService relationService; @Lazy @Autowired private TbEntityViewService entityViewService; - @Autowired - private DeviceProfileService deviceProfileService; - - @Autowired - private AssetProfileService assetProfileService; - @Autowired private WidgetsBundleService widgetsBundleService; @@ -141,76 +130,92 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { EntityId entityId = EntityIdFactory.getByTypeAndUuid( EntityType.valueOf(attributesRequestMsg.getEntityType()), new UUID(attributesRequestMsg.getEntityIdMSB(), attributesRequestMsg.getEntityIdLSB())); - final EdgeEventType type = EdgeUtils.getEdgeEventTypeByEntityType(entityId.getEntityType()); - if (type == null) { + final EdgeEventType entityType = EdgeUtils.getEdgeEventTypeByEntityType(entityId.getEntityType()); + if (entityType == null) { log.warn("[{}] Type doesn't supported {}", tenantId, entityId.getEntityType()); return Futures.immediateFuture(null); } - SettableFuture futureToSet = SettableFuture.create(); String scope = attributesRequestMsg.getScope(); ListenableFuture> findAttrFuture = attributesService.findAll(tenantId, entityId, scope); - Futures.addCallback(findAttrFuture, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable List ssAttributes) { - if (ssAttributes == null || ssAttributes.isEmpty()) { - log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId, - edge.getName(), - entityId.getEntityType(), - entityId.getId()); - futureToSet.set(null); - return; - } + return Futures.transformAsync(findAttrFuture, ssAttributes + -> processEntityAttributesAndAddToEdgeQueue(tenantId, entityId, edge, entityType, scope, ssAttributes, attributesRequestMsg), + dbCallbackExecutorService); + } - try { - Map entityData = new HashMap<>(); - ObjectNode attributes = JacksonUtil.OBJECT_MAPPER.createObjectNode(); - for (AttributeKvEntry attr : ssAttributes) { - if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey())) { - continue; - } - if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) { - attributes.put(attr.getKey(), attr.getBooleanValue().get()); - } else if (attr.getDataType() == DataType.DOUBLE && attr.getDoubleValue().isPresent()) { - attributes.put(attr.getKey(), attr.getDoubleValue().get()); - } else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) { - attributes.put(attr.getKey(), attr.getLongValue().get()); - } else { - attributes.put(attr.getKey(), attr.getValueAsString()); - } + private ListenableFuture processEntityAttributesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge, + EdgeEventType entityType, String scope, List ssAttributes, + AttributesRequestMsg attributesRequestMsg) { + try { + ListenableFuture future; + if (ssAttributes == null || ssAttributes.isEmpty()) { + log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId, + edge.getName(), + entityId.getEntityType(), + entityId.getId()); + future = Futures.immediateFuture(null); + } else { + Map entityData = new HashMap<>(); + ObjectNode attributes = JacksonUtil.newObjectNode(); + for (AttributeKvEntry attr : ssAttributes) { + if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey()) + && !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) { + continue; } + if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) { + attributes.put(attr.getKey(), attr.getBooleanValue().get()); + } else if (attr.getDataType() == DataType.DOUBLE && attr.getDoubleValue().isPresent()) { + attributes.put(attr.getKey(), attr.getDoubleValue().get()); + } else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) { + attributes.put(attr.getKey(), attr.getLongValue().get()); + } else { + attributes.put(attr.getKey(), attr.getValueAsString()); + } + } + if (attributes.size() > 0) { entityData.put("kv", attributes); entityData.put("scope", scope); JsonNode body = JacksonUtil.OBJECT_MAPPER.valueToTree(entityData); log.debug("Sending attributes data msg, entityId [{}], attributes [{}]", entityId, body); - ListenableFuture future = saveEdgeEvent(tenantId, edge.getId(), type, EdgeEventActionType.ATTRIBUTES_UPDATED, entityId, body); - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable Void unused) { - futureToSet.set(null); - } - - @Override - public void onFailure(Throwable throwable) { - String errMsg = String.format("[%s] Failed to save edge event [%s]", edge.getId(), attributesRequestMsg); - log.error(errMsg, throwable); - futureToSet.setException(new RuntimeException(errMsg, throwable)); - } - }, dbCallbackExecutorService); - } catch (Exception e) { - String errMsg = String.format("[%s] Failed to save attribute updates to the edge [%s]", edge.getId(), attributesRequestMsg); - log.error(errMsg, e); - futureToSet.setException(new RuntimeException(errMsg, e)); + future = saveEdgeEvent(tenantId, edge.getId(), entityType, EdgeEventActionType.ATTRIBUTES_UPDATED, entityId, body); + } else { + future = Futures.immediateFuture(null); } } - - @Override - public void onFailure(Throwable t) { - String errMsg = String.format("[%s] Can't find attributes [%s]", edge.getId(), attributesRequestMsg); - log.error(errMsg, t); - futureToSet.setException(new RuntimeException(errMsg, t)); + return Futures.transformAsync(future, v -> processLatestTimeseriesAndAddToEdgeQueue(tenantId, entityId, edge, entityType), dbCallbackExecutorService); + } catch (Exception e) { + String errMsg = String.format("[%s] Failed to save attribute updates to the edge [%s]", edge.getId(), attributesRequestMsg); + log.error(errMsg, e); + return Futures.immediateFailedFuture(new RuntimeException(errMsg, e)); + } + } + + private ListenableFuture processLatestTimeseriesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge, + EdgeEventType entityType) { + ListenableFuture> getAllLatestFuture = timeseriesService.findAllLatest(tenantId, entityId); + return Futures.transformAsync(getAllLatestFuture, tsKvEntries -> { + if (tsKvEntries == null || tsKvEntries.isEmpty()) { + log.trace("[{}][{}] No timeseries found for entity {} [{}]", tenantId, + edge.getName(), + entityId.getEntityType(), + entityId.getId()); + return Futures.immediateFuture(null); } + Map> tsData = new HashMap<>(); + for (TsKvEntry tsKvEntry : tsKvEntries) { + if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(tsKvEntry.getKey())) { + continue; + } + tsData.computeIfAbsent(tsKvEntry.getTs(), k -> new HashMap<>()).put(tsKvEntry.getKey(), tsKvEntry.getValue()); + } + List> futures = new ArrayList<>(); + for (Map.Entry> entry : tsData.entrySet()) { + Map entityBody = new HashMap<>(); + entityBody.put("data", entry.getValue()); + entityBody.put("ts", entry.getKey()); + futures.add(saveEdgeEvent(tenantId, edge.getId(), entityType, EdgeEventActionType.TIMESERIES_UPDATED, entityId, JacksonUtil.valueToTree(entityBody))); + } + return Futures.transform(Futures.allAsList(futures), v -> null, dbCallbackExecutorService); }, dbCallbackExecutorService); - return futureToSet; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java index 4d512234bd..a8932aa993 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 9f34c061fe..11ff6a3321 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -62,18 +63,18 @@ public abstract class AbstractTbEntityService { protected EdgeService edgeService; @Autowired protected AlarmService alarmService; - @Autowired + @Autowired @Lazy protected AlarmSubscriptionService alarmSubscriptionService; @Autowired protected CustomerService customerService; @Autowired protected TbClusterService tbClusterService; - @Autowired(required = false) + @Autowired(required = false) @Lazy private EntitiesVersionControlService vcService; protected ListenableFuture removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { ListenableFuture> alarmsFuture = - alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, false)); + alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false)); ListenableFuture> alarmIdsFuture = Futures.transform(alarmsFuture, page -> page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index 8f8531a5b4..0a75404fb4 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,8 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -130,13 +132,9 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS public void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, CustomerId customerId, E entity, ActionType actionType, - User user, boolean sendToEdge, - Object... additionalInfo) { + User user, Object... additionalInfo) { logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); - - if (sendToEdge) { - sendEntityNotificationMsg(tenantId, entityId, edgeTypeByActionType(actionType), JacksonUtil.toString(customerId)); - } + sendEntityNotificationMsg(tenantId, entityId, edgeTypeByActionType(actionType), JacksonUtil.toString(customerId)); } @Override @@ -224,11 +222,16 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } @Override - public void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, User user, Object... additionalInfo) { + public void notifyCreateOrUpdateAlarm(AlarmInfo alarm, ActionType actionType, User user, Object... additionalInfo) { logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), actionType, user, additionalInfo); sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType(actionType)); } + @Override + public void notifyAlarmComment(Alarm alarm, AlarmComment alarmComment, ActionType actionType, User user) { + logEntityAction(alarm.getTenantId(), alarm.getId(), alarm, alarm.getCustomerId(), actionType, user, alarmComment); + } + @Override public void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId, I entityId, E entity, User user, @@ -306,6 +309,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS return EdgeEventActionType.ALARM_ACK; case ALARM_CLEAR: return EdgeEventActionType.ALARM_CLEAR; + case ALARM_ASSIGNED: + return EdgeEventActionType.ALARM_ASSIGNED; + case ALARM_UNASSIGNED: + return EdgeEventActionType.ALARM_UNASSIGNED; case DELETED: return EdgeEventActionType.DELETED; case RELATION_ADD_OR_UPDATE: diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java index 7c265a4fa3..609a61b903 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index dc6e148cab..c5f8f85831 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -73,8 +75,7 @@ public interface TbNotificationEntityService { void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, CustomerId customerId, E entity, ActionType actionType, - User user, boolean sendToEdge, - Object... additionalInfo); + User user, Object... additionalInfo); void notifyAssignOrUnassignEntityToEdge(TenantId tenantId, I entityId, CustomerId customerId, EdgeId edgeId, @@ -100,7 +101,10 @@ public interface TbNotificationEntityService { void notifyCreateOrUpdateOrDeleteEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType, User user, Object... additionalInfo); - void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, User user, Object... additionalInfo); + void notifyCreateOrUpdateAlarm(AlarmInfo alarm, ActionType actionType, User user, Object... additionalInfo); + + void notifyAlarmComment(Alarm alarm, AlarmComment alarmComment, ActionType actionType, User user); + void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId, I entityId, E entity, User user, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java new file mode 100644 index 0000000000..6dfc81c747 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.alarm; + +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.dao.alarm.AlarmCommentService; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +@Service +@AllArgsConstructor +public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService{ + + @Autowired + private AlarmCommentService alarmCommentService; + + @Override + public AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException { + ActionType actionType = alarmComment.getId() == null ? ActionType.ADDED_COMMENT : ActionType.UPDATED_COMMENT; + if (user != null) { + alarmComment.setUserId(user.getId()); + } + try { + AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment)); + notificationEntityService.notifyAlarmComment(alarm, savedAlarmComment, actionType, user); + return savedAlarmComment; + } catch (Exception e) { + notificationEntityService.logEntityAction(alarm.getTenantId(), emptyId(EntityType.ALARM), alarm, actionType, user, e, alarmComment); + throw e; + } + } + + @Override + public void deleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException { + if (alarmComment.getType() == AlarmCommentType.OTHER) { + alarmComment.setType(AlarmCommentType.SYSTEM); + alarmComment.setUserId(null); + alarmComment.setComment(JacksonUtil.newObjectNode().put("text", + String.format("User %s deleted his comment", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))); + AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.saveAlarmComment(alarm.getTenantId(), alarmComment)); + notificationEntityService.notifyAlarmComment(alarm, savedAlarmComment, ActionType.DELETED_COMMENT, user); + } else { + throw new ThingsboardException("System comment could not be deleted", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 8157eff3e5..caf65d39ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,36 +15,72 @@ */ package org.thingsboard.server.service.entitiy.alarm; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import java.util.List; @Service @AllArgsConstructor +@Slf4j public class DefaultTbAlarmService extends AbstractTbEntityService implements TbAlarmService { + @Autowired + protected TbAlarmCommentService alarmCommentService; + @Override public Alarm save(Alarm alarm, User user) throws ThingsboardException { ActionType actionType = alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = alarm.getTenantId(); try { - Alarm savedAlarm = checkNotNull(alarmSubscriptionService.createOrUpdateAlarm(alarm)); - notificationEntityService.notifyCreateOrUpdateAlarm(savedAlarm, actionType, user); - return savedAlarm; + AlarmApiCallResult result; + if (alarm.getId() == null) { + result = alarmSubscriptionService.createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(alarm, user.getId())); + } else { + result = alarmSubscriptionService.updateAlarm(AlarmUpdateRequest.fromAlarm(alarm, user.getId())); + } + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + actionType = result.isCreated() ? ActionType.ADDED : ActionType.UPDATED; + if (result.isModified()) { + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), actionType, user); + } + AlarmInfo resultAlarm = result.getAlarm(); + if (alarm.isAcknowledged() && !resultAlarm.isAcknowledged()) { + resultAlarm = ack(resultAlarm, alarm.getAckTs(), user); + } + if (alarm.isCleared() && !resultAlarm.isCleared()) { + resultAlarm = clear(resultAlarm, alarm.getClearTs(), user); + } + UserId newAssignee = alarm.getAssigneeId(); + UserId curAssignee = resultAlarm.getAssigneeId(); + if (newAssignee != null && !newAssignee.equals(curAssignee)) { + resultAlarm = assign(resultAlarm, newAssignee, alarm.getAssignTs(), user); + } else if (newAssignee == null && curAssignee != null) { + resultAlarm = unassign(alarm, alarm.getAssignTs(), user); + } + return new Alarm(resultAlarm); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ALARM), alarm, actionType, user, e); throw e; @@ -52,27 +88,126 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } @Override - public ListenableFuture ack(Alarm alarm, User user) { - long ackTs = System.currentTimeMillis(); - ListenableFuture future = alarmSubscriptionService.ackAlarm(alarm.getTenantId(), alarm.getId(), ackTs); - return Futures.transform(future, result -> { - alarm.setAckTs(ackTs); - alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); - notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_ACK, user); - return null; - }, MoreExecutors.directExecutor()); + public AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException { + return ack(alarm, System.currentTimeMillis(), user); + } + + @Override + public AlarmInfo ack(Alarm alarm, long ackTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(ackTs)); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + if (result.isModified()) { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "ACK")) + .build(); + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment, user); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ACK, user); + } else { + throw new ThingsboardException("Alarm was already acknowledged!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return result.getAlarm(); + } + + @Override + public AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException { + return clear(alarm, System.currentTimeMillis(), user); } @Override - public ListenableFuture clear(Alarm alarm, User user) { - long clearTs = System.currentTimeMillis(); - ListenableFuture future = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), null, clearTs); - return Futures.transform(future, result -> { - alarm.setClearTs(clearTs); - alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); - notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_CLEAR, user); - return null; - }, MoreExecutors.directExecutor()); + public AlarmInfo clear(Alarm alarm, long clearTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(clearTs), null); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + if (result.isCleared()) { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "CLEAR")) + .build(); + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment, user); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_CLEAR, user); + } else { + throw new ThingsboardException("Alarm was already cleared!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return result.getAlarm(); + } + + @Override + public AlarmInfo assign(Alarm alarm, UserId assigneeId, long assignTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.assignAlarm(alarm.getTenantId(), alarm.getId(), assigneeId, getOrDefault(assignTs)); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + AlarmInfo alarmInfo = result.getAlarm(); + if (result.isModified()) { + AlarmAssignee assignee = alarmInfo.getAssignee(); + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was assigned by user %s to user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName(), + (assignee.getFirstName() == null || assignee.getLastName() == null) ? assignee.getEmail() : assignee.getFirstName() + " " + assignee.getLastName())) + .put("userId", user.getId().toString()) + .put("assigneeId", assignee.getId().toString()) + .put("subtype", "ASSIGN")) + .build(); + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment, user); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGNED, user); + } else { + throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmInfo; + } + + @Override + public AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(unassignTs)); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + AlarmInfo alarmInfo = result.getAlarm(); + if (result.isModified()) { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned by user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "ASSIGN")) + .build(); + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment, user); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user); + } else { + throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmInfo; } @Override @@ -83,4 +218,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb relatedEdgeIds, user, JacksonUtil.toString(alarm)); return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId()); } -} \ No newline at end of file + + private static long getOrDefault(long ts) { + return ts > 0 ? ts : System.currentTimeMillis(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java new file mode 100644 index 0000000000..a2bca133cb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.alarm; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.exception.ThingsboardException; + +public interface TbAlarmCommentService { + AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException; + + void deleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException; +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index 8cd8d0b49d..a2ae9c8cc7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,27 @@ */ package org.thingsboard.server.service.entitiy.alarm; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.UserId; public interface TbAlarmService { Alarm save(Alarm entity, User user) throws ThingsboardException; - ListenableFuture ack(Alarm alarm, User user); + AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException; - ListenableFuture clear(Alarm alarm, User user); + AlarmInfo ack(Alarm alarm, long ackTs, User user) throws ThingsboardException; + + AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException; + + AlarmInfo clear(Alarm alarm, long clearTs, User user) throws ThingsboardException; + + AlarmInfo assign(Alarm alarm, UserId assigneeId, long assignTs, User user) throws ThingsboardException; + + AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException; Boolean delete(Alarm alarm, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 22edb2a01f..ab57ecd050 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,7 +98,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb try { Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset, - actionType, user, true, assetId.toString(), customerId.toString(), customer.getName()); + actionType, user, assetId.toString(), customerId.toString(), customer.getName()); return savedAsset; } catch (Exception e) { @@ -115,7 +115,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(tenantId, assetId)); CustomerId customerId = customer.getId(); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset, - actionType, user, true, assetId.toString(), customerId.toString(), customer.getName()); + actionType, user, assetId.toString(), customerId.toString(), customer.getName()); return savedAsset; } catch (Exception e) { @@ -131,7 +131,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, publicCustomer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, savedAsset.getCustomerId(), savedAsset, - actionType, user, false, actionType.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); + actionType, user, assetId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedAsset; } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java index 303aabc35b..b966eb8c6a 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java index d053b99a3b..b662994451 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java index e451b3245a..191acf686d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index 986d67dde7..b71cfd222e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java index 870de10efc..e4570e73ee 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index 7fadab17ba..fa5b967bd0 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,7 +84,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement try { Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, - actionType, user, true, dashboardId.toString(), customerId.toString(), customer.getName()); + actionType, user, dashboardId.toString(), customerId.toString(), customer.getName()); return savedDashboard; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, @@ -102,7 +102,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, publicCustomer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, publicCustomer.getId(), savedDashboard, - actionType, user, false, dashboardId.toString(), + actionType, user, dashboardId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDashboard; } catch (Exception e) { @@ -120,7 +120,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, publicCustomer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, publicCustomer.getId(), dashboard, - actionType, user, false, dashboardId.toString(), + actionType, user, dashboardId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDashboard; } catch (Exception e) { @@ -160,14 +160,14 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, - actionType, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); + actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; for (CustomerId customerId : removedCustomerIds) { ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, - ActionType.UNASSIGNED_FROM_CUSTOMER, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); + ActionType.UNASSIGNED_FROM_CUSTOMER, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } return savedDashboard; } @@ -197,7 +197,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, - actionType, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); + actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } return savedDashboard; } @@ -227,7 +227,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, - actionType, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); + actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } return savedDashboard; } @@ -282,7 +282,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement try { Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customer.getId(), savedDashboard, - actionType, user, true, dashboardId.toString(), customer.getId().toString(), customer.getName()); + actionType, user, dashboardId.toString(), customer.getId().toString(), customer.getName()); return savedDashboard; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, user, e, dashboardId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java index 73d7ea5d44..fffe50014d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java index f72092bedf..2ab8de9438 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,17 +76,18 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T @Override public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException { - ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + boolean isCreate = device.getId() == null; + ActionType actionType = isCreate ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = device.getTenantId(); try { + Device oldDevice = isCreate ? null : deviceService.findDeviceById(tenantId, device.getId()); Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); notificationEntityService.notifyCreateOrUpdateDevice(tenantId, savedDevice.getId(), savedDevice.getCustomerId(), - savedDevice, device, actionType, user); + savedDevice, oldDevice, actionType, user); return savedDevice; } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE), device, - actionType, user, e); + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE), device, actionType, user, e); throw e; } } @@ -116,7 +117,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T try { Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice, - actionType, user, true, deviceId.toString(), customerId.toString(), customer.getName()); + actionType, user, deviceId.toString(), customerId.toString(), customer.getName()); return savedDevice; } catch (Exception e) { @@ -136,7 +137,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T CustomerId customerId = customer.getId(); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice, - actionType, user, true, deviceId.toString(), customerId.toString(), customer.getName()); + actionType, user, deviceId.toString(), customerId.toString(), customer.getName()); return savedDevice; } catch (Exception e) { @@ -154,7 +155,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, publicCustomer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, savedDevice.getCustomerId(), savedDevice, - actionType, user, false, deviceId.toString(), + actionType, user, deviceId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDevice; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java index 3ffc120067..e9665c6e3e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java index 650b35db0d..faa6a69983 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/TbDeviceProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/TbDeviceProfileService.java index 03ad8f75cb..fdcbec9c0c 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/TbDeviceProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/TbDeviceProfileService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java index 697fe35a42..c51ae3352e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,6 +48,9 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE ActionType actionType = edge.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = edge.getTenantId(); try { + if (actionType == ActionType.ADDED && edge.getRootRuleChainId() == null) { + edge.setRootRuleChainId(edgeTemplateRootRuleChain.getId()); + } Edge savedEdge = checkNotNull(edgeService.saveEdge(edge)); EdgeId edgeId = savedEdge.getId(); @@ -87,7 +90,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE try { Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(tenantId, edgeId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, edgeId, customerId, savedEdge, - actionType, user, true, edgeId.toString(), customerId.toString(), customer.getName()); + actionType, user, edgeId.toString(), customerId.toString(), customer.getName()); return savedEdge; } catch (Exception e) { @@ -106,7 +109,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE try { Edge savedEdge = checkNotNull(edgeService.unassignEdgeFromCustomer(tenantId, edgeId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, edgeId, customerId, savedEdge, - actionType, user, true, edgeId.toString(), customerId.toString(), customer.getName()); + actionType, user, edgeId.toString(), customerId.toString(), customer.getName()); return savedEdge; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.EDGE), diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java index b7cc0a5072..f458be5a1f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java index 8a32392ebc..b978b730fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java index 09dfd35fe4..2caee86d0c 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java index 3ae5fe67f2..48e6c4b634 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -149,7 +149,7 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen try { EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId, entityViewId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customerId, savedEntityView, - ActionType.ASSIGNED_TO_CUSTOMER, user, true, entityViewId.toString(), customerId.toString(), customer.getName()); + ActionType.ASSIGNED_TO_CUSTOMER, user, entityViewId.toString(), customerId.toString(), customer.getName()); return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), @@ -159,18 +159,19 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen } @Override - public EntityView assignEntityViewToPublicCustomer(TenantId tenantId, CustomerId customerId, Customer publicCustomer, - EntityViewId entityViewId, User user) throws ThingsboardException { + public EntityView assignEntityViewToPublicCustomer(TenantId tenantId, EntityViewId entityViewId, User user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); try { EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId, entityViewId, publicCustomer.getId())); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customerId, savedEntityView, - ActionType.ASSIGNED_TO_CUSTOMER, user, false, savedEntityView.getEntityId().toString(), + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, savedEntityView.getCustomerId(), savedEntityView, + actionType, user, savedEntityView.getId().toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), - ActionType.ASSIGNED_TO_CUSTOMER, user, e, entityViewId.toString()); + actionType, user, e, entityViewId.toString()); throw e; } } @@ -211,14 +212,17 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen @Override public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, User user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; try { EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromCustomer(tenantId, entityViewId)); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customer.getId(), savedEntityView, - ActionType.UNASSIGNED_FROM_CUSTOMER, user, true, customer.getId().toString(), customer.getName()); + actionType, user, savedEntityView.getId().toString(), customer.getId().toString(), customer.getName()); + return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), - ActionType.UNASSIGNED_FROM_CUSTOMER, user, e, entityViewId.toString()); + actionType, user, e, entityViewId.toString()); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java index c4dbedd2eb..d0c18ce98f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,8 +39,7 @@ public interface TbEntityViewService extends ComponentLifecycleListener { EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, User user) throws ThingsboardException; - EntityView assignEntityViewToPublicCustomer(TenantId tenantId, CustomerId customerId, Customer publicCustomer, - EntityViewId entityViewId, User user) throws ThingsboardException; + EntityView assignEntityViewToPublicCustomer(TenantId tenantId, EntityViewId entityViewId, User user) throws ThingsboardException; EntityView assignEntityViewToEdge(TenantId tenantId, CustomerId customerId, EntityViewId entityViewId, Edge edge, User user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java index ac825c211e..4d3c61e615 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/TbOtaPackageService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/TbOtaPackageService.java index aec2f20d0c..3057da88f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/TbOtaPackageService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/TbOtaPackageService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java index 4355d998bb..63e11aeb7a 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,17 +19,13 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.scheduler.SchedulerComponent; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/TbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/TbQueueService.java index 86f82b1236..34651367c7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/TbQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/TbQueueService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java index 01a2eee664..19d4126989 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java index 2dc8fa452c..8a6f793cb0 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java index a25c4acad7..1219a5b929 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java index 96d18e3577..c45d3314cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java new file mode 100644 index 0000000000..1c71bc85a3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java @@ -0,0 +1,201 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.user; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.HasTitle; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.settings.AbstractUserDashboardInfo; +import org.thingsboard.server.common.data.settings.LastVisitedDashboardInfo; +import org.thingsboard.server.common.data.settings.StarredDashboardInfo; +import org.thingsboard.server.common.data.settings.UserDashboardAction; +import org.thingsboard.server.common.data.settings.UserDashboardsInfo; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsType; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.user.UserSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Service +@TbCoreComponent +@AllArgsConstructor +@Slf4j +public class DefaultTbUserSettingsService implements TbUserSettingsService { + + private static final int MAX_DASHBOARD_INFO_LIST_SIZE = 10; + private static final Predicate EMPTY_TITLE = i -> StringUtils.isEmpty(i.getTitle()); + + private final UserSettingsService settingsService; + private final DashboardService dashboardService; + + @Override + public UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings) { + return settingsService.saveUserSettings(tenantId, userSettings); + } + + @Override + public void updateUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, JsonNode settings) { + settingsService.updateUserSettings(tenantId, userId, type, settings); + } + + @Override + public UserSettings findUserSettings(TenantId tenantId, UserId userId, UserSettingsType type) { + return settingsService.findUserSettings(tenantId, userId, type); + } + + @Override + public void deleteUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, List jsonPaths) { + settingsService.deleteUserSettings(tenantId, userId, type, jsonPaths); + } + + @Override + public UserDashboardsInfo findUserDashboardsInfo(TenantId tenantId, UserId id) { + UserSettings us = findUserSettings(tenantId, id, UserSettingsType.VISITED_DASHBOARDS); + if (us == null) { + return UserDashboardsInfo.EMPTY; + } + UserDashboardsInfo stored = JacksonUtil.convertValue(us.getSettings(), UserDashboardsInfo.class); + return refreshDashboardTitles(tenantId, stored); + } + + @Override + public UserDashboardsInfo reportUserDashboardAction(TenantId tenantId, UserId id, DashboardId dashboardId, UserDashboardAction action) { + UserSettings us = findUserSettings(tenantId, id, UserSettingsType.VISITED_DASHBOARDS); + UserDashboardsInfo stored = null; + if (us != null) { + stored = JacksonUtil.convertValue(us.getSettings(), UserDashboardsInfo.class); + } + if (stored == null) { + stored = new UserDashboardsInfo(); + } + + switch (action) { + case STAR: + addToStarred(stored, dashboardId); + break; + case UNSTAR: + removeFromStarred(stored, dashboardId); + break; + case VISIT: + addToVisited(stored, dashboardId); + break; + } + + stored = refreshDashboardTitles(tenantId, stored); + + us = new UserSettings(); + us.setUserId(id); + us.setType(UserSettingsType.VISITED_DASHBOARDS); + us.setSettings(JacksonUtil.valueToTree(stored)); + saveUserSettings(tenantId, us); + return stored; + } + + private void addToVisited(UserDashboardsInfo stored, DashboardId dashboardId) { + UUID id = dashboardId.getId(); + long ts = System.currentTimeMillis(); + var opt = stored.getLast().stream().filter(filterById(id)).findFirst(); + if (opt.isPresent()) { + opt.get().setLastVisited(ts); + } else { + var newInfo = new LastVisitedDashboardInfo(); + newInfo.setId(id); + newInfo.setStarred(stored.getStarred().stream().anyMatch(filterById(id))); + newInfo.setLastVisited(System.currentTimeMillis()); + stored.getLast().add(newInfo); + } + stored.getLast().sort(Comparator.comparing(LastVisitedDashboardInfo::getLastVisited).reversed()); + if (stored.getLast().size() > MAX_DASHBOARD_INFO_LIST_SIZE) { + stored.setLast(stored.getLast().stream().limit(MAX_DASHBOARD_INFO_LIST_SIZE).collect(Collectors.toList())); + } + } + + private void removeFromStarred(UserDashboardsInfo stored, DashboardId dashboardId) { + UUID id = dashboardId.getId(); + stored.getStarred().removeIf(filterById(id)); + stored.getLast().stream().filter(d -> id.equals(d.getId())).findFirst().ifPresent(d -> d.setStarred(false)); + } + + private void addToStarred(UserDashboardsInfo stored, DashboardId dashboardId) { + UUID id = dashboardId.getId(); + long ts = System.currentTimeMillis(); + var opt = stored.getStarred().stream().filter(filterById(id)).findFirst(); + if (opt.isPresent()) { + opt.get().setStarredAt(ts); + } else { + var newInfo = new StarredDashboardInfo(); + newInfo.setId(id); + newInfo.setStarredAt(System.currentTimeMillis()); + stored.getStarred().add(newInfo); + } + stored.getStarred().sort(Comparator.comparing(StarredDashboardInfo::getStarredAt).reversed()); + if (stored.getStarred().size() > MAX_DASHBOARD_INFO_LIST_SIZE) { + stored.setStarred(stored.getStarred().stream().limit(MAX_DASHBOARD_INFO_LIST_SIZE).collect(Collectors.toList())); + } + Set starredMap = + stored.getStarred().stream().map(AbstractUserDashboardInfo::getId).collect(Collectors.toSet()); + stored.getLast().forEach(d -> d.setStarred(starredMap.contains(d.getId()))); + } + + private Predicate filterById(UUID id) { + return d -> id.equals(d.getId()); + } + + private UserDashboardsInfo refreshDashboardTitles(TenantId tenantId, UserDashboardsInfo stored) { + if (stored == null) { + return UserDashboardsInfo.EMPTY; + } + stored.getLast().forEach(i -> i.setTitle(null)); + stored.getStarred().forEach(i -> i.setTitle(null)); + + Set uniqueIds = new HashSet<>(); + stored.getLast().stream().map(AbstractUserDashboardInfo::getId).forEach(uniqueIds::add); + stored.getStarred().stream().map(AbstractUserDashboardInfo::getId).forEach(uniqueIds::add); + + Map dashboardTitles = new HashMap<>(); + uniqueIds.forEach(id -> { + var title = dashboardService.findDashboardTitleById(tenantId, new DashboardId(id)); + if (StringUtils.isNotEmpty(title)) { + dashboardTitles.put(id, title); + } + } + ); + + stored.getLast().forEach(i -> i.setTitle(dashboardTitles.get(i.getId()))); + stored.getLast().removeIf(EMPTY_TITLE); + stored.getStarred().forEach(i -> i.setTitle(dashboardTitles.get(i.getId()))); + stored.getStarred().removeIf(EMPTY_TITLE); + return stored; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java index a3df4b816b..c83c48cc8a 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java index 765733ba3e..3ef79371d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserSettingsService.java new file mode 100644 index 0000000000..a81dbcad2f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserSettingsService.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.user; + +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.settings.UserDashboardAction; +import org.thingsboard.server.common.data.settings.UserDashboardsInfo; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsType; + +import java.util.List; + +public interface TbUserSettingsService { + + void updateUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, JsonNode settings); + + UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings); + + UserSettings findUserSettings(TenantId tenantId, UserId userId, UserSettingsType type); + + void deleteUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, List jsonPaths); + + UserDashboardsInfo findUserDashboardsInfo(TenantId tenantId, UserId id); + + UserDashboardsInfo reportUserDashboardAction(TenantId tenantId, UserId id, DashboardId dashboardId, UserDashboardAction action); +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java index 1706598b9c..7cc150b746 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,10 @@ package org.thingsboard.server.service.entitiy.widgets.bundle; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -34,17 +35,31 @@ public class DefaultWidgetsBundleService extends AbstractTbEntityService impleme @Override public WidgetsBundle save(WidgetsBundle widgetsBundle, User user) throws Exception { - WidgetsBundle savedWidgetsBundle = checkNotNull(widgetsBundleService.saveWidgetsBundle(widgetsBundle)); - autoCommit(user, savedWidgetsBundle.getId()); - notificationEntityService.notifySendMsgToEdgeService(widgetsBundle.getTenantId(), savedWidgetsBundle.getId(), - widgetsBundle.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); - return savedWidgetsBundle; + ActionType actionType = widgetsBundle.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = widgetsBundle.getTenantId(); + try { + WidgetsBundle savedWidgetsBundle = checkNotNull(widgetsBundleService.saveWidgetsBundle(widgetsBundle)); + autoCommit(user, savedWidgetsBundle.getId()); + notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedWidgetsBundle.getId(), + savedWidgetsBundle, user, actionType, true, null); + return savedWidgetsBundle; + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.WIDGETS_BUNDLE), widgetsBundle, actionType, user, e); + throw e; + } } @Override - public void delete(WidgetsBundle widgetsBundle) throws ThingsboardException { - widgetsBundleService.deleteWidgetsBundle(widgetsBundle.getTenantId(), widgetsBundle.getId()); - notificationEntityService.notifySendMsgToEdgeService(widgetsBundle.getTenantId(), widgetsBundle.getId(), - EdgeEventActionType.DELETED); + public void delete(WidgetsBundle widgetsBundle, User user) { + TenantId tenantId = widgetsBundle.getTenantId(); + try { + widgetsBundleService.deleteWidgetsBundle(widgetsBundle.getTenantId(), widgetsBundle.getId()); + notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, widgetsBundle.getId(), widgetsBundle, + user, ActionType.DELETED, true, null); + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.WIDGETS_BUNDLE), + ActionType.DELETED, user, e, widgetsBundle.getId()); + throw e; + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java index 2820934aa3..d60bc66d66 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,8 @@ */ package org.thingsboard.server.service.entitiy.widgets.bundle; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.service.entitiy.SimpleTbEntityService; -public interface TbWidgetsBundleService { - - WidgetsBundle save(WidgetsBundle entity, User currentUser) throws Exception; - - void delete(WidgetsBundle entity) throws ThingsboardException; +public interface TbWidgetsBundleService extends SimpleTbEntityService { } diff --git a/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java index 84ac8c5ee4..910f8d7d8e 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java index 83295171d2..3e092eefae 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/executors/GrpcCallbackExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/GrpcCallbackExecutorService.java index b249ad091d..2d2e7ee9d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/GrpcCallbackExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/executors/GrpcCallbackExecutorService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/executors/NotificationExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/NotificationExecutorService.java new file mode 100644 index 0000000000..81ad36a4a1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/executors/NotificationExecutorService.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.executors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.AbstractListeningExecutor; + +@Component +public class NotificationExecutorService extends AbstractListeningExecutor { + + @Value("${notification_system.thread_pool_size:10}") + private int threadPoolSize; + + @Override + protected int getThreadPollSize() { + return threadPoolSize; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/executors/SharedEventLoopGroupService.java b/application/src/main/java/org/thingsboard/server/service/executors/SharedEventLoopGroupService.java index e812ec4e46..38b563472e 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/SharedEventLoopGroupService.java +++ b/application/src/main/java/org/thingsboard/server/service/executors/SharedEventLoopGroupService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.executors; -import com.google.common.util.concurrent.MoreExecutors; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import lombok.Getter; @@ -23,7 +22,6 @@ import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @Component diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayNotificationsService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayNotificationsService.java index 8eea83c767..52a7d3245a 100644 --- a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayNotificationsService.java +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayNotificationsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayNotificationsService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayNotificationsService.java index d6b20c8858..bb1fbe5391 100644 --- a/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayNotificationsService.java +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayNotificationsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/AbstractCassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/AbstractCassandraDatabaseUpgradeService.java index c149413f5c..ce20b2176b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/AbstractCassandraDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/AbstractCassandraDatabaseUpgradeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java index b223a20867..07ff503b4f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java index 1d843e5e2d..9d266ad4dd 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java index dfdd05e829..764abae52e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseSchemaService.java index e77a034d8a..f623aba033 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java index 25218fa35b..5c7c51eb2c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsLatestDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsLatestDatabaseSchemaService.java index 181fb41610..f95699b418 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsLatestDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsLatestDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseEntitiesUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseEntitiesUpgradeService.java index beb4135cb9..c11cc3614a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseEntitiesUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseEntitiesUpgradeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java index 2d90b8a16a..97b87c7412 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,10 @@ package org.thingsboard.server.service.install; import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.UUIDConverter; @@ -67,11 +67,10 @@ public class DatabaseHelper { public static final String ASSIGNED_CUSTOMERS = "assigned_customers"; public static final String CONFIGURATION = "configuration"; - public static final ObjectMapper objectMapper = new ObjectMapper(); public static void upgradeTo40_assignDashboards(Path dashboardsDump, DashboardService dashboardService, boolean sql) throws Exception { JavaType assignedCustomersType = - objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); + JacksonUtil.constructCollectionType(HashSet.class, ShortCustomerInfo.class); try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(dashboardsDump), CSV_DUMP_FORMAT.withFirstRecordAsHeader())) { csvParser.forEach(record -> { String customerIdString = record.get(CUSTOMER_ID); @@ -80,14 +79,14 @@ public class DatabaseHelper { List customerIds = new ArrayList<>(); if (!StringUtils.isEmpty(assignedCustomersString)) { try { - Set assignedCustomers = objectMapper.readValue(assignedCustomersString, assignedCustomersType); + Set assignedCustomers = JacksonUtil.fromString(assignedCustomersString, assignedCustomersType); assignedCustomers.forEach((customerInfo) -> { CustomerId customerId = customerInfo.getCustomerId(); if (!customerId.isNullUid()) { customerIds.add(customerId); } }); - } catch (IOException e) { + } catch (IllegalArgumentException e) { log.error("Unable to parse assigned customers field", e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaService.java index 8dde1682b2..59774b2088 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseTsUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseTsUpgradeService.java index 16c5bf9cda..38e8dd639a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseTsUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseTsUpgradeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java b/application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java index b3ce77ac7a..7718354106 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 9507912c39..497627f629 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,12 @@ */ package org.thingsboard.server.service.install; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.Getter; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -29,6 +29,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Profile; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; @@ -62,6 +63,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; @@ -82,13 +84,14 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.notification.NotificationSettingsService; +import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; @@ -97,6 +100,7 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import javax.annotation.Nullable; import javax.annotation.PostConstruct; @@ -107,13 +111,14 @@ import java.util.List; import java.util.TreeMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; @Service @Profile("install") @Slf4j public class DefaultSystemDataLoaderService implements SystemDataLoaderService { - private static final ObjectMapper objectMapper = new ObjectMapper(); public static final String CUSTOMER_CRED = "customer"; public static final String DEFAULT_DEVICE_TYPE = "default"; public static final String ACTIVITY_STATE = "active"; @@ -171,6 +176,12 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Autowired private JwtSettingsService jwtSettingsService; + @Autowired + private NotificationSettingsService notificationSettingsService; + + @Autowired + private NotificationTargetService notificationTargetService; + @Bean protected BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -203,8 +214,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { isolatedRuleEngineTenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); TenantProfileQueueConfiguration mainQueueConfiguration = new TenantProfileQueueConfiguration(); - mainQueueConfiguration.setName("Main"); - mainQueueConfiguration.setTopic("tb_rule_engine.main"); + mainQueueConfiguration.setName(DataConstants.MAIN_QUEUE_NAME); + mainQueueConfiguration.setTopic(DataConstants.MAIN_QUEUE_TOPIC); mainQueueConfiguration.setPollInterval(25); mainQueueConfiguration.setPartitions(10); mainQueueConfiguration.setConsumerPerPartition(true); @@ -242,7 +253,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { AdminSettings generalSettings = new AdminSettings(); generalSettings.setTenantId(TenantId.SYS_TENANT_ID); generalSettings.setKey("general"); - ObjectNode node = objectMapper.createObjectNode(); + ObjectNode node = JacksonUtil.newObjectNode(); node.put("baseUrl", "http://localhost:8080"); node.put("prohibitDifferentUrl", false); generalSettings.setJsonValue(node); @@ -251,7 +262,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { AdminSettings mailSettings = new AdminSettings(); mailSettings.setTenantId(TenantId.SYS_TENANT_ID); mailSettings.setKey("mail"); - node = objectMapper.createObjectNode(); + node = JacksonUtil.newObjectNode(); node.put("mailFrom", "ThingsBoard "); node.put("smtpProtocol", "smtp"); node.put("smtpHost", "localhost"); @@ -499,6 +510,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { this.deleteSystemWidgetBundle("entity_admin_widgets"); this.deleteSystemWidgetBundle("navigation_widgets"); this.deleteSystemWidgetBundle("edge_widgets"); + this.deleteSystemWidgetBundle("home_page_widgets"); installScripts.loadSystemWidgets(); } @@ -533,7 +545,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { device.setDeviceProfileId(deviceProfileId); device.setName(name); if (description != null) { - ObjectNode additionalInfo = objectMapper.createObjectNode(); + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); additionalInfo.put("description", description); device.setAdditionalInfo(additionalInfo); } @@ -598,12 +610,12 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Override public void createQueues() { - Queue mainQueue = queueService.findQueueByTenantIdAndName(TenantId.SYS_TENANT_ID, "Main"); + Queue mainQueue = queueService.findQueueByTenantIdAndName(TenantId.SYS_TENANT_ID, DataConstants.MAIN_QUEUE_NAME); if (mainQueue == null) { mainQueue = new Queue(); mainQueue.setTenantId(TenantId.SYS_TENANT_ID); - mainQueue.setName("Main"); - mainQueue.setTopic("tb_rule_engine.main"); + mainQueue.setName(DataConstants.MAIN_QUEUE_NAME); + mainQueue.setTopic(DataConstants.MAIN_QUEUE_TOPIC); mainQueue.setPollInterval(25); mainQueue.setPartitions(10); mainQueue.setConsumerPerPartition(true); @@ -622,12 +634,12 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { queueService.saveQueue(mainQueue); } - Queue highPriorityQueue = queueService.findQueueByTenantIdAndName(TenantId.SYS_TENANT_ID, "HighPriority"); + Queue highPriorityQueue = queueService.findQueueByTenantIdAndName(TenantId.SYS_TENANT_ID, DataConstants.HP_QUEUE_NAME); if (highPriorityQueue == null) { highPriorityQueue = new Queue(); highPriorityQueue.setTenantId(TenantId.SYS_TENANT_ID); - highPriorityQueue.setName("HighPriority"); - highPriorityQueue.setTopic("tb_rule_engine.hp"); + highPriorityQueue.setName(DataConstants.HP_QUEUE_NAME); + highPriorityQueue.setTopic(DataConstants.HP_QUEUE_TOPIC); highPriorityQueue.setPollInterval(25); highPriorityQueue.setPartitions(10); highPriorityQueue.setConsumerPerPartition(true); @@ -646,12 +658,12 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { queueService.saveQueue(highPriorityQueue); } - Queue sequentialByOriginatorQueue = queueService.findQueueByTenantIdAndName(TenantId.SYS_TENANT_ID, "SequentialByOriginator"); + Queue sequentialByOriginatorQueue = queueService.findQueueByTenantIdAndName(TenantId.SYS_TENANT_ID, DataConstants.SQ_QUEUE_NAME); if (sequentialByOriginatorQueue == null) { sequentialByOriginatorQueue = new Queue(); sequentialByOriginatorQueue.setTenantId(TenantId.SYS_TENANT_ID); - sequentialByOriginatorQueue.setName("SequentialByOriginator"); - sequentialByOriginatorQueue.setTopic("tb_rule_engine.sq"); + sequentialByOriginatorQueue.setName(DataConstants.SQ_QUEUE_NAME); + sequentialByOriginatorQueue.setTopic(DataConstants.SQ_QUEUE_TOPIC); sequentialByOriginatorQueue.setPollInterval(25); sequentialByOriginatorQueue.setPartitions(10); sequentialByOriginatorQueue.setPackProcessingTimeout(2000); @@ -671,4 +683,30 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { } } + @Override + @SneakyThrows + public void createDefaultNotificationConfigs() { + log.info("Creating default notification configs for system admin"); + if (notificationTargetService.countNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID) == 0) { + notificationSettingsService.createDefaultNotificationConfigs(TenantId.SYS_TENANT_ID); + } + PageDataIterable tenants = new PageDataIterable<>(tenantService::findTenantsIds, 500); + ExecutorService executor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4)); + log.info("Creating default notification configs for all tenants"); + AtomicInteger count = new AtomicInteger(); + for (TenantId tenantId : tenants) { + executor.submit(() -> { + if (notificationTargetService.countNotificationTargetsByTenantId(tenantId) == 0) { + notificationSettingsService.createDefaultNotificationConfigs(tenantId); + int n = count.incrementAndGet(); + if (n % 500 == 0) { + log.info("{} tenants processed", n); + } + } + }); + } + executor.shutdown(); + executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java index ff779d460e..35370d0854 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,4 +16,9 @@ package org.thingsboard.server.service.install; public interface EntityDatabaseSchemaService extends DatabaseSchemaService { + + void createOrUpdateDeviceInfoView(boolean activityStateInTelemetry); + + void createOrUpdateViewsAndFunctions() throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 38ff5aacec..9a810aa446 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; @@ -30,6 +34,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -41,9 +46,10 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Base64; import java.util.Optional; -import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; +import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource; /** * Created by ashvayka on 18.04.18. @@ -59,17 +65,16 @@ public class InstallScripts { public static final String JSON_DIR = "json"; public static final String SYSTEM_DIR = "system"; public static final String TENANT_DIR = "tenant"; + public static final String EDGE_DIR = "edge"; public static final String DEVICE_PROFILE_DIR = "device_profile"; public static final String DEMO_DIR = "demo"; public static final String RULE_CHAINS_DIR = "rule_chains"; public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates"; public static final String DASHBOARDS_DIR = "dashboards"; - public static final String MODELS_DIR = "models"; + public static final String MODELS_LWM2M_DIR = "lwm2m-registry"; public static final String CREDENTIALS_DIR = "credentials"; - public static final String EDGE_MANAGEMENT = "edge_management"; - public static final String JSON_EXT = ".json"; public static final String XML_EXT = ".xml"; @@ -103,7 +108,7 @@ public class InstallScripts { } private Path getEdgeRuleChainsDir() { - return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, EDGE_MANAGEMENT, RULE_CHAINS_DIR); + return Paths.get(getDataDir(), JSON_DIR, EDGE_DIR, RULE_CHAINS_DIR); } public String getDataDir() { @@ -157,9 +162,9 @@ public class InstallScripts { } public RuleChain createRuleChainFromFile(TenantId tenantId, Path templateFilePath, String newRuleChainName) throws IOException { - JsonNode ruleChainJson = objectMapper.readTree(templateFilePath.toFile()); - RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); - RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); + JsonNode ruleChainJson = JacksonUtil.toJsonNode(templateFilePath.toFile()); + RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); + RuleChainMetaData ruleChainMetaData = JacksonUtil.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); ruleChain.setTenantId(tenantId); if (!StringUtils.isEmpty(newRuleChainName)) { @@ -179,15 +184,15 @@ public class InstallScripts { dirStream.forEach( path -> { try { - JsonNode widgetsBundleDescriptorJson = objectMapper.readTree(path.toFile()); + JsonNode widgetsBundleDescriptorJson = JacksonUtil.toJsonNode(path.toFile()); JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle"); - WidgetsBundle widgetsBundle = objectMapper.treeToValue(widgetsBundleJson, WidgetsBundle.class); + WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class); WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes"); widgetTypesArrayJson.forEach( widgetTypeJson -> { try { - WidgetTypeDetails widgetTypeDetails = objectMapper.treeToValue(widgetTypeJson, WidgetTypeDetails.class); + WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); widgetTypeDetails.setBundleAlias(savedWidgetsBundle.getAlias()); widgetTypeService.saveWidgetType(widgetTypeDetails); } catch (Exception e) { @@ -211,8 +216,8 @@ public class InstallScripts { dirStream.forEach( path -> { try { - JsonNode dashboardJson = objectMapper.readTree(path.toFile()); - Dashboard dashboard = objectMapper.treeToValue(dashboardJson, Dashboard.class); + JsonNode dashboardJson = JacksonUtil.toJsonNode(path.toFile()); + Dashboard dashboard = JacksonUtil.treeToValue(dashboardJson, Dashboard.class); dashboard.setTenantId(tenantId); Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); if (customerId != null && !customerId.isNullUid()) { @@ -244,8 +249,8 @@ public class InstallScripts { dirStream.forEach( path -> { try { - JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile()); - OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class); + JsonNode oauth2ConfigTemplateJson = JacksonUtil.toJsonNode(path.toFile()); + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = JacksonUtil.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class); Optional existingClientRegistrationTemplate = oAuth2TemplateService.findClientRegistrationTemplateByProviderId(clientRegistrationTemplate.getProviderId()); if (existingClientRegistrationTemplate.isPresent()) { @@ -260,4 +265,43 @@ public class InstallScripts { ); } } + + public void loadSystemLwm2mResources() { + Path resourceLwm2mPath = Paths.get(dataDir, MODELS_LWM2M_DIR); + try (DirectoryStream dirStream = Files.newDirectoryStream(resourceLwm2mPath, path -> path.toString().endsWith(InstallScripts.XML_EXT))) { + dirStream.forEach( + path -> { + try { + String data = Base64.getEncoder().encodeToString(Files.readAllBytes(path)); + TbResource tbResource = new TbResource(); + tbResource.setTenantId(TenantId.SYS_TENANT_ID); + tbResource.setData(data); + tbResource.setResourceType(ResourceType.LWM2M_MODEL); + tbResource.setFileName(path.toFile().getName()); + doSaveLwm2mResource(tbResource); + } catch (Exception e) { + log.error("Unable to load resource lwm2m object model from file: [{}]", path.toString()); + throw new RuntimeException("resource lwm2m object model from file", e); + } + } + ); + } catch (Exception e) { + log.error("Unable to load resources lwm2m object model from file: [{}]", resourceLwm2mPath.toString()); + throw new RuntimeException("resource lwm2m object model from file", e); + } + } + + private void doSaveLwm2mResource(TbResource resource) throws ThingsboardException { + log.trace("Executing saveResource [{}]", resource); + if (StringUtils.isEmpty(resource.getData())) { + throw new DataValidationException("Resource data should be specified!"); + } + toLwm2mResource(resource); + TbResource foundResource = + resourceService.getResource(TenantId.SYS_TENANT_ID, ResourceType.LWM2M_MODEL, resource.getResourceKey()); + if (foundResource == null) { + resourceService.saveResource(resource); + } + } } + diff --git a/application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java b/application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java index 5b8d772dca..b02a202bd2 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java index 28e233c3e7..800dcd7e91 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 80665d544f..e1a34e616b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,18 +34,19 @@ import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.sql.tenant.TenantRepository; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; import org.thingsboard.server.service.install.sql.SqlDbHelper; +import org.thingsboard.server.service.install.update.DefaultDataUpdateService; import java.nio.charset.Charset; import java.nio.file.Files; @@ -637,7 +638,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService futures.add(dbUpgradeExecutor.submit(() -> { try { assetProfileService.createDefaultAssetProfile(tenantId); - } catch (Exception e) {} + } catch (Exception e) { + } })); } Futures.allAsList(futures).get(); @@ -656,7 +658,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService futures.add(dbUpgradeExecutor.submit(() -> { try { assetProfileService.findOrCreateAssetProfile(tenantId, assetType); - } catch (Exception e) {} + } catch (Exception e) { + } })); } } @@ -677,6 +680,78 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.4.4": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3004002)) { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.4", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + + try { + conn.createStatement().execute("VACUUM FULL ANALYZE alarm;"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) { + } + + try { + conn.createStatement().execute("ALTER TABLE asset_profile ADD COLUMN default_edge_rule_chain_id uuid"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) { + } + try { + conn.createStatement().execute("ALTER TABLE device_profile ADD COLUMN default_edge_rule_chain_id uuid"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) { + } + try { + conn.createStatement().execute("ALTER TABLE asset_profile ADD CONSTRAINT fk_default_edge_rule_chain_asset_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) { + } + try { + conn.createStatement().execute("ALTER TABLE device_profile ADD CONSTRAINT fk_default_edge_rule_chain_device_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) { + } + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005000;"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; + case "3.5.0": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3005000)) { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.0", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005001;"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; + case "3.5.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3005001)) { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.1", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + + String[] entityNames = new String[]{"device", "component_descriptor", "customer", "dashboard", "rule_chain", "rule_node", "ota_package", + "asset_profile", "asset", "device_profile", "tb_user", "tenant_profile", "tenant", "widgets_bundle", "entity_view", "edge"}; + for (String entityName : entityNames) { + try { + conn.createStatement().execute("ALTER TABLE " + entityName + " DROP COLUMN " + SEARCH_TEXT + " CASCADE"); + } catch (Exception e) { + } + } + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } @@ -709,6 +784,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService } protected boolean isOldSchema(Connection conn, long fromVersion) { + if (DefaultDataUpdateService.getEnv("SKIP_SCHEMA_VERSION_CHECK", false)) { + log.info("Skipped DB schema version check due to SKIP_SCHEMA_VERSION_CHECK set to true!"); + return true; + } boolean isOldSchema = true; try { Statement statement = conn.createStatement(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java index 7ae155402a..b9a62e3dfa 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer public static final String SCHEMA_ENTITIES_SQL = "schema-entities.sql"; public static final String SCHEMA_ENTITIES_IDX_SQL = "schema-entities-idx.sql"; public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql"; + public static final String SCHEMA_VIEWS_AND_FUNCTIONS_SQL = "schema-views-and-functions.sql"; public SqlEntityDatabaseSchemaService() { super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL); @@ -39,4 +40,16 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer executeQueryFromFile(SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL); } + @Override + public void createOrUpdateDeviceInfoView(boolean activityStateInTelemetry) { + String sourceViewName = activityStateInTelemetry ? "device_info_active_ts_view" : "device_info_active_attribute_view"; + executeQuery("DROP VIEW IF EXISTS device_info_view CASCADE;"); + executeQuery("CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM " + sourceViewName + ";"); + } + + @Override + public void createOrUpdateViewsAndFunctions() throws Exception { + log.info("Installing SQL DataBase schema views and functions: " + SCHEMA_VIEWS_AND_FUNCTIONS_SQL); + executeQueryFromFile(SCHEMA_VIEWS_AND_FUNCTIONS_SQL); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java index f3ede624ff..8627fd4b14 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java index 003f544b56..6400463e11 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index 041351f5e4..fb6b28592c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,4 +38,7 @@ public interface SystemDataLoaderService { void deleteSystemWidgetBundle(String bundleAlias) throws Exception; void createQueues(); + + void createDefaultNotificationConfigs(); + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/TbRuleEngineQueueConfigService.java b/application/src/main/java/org/thingsboard/server/service/install/TbRuleEngineQueueConfigService.java index 1125467082..455bd56309 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TbRuleEngineQueueConfigService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TbRuleEngineQueueConfigService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; import javax.annotation.PostConstruct; @@ -39,7 +40,7 @@ public class TbRuleEngineQueueConfigService { @PostConstruct public void validate() { - queues.stream().filter(queue -> queue.getName().equals("Main")).findFirst().orElseThrow(() -> { + queues.stream().filter(queue -> queue.getName().equals(DataConstants.MAIN_QUEUE_NAME)).findFirst().orElseThrow(() -> { log.error("Main queue is not configured in thingsboard.yml"); return new RuntimeException("No \"Main\" queue configured!"); }); diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseSchemaService.java index 409498cfd3..643fb917aa 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java index 16c054b2fc..205ec14591 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/TsDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/TsDatabaseSchemaService.java index 4274f1dc65..8e92d0956f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TsDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TsDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/TsLatestDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/TsLatestDatabaseSchemaService.java index d74afa880c..9a84e0ff81 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TsLatestDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TsLatestDatabaseSchemaService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CQLStatementsParser.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CQLStatementsParser.java index 9883009fa1..666a0b972a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/cql/CQLStatementsParser.java +++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CQLStatementsParser.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java index add28452d3..4bcd52daa9 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java index e9f72a64bf..cdaf068585 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java index a003c4c625..80a938aef1 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import org.thingsboard.server.common.data.UUIDConverter; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.sql.Types; import java.util.regex.Pattern; @Data diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnData.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnData.java index 7a6a014686..e8a1b54705 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnData.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnData.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java index 206fb2d8e5..49eb277ab8 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlEventTsColumn.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlEventTsColumn.java index 38a3225778..b9f5d64513 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlEventTsColumn.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlEventTsColumn.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java index 7f3669fc69..745e3e4e97 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java index 1aa7602a35..4357e6be63 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java index 42cdd2ebff..d4e992cffd 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/TsLatestMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/TsLatestMigrateService.java index c32f08bfc7..51ca5d012a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/TsLatestMigrateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/TsLatestMigrateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java index 83f53d9fb1..f321fb28d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/CacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/CacheCleanupService.java index 083b1d39b5..f85bc3ae22 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/CacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/CacheCleanupService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java index 909e1628ac..ae573b829a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java index 6e67876dc4..278fc0adf5 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,6 +78,14 @@ public class DefaultCacheCleanupService implements CacheCleanupService { clearCacheByName("assets"); clearCacheByName("repositorySettings"); break; + case "3.4.2": + log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ..."); + clearCacheByName("repositorySettings"); + break; + case "3.4.4": + log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0"); + clearAll(); + break; default: //Do nothing, since cache cleanup is optional. } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index ee34bd7f23..13943d5698 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.thingsboard.rule.engine.flow.TbRuleChainInputNode; import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration; import org.thingsboard.rule.engine.profile.TbDeviceProfileNode; import org.thingsboard.rule.engine.profile.TbDeviceProfileNodeConfiguration; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; @@ -555,13 +556,13 @@ public class DefaultDataUpdateService implements DataUpdateService { }; private void updateTenantAlarmsCustomer(TenantId tenantId, String name, AtomicLong processed) { - AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, false); + AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, null, false); PageData alarms = alarmDao.findAlarms(tenantId, alarmQuery); boolean hasNext = true; while (hasNext) { for (Alarm alarm : alarms.getData()) { if (alarm.getCustomerId() == null && alarm.getOriginator() != null) { - alarm.setCustomerId(entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator())); + alarm.setCustomerId(entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator()).get()); alarmDao.save(tenantId, alarm); } if (processed.incrementAndGet() % 1000 == 0) { @@ -651,8 +652,8 @@ public class DefaultDataUpdateService implements DataUpdateService { private TenantProfileQueueConfiguration getMainQueueConfiguration() { TenantProfileQueueConfiguration mainQueueConfiguration = new TenantProfileQueueConfiguration(); - mainQueueConfiguration.setName("Main"); - mainQueueConfiguration.setTopic("tb_rule_engine.main"); + mainQueueConfiguration.setName(DataConstants.MAIN_QUEUE_NAME); + mainQueueConfiguration.setTopic(DataConstants.MAIN_QUEUE_TOPIC); mainQueueConfiguration.setPollInterval(25); mainQueueConfiguration.setPartitions(10); mainQueueConfiguration.setConsumerPerPartition(true); @@ -671,7 +672,7 @@ public class DefaultDataUpdateService implements DataUpdateService { return mainQueueConfiguration; } - private boolean getEnv(String name, boolean defaultValue) { + public static boolean getEnv(String name, boolean defaultValue) { String env = System.getenv(name); if (env == null) { return defaultValue; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java index b2fb707791..ae47a94c1a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.thingsboard.server.service.install.update; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.SearchTextBased; -import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java index 87179b26b5..dd24aa7797 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MService.java b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MService.java index eee64084d6..d311e67431 100644 --- a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MService.java +++ b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java index 31e60b3f18..2e80a4ef74 100644 --- a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 7b26af989c..f3fd18141a 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ import org.thingsboard.rule.engine.api.TbEmail; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageRecordKey; -import org.thingsboard.server.common.data.ApiUsageStateMailMessage; +import org.thingsboard.server.common.data.ApiUsageRecordState; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -64,8 +64,6 @@ public class DefaultMailService implements MailService { public static final String MAIL_PROP = "mail."; public static final String TARGET_EMAIL = "targetEmail"; public static final String UTF_8 = "UTF-8"; - public static final int _10K = 10000; - public static final int _1M = 1000000; private final MessageSource messages; private final Configuration freemarkerConfig; @@ -335,7 +333,7 @@ public class DefaultMailService implements MailService { } @Override - public void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException { + public void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageRecordState recordState) throws ThingsboardException { String subject = messages.getMessage("api.usage.state", null, Locale.US); Map model = new HashMap<>(); @@ -350,11 +348,11 @@ public class DefaultMailService implements MailService { message = mergeTemplateIntoString("state.enabled.ftl", model); break; case WARNING: - model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(msg.getKey(), msg.getValue(), msg.getThreshold())); + model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(recordState)); message = mergeTemplateIntoString("state.warning.ftl", model); break; case DISABLED: - model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(msg.getKey(), msg.getThreshold())); + model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(recordState)); message = mergeTemplateIntoString("state.disabled.ftl", model); break; } @@ -366,6 +364,11 @@ public class DefaultMailService implements MailService { mailSender.testConnection(); } + @Override + public boolean isConfigured(TenantId tenantId) { + return mailSender != null; + } + private String toEnabledValueLabel(ApiFeature apiFeature) { switch (apiFeature) { case DB: @@ -406,10 +409,10 @@ public class DefaultMailService implements MailService { } } - private String toWarningValueLabel(ApiUsageRecordKey key, long value, long threshold) { - String valueInM = getValueAsString(value); - String thresholdInM = getValueAsString(threshold); - switch (key) { + private String toWarningValueLabel(ApiUsageRecordState recordState) { + String valueInM = recordState.getValueAsString(); + String thresholdInM = recordState.getThresholdAsString(); + switch (recordState.getKey()) { case STORAGE_DP_COUNT: case TRANSPORT_DP_COUNT: return valueInM + " out of " + thresholdInM + " allowed data points"; @@ -428,36 +431,26 @@ public class DefaultMailService implements MailService { } } - private String toDisabledValueLabel(ApiUsageRecordKey key, long value) { - switch (key) { + private String toDisabledValueLabel(ApiUsageRecordState recordState) { + switch (recordState.getKey()) { case STORAGE_DP_COUNT: case TRANSPORT_DP_COUNT: - return getValueAsString(value) + " data points"; + return recordState.getValueAsString() + " data points"; case TRANSPORT_MSG_COUNT: - return getValueAsString(value) + " messages"; + return recordState.getValueAsString() + " messages"; case JS_EXEC_COUNT: - return "JavaScript functions " + getValueAsString(value) + " times"; + return "JavaScript functions " + recordState.getValueAsString() + " times"; case RE_EXEC_COUNT: - return getValueAsString(value) + " Rule Engine messages"; + return recordState.getValueAsString() + " Rule Engine messages"; case EMAIL_EXEC_COUNT: - return getValueAsString(value) + " Email messages"; + return recordState.getValueAsString() + " Email messages"; case SMS_EXEC_COUNT: - return getValueAsString(value) + " SMS messages"; + return recordState.getValueAsString() + " SMS messages"; default: throw new RuntimeException("Not implemented!"); } } - private String getValueAsString(long value) { - if (value > _1M && value % _1M < _10K) { - return value / _1M + "M"; - } else if (value > _10K) { - return String.format("%.2fM", ((double) value) / 1000000); - } else { - return value + ""; - } - } - private void sendMail(JavaMailSenderImpl mailSender, String mailFrom, String email, String subject, String message, long timeout) throws ThingsboardException { try { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java index 8a8c79b51c..ededd90969 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/mail/PasswordResetExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/PasswordResetExecutorService.java index ea761c144d..642af39f04 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/PasswordResetExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/PasswordResetExecutorService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java new file mode 100644 index 0000000000..0cb9dc780b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -0,0 +1,424 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.NotificationId; +import org.thingsboard.server.common.data.id.NotificationRequestId; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.notification.AlreadySentException; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestConfig; +import org.thingsboard.server.common.data.notification.NotificationRequestStats; +import org.thingsboard.server.common.data.notification.NotificationRequestStatus; +import org.thingsboard.server.common.data.notification.NotificationStatus; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; +import org.thingsboard.server.dao.notification.NotificationRequestService; +import org.thingsboard.server.dao.notification.NotificationService; +import org.thingsboard.server.dao.notification.NotificationSettingsService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.executors.NotificationExecutorService; +import org.thingsboard.server.service.notification.channels.NotificationChannel; +import org.thingsboard.server.service.subscription.TbSubscriptionUtils; +import org.thingsboard.server.service.telemetry.AbstractSubscriptionService; +import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +@SuppressWarnings({"UnstableApiUsage", "rawtypes"}) +public class DefaultNotificationCenter extends AbstractSubscriptionService implements NotificationCenter, NotificationChannel { + + private final NotificationTargetService notificationTargetService; + private final NotificationRequestService notificationRequestService; + private final NotificationService notificationService; + private final NotificationTemplateService notificationTemplateService; + private final NotificationSettingsService notificationSettingsService; + private final UserService userService; + private final NotificationExecutorService notificationExecutor; + private final DbCallbackExecutorService dbCallbackExecutorService; + private final NotificationsTopicService notificationsTopicService; + private final TbQueueProducerProvider producerProvider; + private final RateLimitService rateLimitService; + + private Map channels; + + @Override + public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest request, Consumer callback) { + if (request.getRuleId() == null) { + if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS, tenantId)) { + throw new TbRateLimitsException(EntityType.TENANT); + } + } + + NotificationTemplate notificationTemplate; + if (request.getTemplateId() != null) { + notificationTemplate = notificationTemplateService.findNotificationTemplateById(tenantId, request.getTemplateId()); + } else { + notificationTemplate = request.getTemplate(); + } + if (notificationTemplate == null) throw new IllegalArgumentException("Template is missing"); + + Set deliveryMethods = new HashSet<>(); + List targets = request.getTargets().stream().map(NotificationTargetId::new) + .map(id -> notificationTargetService.findNotificationTargetById(tenantId, id)) + .collect(Collectors.toList()); + + NotificationRuleId ruleId = request.getRuleId(); + notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> { + if (!template.isEnabled()) return; + try { + channels.get(deliveryMethod).check(tenantId); + } catch (Exception e) { + if (ruleId == null) { + throw new IllegalArgumentException(e.getMessage()); + } else { + return; // if originated by rule - just ignore delivery method + } + } + if (ruleId == null) { + if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) { + throw new IllegalArgumentException("Recipients for " + deliveryMethod.getName() + " delivery method not chosen"); + } + } + deliveryMethods.add(deliveryMethod); + }); + if (deliveryMethods.isEmpty()) { + throw new IllegalArgumentException("No delivery methods to send notification with"); + } + + if (request.getAdditionalConfig() != null) { + NotificationRequestConfig config = request.getAdditionalConfig(); + if (config.getSendingDelayInSec() > 0 && request.getId() == null) { + request.setStatus(NotificationRequestStatus.SCHEDULED); + request = notificationRequestService.saveNotificationRequest(tenantId, request); + forwardToNotificationSchedulerService(tenantId, request.getId()); + return request; + } + } + NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); + + log.debug("Processing notification request (tenantId: {}, targets: {})", tenantId, request.getTargets()); + request.setStatus(NotificationRequestStatus.PROCESSING); + request = notificationRequestService.saveNotificationRequest(tenantId, request); + + NotificationProcessingContext ctx = NotificationProcessingContext.builder() + .tenantId(tenantId) + .request(request) + .deliveryMethods(deliveryMethods) + .template(notificationTemplate) + .settings(settings) + .build(); + + notificationExecutor.submit(() -> { + List> results = new ArrayList<>(); + + for (NotificationTarget target : targets) { + List> result = processForTarget(target, ctx); + results.addAll(result); + } + + Futures.whenAllComplete(results).run(() -> { + NotificationRequestId requestId = ctx.getRequest().getId(); + log.debug("[{}] Notification request processing is finished", requestId); + NotificationRequestStats stats = ctx.getStats(); + try { + notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats); + } catch (Exception e) { + log.error("[{}] Failed to update stats for notification request", requestId, e); + } + + if (callback != null) { + try { + callback.accept(stats); + } catch (Exception e) { + log.error("Failed to process callback for notification request {}", requestId, e); + } + } + }, dbCallbackExecutorService); + }); + + return request; + } + + private List> processForTarget(NotificationTarget target, NotificationProcessingContext ctx) { + Iterable recipients; + switch (target.getConfiguration().getType()) { + case PLATFORM_USERS: { + PlatformUsersNotificationTargetConfig targetConfig = (PlatformUsersNotificationTargetConfig) target.getConfiguration(); + if (targetConfig.getUsersFilter().getType().isForRules()) { + recipients = new PageDataIterable<>(pageLink -> { + return notificationTargetService.findRecipientsForRuleNotificationTargetConfig(ctx.getTenantId(), targetConfig, (RuleOriginatedNotificationInfo) ctx.getRequest().getInfo(), pageLink); + }, 500); + } else { + recipients = new PageDataIterable<>(pageLink -> { + return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), targetConfig, pageLink); + }, 500); + } + break; + } + case SLACK: { + SlackNotificationTargetConfig targetConfig = (SlackNotificationTargetConfig) target.getConfiguration(); + recipients = List.of(targetConfig.getConversation()); + break; + } + default: { + recipients = Collections.emptyList(); + } + } + + Set deliveryMethods = new HashSet<>(ctx.getDeliveryMethods()); + deliveryMethods.removeIf(deliveryMethod -> !target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod)); + log.debug("[{}] Processing notification request for {} target ({}) for delivery methods {}", ctx.getRequest().getId(), target.getConfiguration().getType(), target.getId(), deliveryMethods); + + List> results = new ArrayList<>(); + if (!deliveryMethods.isEmpty()) { + for (NotificationRecipient recipient : recipients) { + for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { + ListenableFuture resultFuture = processForRecipient(deliveryMethod, recipient, ctx); + DonAsynchron.withCallback(resultFuture, result -> { + ctx.getStats().reportSent(deliveryMethod, recipient); + }, error -> { + ctx.getStats().reportError(deliveryMethod, error, recipient); + }); + results.add(resultFuture); + } + } + } + return results; + } + + private ListenableFuture processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) { + if (ctx.getStats().contains(deliveryMethod, recipient.getId())) { + return Futures.immediateFailedFuture(new AlreadySentException()); + } + + DeliveryMethodNotificationTemplate processedTemplate; + try { + processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient); + } catch (Exception e) { + return Futures.immediateFailedFuture(e); + } + + NotificationChannel notificationChannel = channels.get(deliveryMethod); + log.trace("[{}] Sending {} notification for recipient {}", ctx.getRequest().getId(), deliveryMethod, recipient); + return notificationChannel.sendNotification(recipient, processedTemplate, ctx); + } + + @Override + public ListenableFuture sendNotification(User recipient, WebDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { + NotificationRequest request = ctx.getRequest(); + Notification notification = Notification.builder() + .requestId(request.getId()) + .recipientId(recipient.getId()) + .type(ctx.getNotificationTemplate().getNotificationType()) + .subject(processedTemplate.getSubject()) + .text(processedTemplate.getBody()) + .additionalConfig(processedTemplate.getAdditionalConfig()) + .info(request.getInfo()) + .status(NotificationStatus.SENT) + .build(); + try { + notification = notificationService.saveNotification(recipient.getTenantId(), notification); + } catch (Exception e) { + log.error("Failed to create notification for recipient {}", recipient.getId(), e); + return Futures.immediateFailedFuture(e); + } + + NotificationUpdate update = NotificationUpdate.builder() + .created(true) + .notification(notification) + .build(); + return onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update); + } + + @Override + public void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId) { + boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId); + if (updated) { + log.trace("Marked notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId); + NotificationUpdate update = NotificationUpdate.builder() + .updated(true) + .notificationId(notificationId.getId()) + .newStatus(NotificationStatus.READ) + .build(); + onNotificationUpdate(tenantId, recipientId, update); + } + } + + @Override + public void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) { + int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, recipientId); + if (updatedCount > 0) { + log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId); + NotificationUpdate update = NotificationUpdate.builder() + .updated(true) + .allNotifications(true) + .newStatus(NotificationStatus.READ) + .build(); + onNotificationUpdate(tenantId, recipientId, update); + } + } + + @Override + public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) { + Notification notification = notificationService.findNotificationById(tenantId, notificationId); + boolean deleted = notificationService.deleteNotification(tenantId, recipientId, notificationId); + if (deleted) { + NotificationUpdate update = NotificationUpdate.builder() + .deleted(true) + .notification(notification) + .build(); + onNotificationUpdate(tenantId, recipientId, update); + } + } + + @Override + public Set getAvailableDeliveryMethods(TenantId tenantId) { + return channels.values().stream() + .filter(channel -> { + try { + channel.check(tenantId); + return true; + } catch (Exception e) { + return false; + } + }) + .map(NotificationChannel::getDeliveryMethod) + .collect(Collectors.toSet()); + } + + @Override + public void check(TenantId tenantId) throws Exception { + } + + @Override + public void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId) { + log.debug("Deleting notification request {}", notificationRequestId); + NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, notificationRequestId); + notificationRequestService.deleteNotificationRequest(tenantId, notificationRequestId); + + if (notificationRequest.isSent()) { + // TODO: no need to send request update for other than PLATFORM_USERS target type + onNotificationRequestUpdate(tenantId, NotificationRequestUpdate.builder() + .notificationRequestId(notificationRequestId) + .deleted(true) + .build()); + } else if (notificationRequest.isScheduled()) { + // TODO: just forward to scheduler service + clusterService.broadcastEntityStateChangeEvent(tenantId, notificationRequestId, ComponentLifecycleEvent.DELETED); + } + } + + private void forwardToNotificationSchedulerService(TenantId tenantId, NotificationRequestId notificationRequestId) { + TransportProtos.NotificationSchedulerServiceMsg.Builder msg = TransportProtos.NotificationSchedulerServiceMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setRequestIdMSB(notificationRequestId.getId().getMostSignificantBits()) + .setRequestIdLSB(notificationRequestId.getId().getLeastSignificantBits()) + .setTs(System.currentTimeMillis()); + TransportProtos.ToCoreMsg toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setNotificationSchedulerServiceMsg(msg) + .build(); + clusterService.pushMsgToCore(tenantId, notificationRequestId, toCoreMsg, null); + } + + private ListenableFuture onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) { + log.trace("Submitting notification update for recipient {}: {}", recipientId, update); + return Futures.submit(() -> { + forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> { + subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, TbCallback.EMPTY); + }, () -> TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, update)); + }, wsCallBackExecutor); + } + + private void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate update) { + log.trace("Submitting notification request update: {}", update); + wsCallBackExecutor.submit(() -> { + TransportProtos.ToCoreNotificationMsg notificationRequestUpdateProto = TbSubscriptionUtils.notificationRequestUpdateToProto(tenantId, update); + Set coreServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_CORE)); + for (String serviceId : coreServices) { + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); + producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), notificationRequestUpdateProto), null); + } + }); + } + + @Override + public NotificationDeliveryMethod getDeliveryMethod() { + return NotificationDeliveryMethod.WEB; + } + + @Override + protected String getExecutorPrefix() { + return "notification"; + } + + @Autowired + public void setChannels(List channels, NotificationCenter webNotificationChannel) { + this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c)); + this.channels.put(NotificationDeliveryMethod.WEB, (NotificationChannel) webNotificationChannel); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java new file mode 100644 index 0000000000..01591003ad --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java @@ -0,0 +1,175 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.NotificationRequestId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestConfig; +import org.thingsboard.server.common.data.notification.NotificationRequestStats; +import org.thingsboard.server.common.data.notification.NotificationRequestStatus; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.notification.NotificationRequestService; +import org.thingsboard.server.queue.scheduler.SchedulerComponent; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.executors.NotificationExecutorService; +import org.thingsboard.server.service.partition.AbstractPartitionBasedService; + +import javax.annotation.PostConstruct; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@TbCoreComponent +@Service +@RequiredArgsConstructor +@Slf4j +@SuppressWarnings("UnstableApiUsage") +public class DefaultNotificationSchedulerService extends AbstractPartitionBasedService implements NotificationSchedulerService { + + private final NotificationCenter notificationCenter; + private final NotificationRequestService notificationRequestService; + private final SchedulerComponent scheduler; + private final NotificationExecutorService notificationExecutor; + + private final Map scheduledNotificationRequests = new ConcurrentHashMap<>(); + + @PostConstruct + public void init() { + super.init(); + } + + @Override + protected Map>> onAddedPartitions(Set addedPartitions) { + PageDataIterable notificationRequests = new PageDataIterable<>(pageLink -> { + return notificationRequestService.findScheduledNotificationRequests(pageLink); + }, 1000); + for (NotificationRequest notificationRequest : notificationRequests) { + TopicPartitionInfo requestPartition = partitionService.resolve(ServiceType.TB_CORE, notificationRequest.getTenantId(), notificationRequest.getId()); + if (addedPartitions.contains(requestPartition)) { + partitionedEntities.computeIfAbsent(requestPartition, k -> ConcurrentHashMap.newKeySet()).add(notificationRequest.getId()); + if (!scheduledNotificationRequests.containsKey(notificationRequest.getId())) { + scheduleNotificationRequest(notificationRequest.getTenantId(), notificationRequest, notificationRequest.getCreatedTime()); + } + } + } + return Collections.emptyMap(); + } + + @Override + public void scheduleNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId, long requestTs) { + NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, notificationRequestId); + scheduleNotificationRequest(tenantId, notificationRequest, requestTs); + } + + private void scheduleNotificationRequest(TenantId tenantId, NotificationRequest request, long requestTs) { + int delayInSec = Optional.ofNullable(request) + .map(NotificationRequest::getAdditionalConfig) + .map(NotificationRequestConfig::getSendingDelayInSec) + .orElse(0); + if (delayInSec <= 0) return; + long delayInMs = TimeUnit.SECONDS.toMillis(delayInSec) - (System.currentTimeMillis() - requestTs); + if (delayInMs < 0) { + delayInMs = 0; + } + + ScheduledFuture scheduledTask = scheduler.schedule(() -> { + NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, request.getId()); + if (notificationRequest == null) return; + + notificationExecutor.executeAsync(() -> { + try { + notificationCenter.processNotificationRequest(tenantId, notificationRequest, null); + } catch (Exception e) { + log.error("Failed to process scheduled notification request {}", notificationRequest.getId(), e); + NotificationRequestStats stats = new NotificationRequestStats(); + stats.setError(e.getMessage()); + notificationRequestService.updateNotificationRequest(tenantId, request.getId(), NotificationRequestStatus.SENT, stats); + } + }); + scheduledNotificationRequests.remove(notificationRequest.getId()); + }, delayInMs, TimeUnit.MILLISECONDS); + scheduledNotificationRequests.put(request.getId(), new ScheduledRequestMetadata(tenantId, scheduledTask)); + } + + @EventListener(ComponentLifecycleMsg.class) + public void handleComponentLifecycleEvent(ComponentLifecycleMsg event) { + if (event.getEvent() == ComponentLifecycleEvent.DELETED) { + EntityId entityId = event.getEntityId(); + switch (entityId.getEntityType()) { + case NOTIFICATION_REQUEST: + cancelAndRemove((NotificationRequestId) entityId); + break; + case TENANT: + Set toCancel = new HashSet<>(); + scheduledNotificationRequests.forEach((notificationRequestId, scheduledRequestMetadata) -> { + if (scheduledRequestMetadata.getTenantId().equals(entityId)) { + toCancel.add(notificationRequestId); + } + }); + toCancel.forEach(this::cancelAndRemove); + break; + } + } + } + + @Override + protected void cleanupEntityOnPartitionRemoval(NotificationRequestId notificationRequestId) { + cancelAndRemove(notificationRequestId); + } + + private void cancelAndRemove(NotificationRequestId notificationRequestId) { + ScheduledRequestMetadata md = scheduledNotificationRequests.remove(notificationRequestId); + if (md != null) { + md.getFuture().cancel(false); + } + } + + @Override + protected String getServiceName() { + return "Notifications scheduler"; + } + + @Override + protected String getSchedulerExecutorName() { + return "notifications-scheduler"; + } + + @Data + private static class ScheduledRequestMetadata { + private final TenantId tenantId; + private final ScheduledFuture future; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java new file mode 100644 index 0000000000..27a9cabe43 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java @@ -0,0 +1,138 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification; + +import com.google.common.base.Strings; +import lombok.Builder; +import lombok.Getter; +import org.apache.commons.collections4.MapUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestStats; +import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.HasSubject; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; +import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.util.TemplateUtils; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.apache.commons.lang3.StringUtils.isNotEmpty; + +@SuppressWarnings("unchecked") +public class NotificationProcessingContext { + + @Getter + private final TenantId tenantId; + private final NotificationSettings settings; + @Getter + private final NotificationRequest request; + @Getter + private final Set deliveryMethods; + @Getter + private final NotificationTemplate notificationTemplate; + + private final Map templates; + @Getter + private final NotificationRequestStats stats; + + @Builder + public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, Set deliveryMethods, + NotificationTemplate template, NotificationSettings settings) { + this.tenantId = tenantId; + this.request = request; + this.deliveryMethods = deliveryMethods; + this.settings = settings; + this.notificationTemplate = template; + this.templates = new EnumMap<>(NotificationDeliveryMethod.class); + this.stats = new NotificationRequestStats(); + init(); + } + + private void init() { + NotificationTemplateConfig templateConfig = notificationTemplate.getConfiguration(); + templateConfig.getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> { + if (template.isEnabled()) { + template = processTemplate(template, null); // processing template with immutable params + templates.put(deliveryMethod, template); + } + }); + } + + public C getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) { + return (C) settings.getDeliveryMethodsConfigs().get(deliveryMethod); + } + + public T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) { + T template = (T) templates.get(deliveryMethod); + Map additionalTemplateContext = null; + if (recipient != null) { + additionalTemplateContext = createTemplateContextForRecipient(recipient); + } + if (MapUtils.isNotEmpty(additionalTemplateContext) && template.containsAny(additionalTemplateContext.keySet().toArray(String[]::new))) { + template = processTemplate(template, additionalTemplateContext); + } + return template; + } + + private T processTemplate(T template, Map additionalTemplateContext) { + Map templateContext = new HashMap<>(); + if (request.getInfo() != null) { + templateContext.putAll(request.getInfo().getTemplateData()); + } + if (additionalTemplateContext != null) { + templateContext.putAll(additionalTemplateContext); + } + if (templateContext.isEmpty()) return template; + + template = (T) template.copy(); + template.setBody(TemplateUtils.processTemplate(template.getBody(), templateContext)); + if (template instanceof HasSubject) { + String subject = ((HasSubject) template).getSubject(); + ((HasSubject) template).setSubject(TemplateUtils.processTemplate(subject, templateContext)); + } + if (template instanceof WebDeliveryMethodNotificationTemplate) { + WebDeliveryMethodNotificationTemplate webNotificationTemplate = (WebDeliveryMethodNotificationTemplate) template; + String buttonText = webNotificationTemplate.getButtonText(); + if (isNotEmpty(buttonText)) { + webNotificationTemplate.setButtonText(TemplateUtils.processTemplate(buttonText, templateContext)); + } + String buttonLink = webNotificationTemplate.getButtonLink(); + if (isNotEmpty(buttonLink)) { + webNotificationTemplate.setButtonLink(TemplateUtils.processTemplate(buttonLink, templateContext)); + } + } + return template; + } + + private Map createTemplateContextForRecipient(NotificationRecipient recipient) { + return Map.of( + "recipientTitle", recipient.getTitle(), + "recipientEmail", Strings.nullToEmpty(recipient.getEmail()), + "recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()), + "recipientLastName", Strings.nullToEmpty(recipient.getLastName()) + ); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/NotificationSchedulerService.java b/application/src/main/java/org/thingsboard/server/service/notification/NotificationSchedulerService.java new file mode 100644 index 0000000000..c02d96345d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/NotificationSchedulerService.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification; + +import org.thingsboard.server.common.data.id.NotificationRequestId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface NotificationSchedulerService { + + void scheduleNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId, long requestTs); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java new file mode 100644 index 0000000000..8b3c9551c2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.channels; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.TbEmail; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.service.mail.MailExecutorService; +import org.thingsboard.server.service.notification.NotificationProcessingContext; + +@Component +@RequiredArgsConstructor +public class EmailNotificationChannel implements NotificationChannel { + + private final MailService mailService; + private final MailExecutorService executor; + + @Override + public ListenableFuture sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { + return executor.submit(() -> { + mailService.send(recipient.getTenantId(), null, TbEmail.builder() + .to(recipient.getEmail()) + .subject(processedTemplate.getSubject()) + .body(processedTemplate.getBody()) + .html(true) + .build()); + return null; + }); + } + + @Override + public void check(TenantId tenantId) throws Exception { + if (!mailService.isConfigured(tenantId)) { + throw new RuntimeException("Mail server is not configured"); + } + } + + @Override + public NotificationDeliveryMethod getDeliveryMethod() { + return NotificationDeliveryMethod.EMAIL; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java new file mode 100644 index 0000000000..02fe6264d2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.channels; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.service.notification.NotificationProcessingContext; + +public interface NotificationChannel { + + ListenableFuture sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx); + + void check(TenantId tenantId) throws Exception; + + NotificationDeliveryMethod getDeliveryMethod(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java new file mode 100644 index 0000000000..46afbd7270 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.channels; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.rule.engine.api.slack.SlackService; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; +import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.notification.NotificationSettingsService; +import org.thingsboard.server.service.executors.ExternalCallExecutorService; +import org.thingsboard.server.service.notification.NotificationProcessingContext; + +@Component +@RequiredArgsConstructor +public class SlackNotificationChannel implements NotificationChannel { + + private final SlackService slackService; + private final NotificationSettingsService notificationSettingsService; + private final ExternalCallExecutorService executor; + + @Override + public ListenableFuture sendNotification(SlackConversation conversation, SlackDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { + SlackNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.SLACK); + return executor.submit(() -> { + slackService.sendMessage(ctx.getTenantId(), config.getBotToken(), conversation.getId(), processedTemplate.getBody()); + return null; + }); + } + + @Override + public void check(TenantId tenantId) throws Exception { + NotificationSettings notificationSettings = notificationSettingsService.findNotificationSettings(tenantId); + if (!notificationSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.SLACK)) { + throw new RuntimeException("Slack API token is not configured"); + } + } + + @Override + public NotificationDeliveryMethod getDeliveryMethod() { + return NotificationDeliveryMethod.SLACK; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java new file mode 100644 index 0000000000..3c44dabd4b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.channels; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import org.thingsboard.rule.engine.api.SmsService; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.service.notification.NotificationProcessingContext; +import org.thingsboard.server.service.sms.SmsExecutorService; + +@Component +@RequiredArgsConstructor +public class SmsNotificationChannel implements NotificationChannel { + + private final SmsService smsService; + private final SmsExecutorService executor; + + @Override + public ListenableFuture sendNotification(User recipient, SmsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { + String phone = recipient.getPhone(); + if (StringUtils.isBlank(phone)) { + return Futures.immediateFailedFuture(new RuntimeException("User does not have phone number")); + } + + return executor.submit(() -> { + smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody()); + return null; + }); + } + + @Override + public void check(TenantId tenantId) throws Exception { + if (!smsService.isConfigured(tenantId)) { + throw new RuntimeException("SMS provider is not configured"); + } + } + + @Override + public NotificationDeliveryMethod getDeliveryMethod() { + return NotificationDeliveryMethod.SMS; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java new file mode 100644 index 0000000000..9c57b3cc77 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java @@ -0,0 +1,255 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.NotificationRequestId; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestConfig; +import org.thingsboard.server.common.data.notification.NotificationRequestStatus; +import org.thingsboard.server.common.data.notification.info.NotificationInfo; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; +import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.dao.notification.NotificationRequestService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; +import org.thingsboard.server.service.executors.NotificationExecutorService; +import org.thingsboard.server.service.notification.rule.cache.NotificationRulesCache; +import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor; +import org.thingsboard.server.service.notification.rule.trigger.RuleEngineMsgNotificationRuleTriggerProcessor; + +import javax.annotation.PostConstruct; +import java.io.Serializable; +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +@SuppressWarnings({"rawtypes", "unchecked"}) +public class DefaultNotificationRuleProcessor implements NotificationRuleProcessor { + + private final NotificationRulesCache notificationRulesCache; + private final NotificationRequestService notificationRequestService; + private final PartitionService partitionService; + private final RateLimitService rateLimitService; + @Autowired @Lazy + private NotificationCenter notificationCenter; + private final NotificationExecutorService notificationExecutor; + private final CacheManager cacheManager; + private Cache sentNotifications; + + private final Map triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class); + + @PostConstruct + private void init() { + sentNotifications = cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE); + if (sentNotifications == null) { + throw new IllegalStateException("Sent notifications cache is not set up"); + } + } + + @Override + public void process(NotificationRuleTrigger trigger) { + NotificationRuleTriggerType triggerType = trigger.getType(); + if (triggerType == null) return; + TenantId tenantId = triggerType.isTenantLevel() ? trigger.getTenantId() : TenantId.SYS_TENANT_ID; + + try { + List rules = notificationRulesCache.getEnabled(tenantId, triggerType); + for (NotificationRule rule : rules) { + notificationExecutor.submit(() -> { + try { + processNotificationRule(rule, trigger); + } catch (Throwable e) { + log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e); + } + }); + } + } catch (Throwable e) { + log.error("Failed to process notification rules for trigger: {}", trigger, e); + } + } + + private void processNotificationRule(NotificationRule rule, NotificationRuleTrigger trigger) { + NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig(); + log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType()); + + if (matchesClearRule(trigger, triggerConfig)) { + List notificationRequests = findAlreadySentNotificationRequests(rule, trigger); + if (notificationRequests.isEmpty()) { + return; + } + + List targets = notificationRequests.stream() + .filter(NotificationRequest::isSent) + .flatMap(notificationRequest -> notificationRequest.getTargets().stream()) + .distinct().collect(Collectors.toList()); + NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig); + submitNotificationRequest(targets, rule, trigger.getOriginatorEntityId(), notificationInfo, 0); + + notificationRequests.forEach(notificationRequest -> { + if (notificationRequest.isScheduled()) { + notificationCenter.deleteNotificationRequest(rule.getTenantId(), notificationRequest.getId()); + } + }); + return; + } + + if (matchesFilter(trigger, triggerConfig)) { + if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, rule.getTenantId(), rule.getId())) { + log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName()); + return; + } + if (trigger.getType().isDeduplicate() && alreadySent(rule.getId(), trigger)) { + return; + } + + NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig); + rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> { + submitNotificationRequest(targets, rule, trigger.getOriginatorEntityId(), notificationInfo, delay); + }); + } + } + + private List findAlreadySentNotificationRequests(NotificationRule rule, NotificationRuleTrigger trigger) { + return notificationRequestService.findNotificationRequestsByRuleIdAndOriginatorEntityId(rule.getTenantId(), rule.getId(), trigger.getOriginatorEntityId()); + } + + private void submitNotificationRequest(List targets, NotificationRule rule, + EntityId originatorEntityId, NotificationInfo notificationInfo, int delayInSec) { + NotificationRequestConfig config = new NotificationRequestConfig(); + if (delayInSec > 0) { + config.setSendingDelayInSec(delayInSec); + } + NotificationRequest notificationRequest = NotificationRequest.builder() + .tenantId(rule.getTenantId()) + .targets(targets) + .templateId(rule.getTemplateId()) + .additionalConfig(config) + .info(notificationInfo) + .ruleId(rule.getId()) + .originatorEntityId(originatorEntityId) + .build(); + notificationExecutor.submit(() -> { + try { + log.debug("Submitting notification request for rule '{}' with delay of {} sec to targets {}", rule.getName(), delayInSec, targets); + notificationCenter.processNotificationRequest(rule.getTenantId(), notificationRequest, null); + } catch (Exception e) { + log.error("Failed to process notification request for tenant {} for rule {}", rule.getTenantId(), rule.getId(), e); + } + }); + } + + private boolean matchesFilter(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) { + return triggerProcessors.get(triggerConfig.getTriggerType()).matchesFilter(trigger, triggerConfig); + } + + private boolean matchesClearRule(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) { + return triggerProcessors.get(triggerConfig.getTriggerType()).matchesClearRule(trigger, triggerConfig); + } + + private NotificationInfo constructNotificationInfo(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) { + return triggerProcessors.get(triggerConfig.getTriggerType()).constructNotificationInfo(trigger); + } + + private boolean alreadySent(NotificationRuleId ruleId, NotificationRuleTrigger trigger) { + String key = ruleId + "_" + trigger.getOriginatorEntityId(); + SentNotification sent = sentNotifications.get(key, SentNotification.class); + boolean alreadySent; + if (sent != null && sent.getTrigger().equals(trigger)) { + alreadySent = true; + log.debug("Notification for {} trigger was already sent, ignoring", trigger.getType()); + // updating cache anyway so that the value is not removed by ttl + } else { + alreadySent = false; + sent = new SentNotification(trigger); + } + log.trace("[{}] Putting to sentNotifications cache: {}", ruleId, trigger); + sentNotifications.put(key, sent); + return alreadySent; + } + + @EventListener(ComponentLifecycleMsg.class) + public void onNotificationRuleDeleted(ComponentLifecycleMsg componentLifecycleMsg) { + if (componentLifecycleMsg.getEvent() != ComponentLifecycleEvent.DELETED || + componentLifecycleMsg.getEntityId().getEntityType() != EntityType.NOTIFICATION_RULE) { + return; + } + + TenantId tenantId = componentLifecycleMsg.getTenantId(); + NotificationRuleId notificationRuleId = (NotificationRuleId) componentLifecycleMsg.getEntityId(); + if (partitionService.isMyPartition(ServiceType.TB_CORE, tenantId, notificationRuleId)) { + notificationExecutor.submit(() -> { + List scheduledForRule = notificationRequestService.findNotificationRequestsIdsByStatusAndRuleId(tenantId, NotificationRequestStatus.SCHEDULED, notificationRuleId); + for (NotificationRequestId notificationRequestId : scheduledForRule) { + notificationCenter.deleteNotificationRequest(tenantId, notificationRequestId); + } + }); + } + } + + @Autowired + public void setTriggerProcessors(Collection processors) { + Map ruleEngineMsgTypeToTriggerType = new HashMap<>(); + processors.forEach(processor -> { + triggerProcessors.put(processor.getTriggerType(), processor); + if (processor instanceof RuleEngineMsgNotificationRuleTriggerProcessor) { + Set supportedMsgTypes = ((RuleEngineMsgNotificationRuleTriggerProcessor) processor).getSupportedMsgTypes(); + supportedMsgTypes.forEach(supportedMsgType -> { + ruleEngineMsgTypeToTriggerType.put(supportedMsgType, processor.getTriggerType()); + }); + } + }); + RuleEngineMsgTrigger.msgTypeToTriggerType = ruleEngineMsgTypeToTriggerType; + } + + @Data + private static class SentNotification implements Serializable { + private static final long serialVersionUID = 38973480405095422L; + + private final NotificationRuleTrigger trigger; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java new file mode 100644 index 0000000000..a02b7bf1e4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.dao.notification.NotificationRuleService; + +import javax.annotation.PostConstruct; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DefaultNotificationRulesCache implements NotificationRulesCache { + + private final NotificationRuleService notificationRuleService; + + @Value("${cache.notificationRules.maxSize:1000}") + private int cacheMaxSize; + @Value("${cache.notificationRules.timeToLiveInMinutes:30}") + private int cacheValueTtl; + private Cache> cache; + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + @PostConstruct + private void init() { + cache = Caffeine.newBuilder() + .maximumSize(cacheMaxSize) + .expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES) + .build(); + } + + @EventListener(ComponentLifecycleMsg.class) + public void onComponentLifecycleEvent(ComponentLifecycleMsg event) { + switch (event.getEntityId().getEntityType()) { + case NOTIFICATION_RULE: + evict(event.getTenantId()); // TODO: evict by trigger type of the rule + break; + case TENANT: + if (event.getEvent() == ComponentLifecycleEvent.DELETED) { + lock.writeLock().lock(); // locking in case rules for tenant are fetched while evicting + try { + evict(event.getTenantId()); + } finally { + lock.writeLock().unlock(); + } + } + break; + } + } + + @Override + public List getEnabled(TenantId tenantId, NotificationRuleTriggerType triggerType) { + lock.readLock().lock(); + try { + log.trace("Retrieving notification rules of type {} for tenant {} from cache", triggerType, tenantId); + return cache.get(key(tenantId, triggerType), k -> { + List rules = notificationRuleService.findEnabledNotificationRulesByTenantIdAndTriggerType(tenantId, triggerType); + log.trace("Fetched notification rules of type {} for tenant {} (count: {})", triggerType, tenantId, rules.size()); + return !rules.isEmpty() ? rules : Collections.emptyList(); + }); + } finally { + lock.readLock().unlock(); + } + } + + private void evict(TenantId tenantId) { + cache.invalidateAll(Arrays.stream(NotificationRuleTriggerType.values()) + .map(triggerType -> key(tenantId, triggerType)) + .collect(Collectors.toList())); + log.trace("Evicted all notification rules for tenant {} from cache", tenantId); + } + + private static CacheKey key(TenantId tenantId, NotificationRuleTriggerType triggerType) { + return new CacheKey(tenantId, triggerType); + } + + @Data + private static class CacheKey { + private final TenantId tenantId; + private final NotificationRuleTriggerType triggerType; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/NotificationRulesCache.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/NotificationRulesCache.java new file mode 100644 index 0000000000..2a1054b352 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/NotificationRulesCache.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.cache; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; + +import java.util.List; + +public interface NotificationRulesCache { + + List getEnabled(TenantId tenantId, NotificationRuleTriggerType triggerType); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java new file mode 100644 index 0000000000..ed72f6be58 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.notification.info.AlarmAssignmentNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig.Action; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; + +import java.util.Set; + +import static org.apache.commons.collections.CollectionUtils.isEmpty; +import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; + +@Service +public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) { + Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? Action.ASSIGNED : Action.UNASSIGNED; + if (!triggerConfig.getNotifyOn().contains(action)) { + return false; + } + Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class); + return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) && + emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) && + (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { + AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class); + AlarmAssignee assignee = alarmInfo.getAssignee(); + return AlarmAssignmentNotificationInfo.builder() + .action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? "assigned" : "unassigned") + .assigneeFirstName(assignee != null ? assignee.getFirstName() : null) + .assigneeLastName(assignee != null ? assignee.getLastName() : null) + .assigneeEmail(assignee != null ? assignee.getEmail() : null) + .assigneeId(assignee != null ? assignee.getId() : null) + .userEmail(trigger.getMsg().getMetaData().getValue("userEmail")) + .userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName")) + .userLastName(trigger.getMsg().getMetaData().getValue("userLastName")) + .alarmId(alarmInfo.getUuidId()) + .alarmType(alarmInfo.getType()) + .alarmOriginator(alarmInfo.getOriginator()) + .alarmOriginatorName(alarmInfo.getOriginatorName()) + .alarmSeverity(alarmInfo.getSeverity()) + .alarmStatus(alarmInfo.getStatus()) + .alarmCustomerId(alarmInfo.getCustomerId()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM_ASSIGNMENT; + } + + @Override + public Set getSupportedMsgTypes() { + return Set.of(DataConstants.ALARM_ASSIGNED, DataConstants.ALARM_UNASSIGNED); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java new file mode 100644 index 0000000000..cf71718f83 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java @@ -0,0 +1,93 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; + +import java.util.Set; + +import static org.apache.commons.collections.CollectionUtils.isEmpty; +import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; + +@Service +public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) { + TbMsg msg = trigger.getMsg(); + if (msg.getMetaData().getValue("comment") == null) { + return false; + } + if (msg.getType().equals(DataConstants.COMMENT_UPDATED) && !triggerConfig.isNotifyOnCommentUpdate()) { + return false; + } + if (triggerConfig.isOnlyUserComments()) { + AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class); + if (comment.getType() == AlarmCommentType.SYSTEM) { + return false; + } + } + Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class); + return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) && + emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) && + (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { + TbMsg msg = trigger.getMsg(); + AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class); + AlarmInfo alarmInfo = JacksonUtil.fromString(msg.getData(), AlarmInfo.class); + return AlarmCommentNotificationInfo.builder() + .comment(comment.getComment().get("text").asText()) + .action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated") + .userEmail(msg.getMetaData().getValue("userEmail")) + .userFirstName(msg.getMetaData().getValue("userFirstName")) + .userLastName(msg.getMetaData().getValue("userLastName")) + .alarmId(alarmInfo.getUuidId()) + .alarmType(alarmInfo.getType()) + .alarmOriginator(alarmInfo.getOriginator()) + .alarmOriginatorName(alarmInfo.getOriginatorName()) + .alarmSeverity(alarmInfo.getSeverity()) + .alarmStatus(alarmInfo.getStatus()) + .alarmCustomerId(alarmInfo.getCustomerId()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM_COMMENT; + } + + @Override + public Set getSupportedMsgTypes() { + return Set.of(DataConstants.COMMENT_CREATED, DataConstants.COMMENT_UPDATED); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java new file mode 100644 index 0000000000..c32ed26854 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java @@ -0,0 +1,120 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger; + +import static org.apache.commons.collections.CollectionUtils.isNotEmpty; +import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; + +@Service +public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(AlarmTrigger trigger, AlarmNotificationRuleTriggerConfig triggerConfig) { + AlarmApiCallResult alarmUpdate = trigger.getAlarmUpdate(); + Alarm alarm = alarmUpdate.getAlarm(); + if (!typeMatches(alarm, triggerConfig)) { + return false; + } + + if (alarmUpdate.isCreated()) { + if (triggerConfig.getNotifyOn().contains(AlarmAction.CREATED)) { + return severityMatches(alarm, triggerConfig); + } + } else if (alarmUpdate.isSeverityChanged()) { + if (triggerConfig.getNotifyOn().contains(AlarmAction.SEVERITY_CHANGED)) { + return severityMatches(alarmUpdate.getOld(), triggerConfig) || severityMatches(alarm, triggerConfig); + } else { + // if we haven't yet sent notification about the alarm + return !severityMatches(alarmUpdate.getOld(), triggerConfig) && severityMatches(alarm, triggerConfig); + } + } else if (alarmUpdate.isAcknowledged()) { + if (triggerConfig.getNotifyOn().contains(AlarmAction.ACKNOWLEDGED)) { + return severityMatches(alarm, triggerConfig); + } + } else if (alarmUpdate.isCleared()) { + if (triggerConfig.getNotifyOn().contains(AlarmAction.CLEARED)) { + return severityMatches(alarm, triggerConfig); + } + } + return false; + } + + @Override + public boolean matchesClearRule(AlarmTrigger trigger, AlarmNotificationRuleTriggerConfig triggerConfig) { + AlarmApiCallResult alarmUpdate = trigger.getAlarmUpdate(); + Alarm alarm = alarmUpdate.getAlarm(); + if (!typeMatches(alarm, triggerConfig)) { + return false; + } + if (alarmUpdate.isDeleted()) { + return true; + } + ClearRule clearRule = triggerConfig.getClearRule(); + if (clearRule != null) { + if (isNotEmpty(clearRule.getAlarmStatuses())) { + return AlarmStatusFilter.from(clearRule.getAlarmStatuses()).matches(alarm); + } + } + return false; + } + + private boolean severityMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { + return emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()); + } + + private boolean typeMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { + return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()); + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmTrigger trigger) { + AlarmApiCallResult alarmUpdate = trigger.getAlarmUpdate(); + AlarmInfo alarmInfo = alarmUpdate.getAlarm(); + return AlarmNotificationInfo.builder() + .alarmId(alarmInfo.getUuidId()) + .alarmType(alarmInfo.getType()) + .action(alarmUpdate.isCreated() ? "created" : + alarmUpdate.isSeverityChanged() ? "severity changed" : + alarmUpdate.isAcknowledged() ? "acknowledged" : + alarmUpdate.isCleared() ? "cleared" : + alarmUpdate.isDeleted() ? "deleted" : null) + .alarmOriginator(alarmInfo.getOriginator()) + .alarmOriginatorName(alarmInfo.getOriginatorName()) + .alarmSeverity(alarmInfo.getSeverity()) + .alarmStatus(alarmInfo.getStatus()) + .alarmCustomerId(alarmInfo.getCustomerId()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java new file mode 100644 index 0000000000..4e66a7905c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.notification.info.ApiUsageLimitNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; +import org.thingsboard.server.dao.tenant.TenantService; + +import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; + +@Service +@RequiredArgsConstructor +public class ApiUsageLimitTriggerProcessor implements NotificationRuleTriggerProcessor { + + private final TenantService tenantService; + + @Override + public boolean matchesFilter(ApiUsageLimitTrigger trigger, ApiUsageLimitNotificationRuleTriggerConfig triggerConfig) { + return emptyOrContains(triggerConfig.getApiFeatures(), trigger.getState().getApiFeature()) && + emptyOrContains(triggerConfig.getNotifyOn(), trigger.getStatus()); + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(ApiUsageLimitTrigger trigger) { + return ApiUsageLimitNotificationInfo.builder() + .feature(trigger.getState().getApiFeature()) + .recordKey(trigger.getState().getKey()) + .status(trigger.getStatus()) + .limit(trigger.getState().getThresholdAsString()) + .currentValue(trigger.getState().getValueAsString()) + .tenantId(trigger.getTenantId()) + .tenantName(tenantService.findTenantById(trigger.getTenantId()).getName()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.API_USAGE_LIMIT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java new file mode 100644 index 0000000000..6e181044c7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.info.DeviceActivityNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; + +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor { + + private final TbDeviceProfileCache deviceProfileCache; + + @Override + public boolean matchesFilter(RuleEngineMsgTrigger trigger, DeviceActivityNotificationRuleTriggerConfig triggerConfig) { + if (trigger.getMsg().getOriginator().getEntityType() != EntityType.DEVICE) { + return false; + } + DeviceEvent event = trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? DeviceEvent.ACTIVE : DeviceEvent.INACTIVE; + if (!triggerConfig.getNotifyOn().contains(event)) { + return false; + } + DeviceId deviceId = (DeviceId) trigger.getMsg().getOriginator(); + if (CollectionUtils.isNotEmpty(triggerConfig.getDevices())) { + return triggerConfig.getDevices().contains(deviceId.getId()); + } else if (CollectionUtils.isNotEmpty(triggerConfig.getDeviceProfiles())) { + DeviceProfile deviceProfile = deviceProfileCache.get(TenantId.SYS_TENANT_ID, deviceId); + return deviceProfile != null && triggerConfig.getDeviceProfiles().contains(deviceProfile.getUuidId()); + } else { + return true; + } + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { + TbMsg msg = trigger.getMsg(); + return DeviceActivityNotificationInfo.builder() + .eventType(trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? "active" : "inactive") + .deviceId(msg.getOriginator().getId()) + .deviceName(msg.getMetaData().getValue("deviceName")) + .deviceType(msg.getMetaData().getValue("deviceType")) + .deviceLabel(msg.getMetaData().getValue("deviceLabel")) + .deviceCustomerId(msg.getCustomerId()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.DEVICE_ACTIVITY; + } + + @Override + public Set getSupportedMsgTypes() { + return Set.of(DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java new file mode 100644 index 0000000000..95ace87653 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.notification.info.EntitiesLimitNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger; +import org.thingsboard.server.dao.entity.EntityCountService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.tenant.TenantService; + +import static org.apache.commons.collections.CollectionUtils.isNotEmpty; + +@Service +@RequiredArgsConstructor +public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerProcessor { + + private final EntityCountService entityCountService; + private final TbTenantProfileCache tenantProfileCache; + private final TenantService tenantService; + + @Override + public boolean matchesFilter(EntitiesLimitTrigger trigger, EntitiesLimitNotificationRuleTriggerConfig triggerConfig) { + if (isNotEmpty(triggerConfig.getEntityTypes()) && !triggerConfig.getEntityTypes().contains(trigger.getEntityType())) { + return false; + } + DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(trigger.getTenantId()).getDefaultProfileConfiguration(); + long limit = profileConfiguration.getEntitiesLimit(trigger.getEntityType()); + if (limit <= 0) { + return false; + } + long currentCount = entityCountService.countByTenantIdAndEntityType(trigger.getTenantId(), trigger.getEntityType()); + trigger.setLimit(limit); + trigger.setCurrentCount(currentCount); + return (int) (limit * triggerConfig.getThreshold()) == currentCount; // strict comparing not to send notification on each new entity + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(EntitiesLimitTrigger trigger) { + return EntitiesLimitNotificationInfo.builder() + .entityType(trigger.getEntityType()) + .currentCount(trigger.getCurrentCount()) + .limit(trigger.getLimit()) + .percents((int) (((float)trigger.getCurrentCount() / trigger.getLimit()) * 100)) + .tenantId(trigger.getTenantId()) + .tenantName(tenantService.findTenantById(trigger.getTenantId()).getName()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ENTITIES_LIMIT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java new file mode 100644 index 0000000000..ea4902fb05 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger; + +import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; + +@Service +public class EntityActionTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(EntityActionTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) { + return ((trigger.getActionType() == ActionType.ADDED && triggerConfig.isCreated()) + || (trigger.getActionType() == ActionType.UPDATED && triggerConfig.isUpdated()) + || (trigger.getActionType() == ActionType.DELETED && triggerConfig.isDeleted())) + && emptyOrContains(triggerConfig.getEntityTypes(), trigger.getEntityId().getEntityType()); + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(EntityActionTrigger trigger) { + return EntityActionNotificationInfo.builder() + .entityId(trigger.getEntityId()) + .entityName(trigger.getEntity().getName()) + .actionType(trigger.getActionType()) + .userId(trigger.getUser().getUuidId()) + .userTitle(trigger.getUser().getTitle()) + .userEmail(trigger.getUser().getEmail()) + .userFirstName(trigger.getUser().getFirstName()) + .userLastName(trigger.getUser().getLastName()) + .entityCustomerId(trigger.getEntity() instanceof HasCustomerId ? + ((HasCustomerId) trigger.getEntity()).getCustomerId() : + trigger.getUser().getCustomerId()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ENTITY_ACTION; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java new file mode 100644 index 0000000000..639378ad72 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.UpdateMessage; +import org.thingsboard.server.common.data.notification.info.NewPlatformVersionNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger; + +@Service +@RequiredArgsConstructor +public class NewPlatformVersionTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(NewPlatformVersionTrigger trigger, NewPlatformVersionNotificationRuleTriggerConfig triggerConfig) { + return trigger.getUpdateInfo().isUpdateAvailable(); + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(NewPlatformVersionTrigger trigger) { + UpdateMessage updateInfo = trigger.getUpdateInfo(); + return NewPlatformVersionNotificationInfo.builder() + .latestVersion(updateInfo.getLatestVersion()) + .latestVersionReleaseNotesUrl(updateInfo.getLatestVersionReleaseNotesUrl()) + .upgradeInstructionsUrl(updateInfo.getUpgradeInstructionsUrl()) + .currentVersion(updateInfo.getCurrentVersion()) + .currentVersionReleaseNotesUrl(updateInfo.getCurrentVersionReleaseNotesUrl()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.NEW_PLATFORM_VERSION; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java new file mode 100644 index 0000000000..fe7fb82ee5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; + +public interface NotificationRuleTriggerProcessor { + + boolean matchesFilter(T trigger, C triggerConfig); + + default boolean matchesClearRule(T trigger, C triggerConfig) { + return false; + } + + RuleOriginatedNotificationInfo constructNotificationInfo(T trigger); + + NotificationRuleTriggerType getTriggerType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java new file mode 100644 index 0000000000..6e37db83ef --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.notification.info.RuleEngineComponentLifecycleEventNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.notification.trigger.RuleEngineComponentLifecycleEventTrigger; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.queue.discovery.PartitionService; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class RuleEngineComponentLifecycleEventTriggerProcessor implements NotificationRuleTriggerProcessor { + + private final PartitionService partitionService; + + @Override + public boolean matchesFilter(RuleEngineComponentLifecycleEventTrigger trigger, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig) { + if (CollectionUtils.isNotEmpty(triggerConfig.getRuleChains())) { + if (!triggerConfig.getRuleChains().contains(trigger.getRuleChainId().getId())) { + return false; + } + } + if (!partitionService.resolve(ServiceType.TB_RULE_ENGINE, trigger.getTenantId(), trigger.getComponentId()).isMyPartition()) { + return false; + } + + EntityType componentType = trigger.getComponentId().getEntityType(); + Set trackedEvents; + boolean onlyFailures; + if (componentType == EntityType.RULE_CHAIN) { + trackedEvents = triggerConfig.getRuleChainEvents(); + onlyFailures = triggerConfig.isOnlyRuleChainLifecycleFailures(); + } else if (componentType == EntityType.RULE_NODE && triggerConfig.isTrackRuleNodeEvents()) { + trackedEvents = triggerConfig.getRuleNodeEvents(); + onlyFailures = triggerConfig.isOnlyRuleNodeLifecycleFailures(); + } else { + return false; + } + if (CollectionUtils.isEmpty(trackedEvents)) { + trackedEvents = Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED); + } + + if (!trackedEvents.contains(trigger.getEventType())) { + return false; + } + if (onlyFailures) { + return trigger.getError() != null; + } + return true; + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineComponentLifecycleEventTrigger trigger) { + return RuleEngineComponentLifecycleEventNotificationInfo.builder() + .ruleChainId(trigger.getRuleChainId()) + .ruleChainName(trigger.getRuleChainName()) + .componentId(trigger.getComponentId()) + .componentName(trigger.getComponentName()) + .action(trigger.getEventType() == ComponentLifecycleEvent.STARTED ? "start" : + trigger.getEventType() == ComponentLifecycleEvent.UPDATED ? "update" : + trigger.getEventType() == ComponentLifecycleEvent.STOPPED ? "stop" : null) + .eventType(trigger.getEventType()) + .error(getErrorMsg(trigger.getError())) + .build(); + } + + private String getErrorMsg(Throwable error) { + if (error == null) return null; + + StringWriter sw = new StringWriter(); + error.printStackTrace(new PrintWriter(sw)); + return StringUtils.abbreviate(ExceptionUtils.getStackTrace(error), 200); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java new file mode 100644 index 0000000000..ac8f692f02 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; +import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; + +import java.util.Set; + +public interface RuleEngineMsgNotificationRuleTriggerProcessor extends NotificationRuleTriggerProcessor { + + Set getSupportedMsgTypes(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java b/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java index 17ea56de2f..6ab61426a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -55,10 +54,8 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.UUID; import java.util.function.Consumer; diff --git a/application/src/main/java/org/thingsboard/server/service/ota/OtaPackageStateService.java b/application/src/main/java/org/thingsboard/server/service/ota/OtaPackageStateService.java index d6915b3907..aaf8b08445 100644 --- a/application/src/main/java/org/thingsboard/server/service/ota/OtaPackageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/ota/OtaPackageStateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java b/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java index 305696662b..bda4108ea7 100644 --- a/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java +++ b/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,16 +20,17 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import java.util.ArrayList; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -48,6 +49,8 @@ public abstract class AbstractPartitionBasedService extends protected final ConcurrentMap>> partitionedFetchTasks = new ConcurrentHashMap<>(); final Queue> subscribeQueue = new ConcurrentLinkedQueue<>(); + @Autowired + protected PartitionService partitionService; protected ListeningScheduledExecutorService scheduledExecutor; abstract protected String getServiceName(); diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java index 23d0e1b616..6d6a6b88b9 100644 --- a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java index 8ec29fff81..2b5a641cd5 100644 --- a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java index 6c332e63ff..e9b104168e 100644 --- a/application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java index 03fc3e36ce..536eaab883 100644 --- a/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index d2a6369c51..89b4002223 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,12 +29,14 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.KvUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; @@ -55,7 +57,6 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.security.AccessValidator; @@ -205,6 +206,11 @@ public class DefaultEntityQueryService implements EntityQueryService { } } + @Override + public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) { + return alarmService.countAlarmsByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); + } + private EntityDataQuery buildEntityDataQuery(AlarmDataQuery query) { EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityDataSortOrder entitiesSortOrder; diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java index a2f1efb1e4..cb936de31c 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -34,6 +35,8 @@ public interface EntityQueryService { PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query); + long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query); + DeferredResult getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, boolean isTimeseries, boolean isAttributes); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java index d7a333a090..24c55d9efe 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index e2779ba28b..76631aaa95 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -410,11 +410,13 @@ public class DefaultTbClusterService implements TbClusterService { if (entityType.equals(EntityType.TENANT) || entityType.equals(EntityType.TENANT_PROFILE) || entityType.equals(EntityType.DEVICE_PROFILE) + || (entityType.equals(EntityType.ASSET) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || entityType.equals(EntityType.ASSET_PROFILE) || entityType.equals(EntityType.API_USAGE_STATE) || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || entityType.equals(EntityType.ENTITY_VIEW) - || entityType.equals(EntityType.EDGE)) { + || entityType.equals(EntityType.EDGE) + || entityType.equals(EntityType.NOTIFICATION_RULE)) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); for (String serviceId : tbCoreServices) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 931acf908c..2bd0043046 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,25 +18,28 @@ package org.thingsboard.server.service.queue; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; -import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.stats.StatsFactory; -import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; @@ -60,11 +63,14 @@ import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.edge.EdgeNotificationService; +import org.thingsboard.server.service.notification.NotificationSchedulerService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; @@ -72,12 +78,16 @@ import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import org.thingsboard.server.service.sync.vc.GitVersionControlQueueService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; +import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -120,6 +130,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer; private final TbQueueConsumer> firmwareStatesConsumer; @@ -145,8 +157,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService jwtSettingsService) { - super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService); + ApplicationEventPublisher eventPublisher, + Optional jwtSettingsService, + NotificationSchedulerService notificationSchedulerService, + NotificationRuleProcessor notificationRuleProcessor) { + super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService); this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer(); this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer(); this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer(); @@ -159,6 +174,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService notificationRuleTrigger = encodingService.decode(toCoreNotification + .getNotificationRuleProcessorMsg().getTrigger().toByteArray()); + notificationRuleTrigger.ifPresent(notificationRuleProcessor::process); + callback.onSuccess(); } if (statsEnabled) { stats.log(toCoreNotification); @@ -458,6 +486,18 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> nfConsumer; protected final Optional jwtSettingsService; @@ -83,7 +85,8 @@ public abstract class AbstractConsumerService> nfConsumer, Optional jwtSettingsService) { + PartitionService partitionService, ApplicationEventPublisher eventPublisher, + TbQueueConsumer> nfConsumer, Optional jwtSettingsService) { this.actorContext = actorContext; this.encodingService = encodingService; this.tenantProfileCache = tenantProfileCache; @@ -91,6 +94,7 @@ public abstract class AbstractConsumerService objectModels = - ddfFileParser.parse(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); - if (objectModels.size() == 0) { - return null; - } else { - ObjectModel obj = objectModels.get(0); - LwM2mObject lwM2mObject = new LwM2mObject(); - lwM2mObject.setId(obj.id); - lwM2mObject.setKeyId(resource.getResourceKey()); - lwM2mObject.setName(obj.name); - lwM2mObject.setMultiple(obj.multiple); - lwM2mObject.setMandatory(obj.mandatory); - LwM2mInstance instance = new LwM2mInstance(); - instance.setId(0); - List resources = new ArrayList<>(); - obj.resources.forEach((k, v) -> { - if (isSave) { - LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); - resources.add(lwM2MResourceObserve); - } else if (v.operations.isReadable()) { - LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); - resources.add(lwM2MResourceObserve); - } - }); - if (isSave || resources.size() > 0) { - instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); - lwM2mObject.setInstances(new LwM2mInstance[]{instance}); - return lwM2mObject; - } else { - return null; - } - } - } catch (IOException | InvalidDDFFileException e) { - log.error("Could not parse the XML of objectModel with name [{}]", resource.getSearchText(), e); - return null; - } - } - @Override public TbResource save(TbResource tbResource, User user) throws ThingsboardException { ActionType actionType = tbResource.getId() == null ? ActionType.ADDED : ActionType.UPDATED; @@ -215,35 +161,11 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements throw new DataValidationException("Resource data should be specified!"); } if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) { - try { - List objectModels = - ddfFileParser.parse(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); - if (!objectModels.isEmpty()) { - ObjectModel objectModel = objectModels.get(0); - - String resourceKey = objectModel.id + LWM2M_SEPARATOR_KEY + objectModel.version; - String name = objectModel.name; - resource.setResourceKey(resourceKey); - if (resource.getId() == null) { - resource.setTitle(name + " id=" + objectModel.id + " v" + objectModel.version); - } - resource.setSearchText(resourceKey + LWM2M_SEPARATOR_SEARCH_TEXT + name); - } else { - throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); - } - } catch (InvalidDDFFileException e) { - log.error("Failed to parse file {}", resource.getFileName(), e); - throw new DataValidationException("Failed to parse file " + resource.getFileName()); - } catch (IOException e) { - throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); - } - if (resource.getResourceType().equals(ResourceType.LWM2M_MODEL) && toLwM2mObject(resource, true) == null) { - throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); - } + toLwm2mResource(resource); } else { resource.setResourceKey(resource.getFileName()); } - return resourceService.saveResource(resource); } } + diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index 6fd95660fc..024d391482 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java index fe70f441f5..bca7c7d81a 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package org.thingsboard.server.service.rpc; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.cluster.TbClusterService; @@ -57,8 +57,6 @@ import java.util.function.Consumer; @TbCoreComponent public class DefaultTbCoreDeviceRpcService implements TbCoreDeviceRpcService { - private static final ObjectMapper json = new ObjectMapper(); - private final DeviceService deviceService; private final TbClusterService clusterService; private final TbServiceInfoProvider serviceInfoProvider; @@ -159,7 +157,7 @@ public class DefaultTbCoreDeviceRpcService implements TbCoreDeviceRpcService { } private void sendRpcRequestToRuleEngine(ToDeviceRpcRequest msg, SecurityUser currentUser) { - ObjectNode entityNode = json.createObjectNode(); + ObjectNode entityNode = JacksonUtil.newObjectNode(); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("requestUUID", msg.getId().toString()); metaData.putValue("originServiceId", serviceId); @@ -184,9 +182,9 @@ public class DefaultTbCoreDeviceRpcService implements TbCoreDeviceRpcService { entityNode.put(DataConstants.ADDITIONAL_INFO, msg.getAdditionalInfo()); try { - TbMsg tbMsg = TbMsg.newMsg(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE, msg.getDeviceId(), Optional.ofNullable(currentUser).map(User::getCustomerId).orElse(null), metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode)); + TbMsg tbMsg = TbMsg.newMsg(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE, msg.getDeviceId(), Optional.ofNullable(currentUser).map(User::getCustomerId).orElse(null), metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); clusterService.pushMsgToRuleEngine(msg.getTenantId(), msg.getDeviceId(), tbMsg, null); - } catch (JsonProcessingException e) { + } catch (IllegalArgumentException e) { throw new RuntimeException(e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java index c0f6cc750f..1fef6acbc0 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; +import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse; +import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rpc.Rpc; import org.thingsboard.server.common.data.rpc.RpcError; -import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; -import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse; import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -35,7 +36,6 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.util.TbRuleEngineComponent; -import org.thingsboard.server.cluster.TbClusterService; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java index 128a1a5b84..2a3e30adcf 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,10 @@ package org.thingsboard.server.service.rpc; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.ToString; -import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; @ToString diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/LocalRequestMetaData.java b/application/src/main/java/org/thingsboard/server/service/rpc/LocalRequestMetaData.java index 5f3f01756d..cdb6e5586a 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/LocalRequestMetaData.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/LocalRequestMetaData.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/RemoveRpcActorMsg.java b/application/src/main/java/org/thingsboard/server/service/rpc/RemoveRpcActorMsg.java index 9173d4455c..c0b0c64b2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/RemoveRpcActorMsg.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/RemoveRpcActorMsg.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/TbCoreDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/TbCoreDeviceRpcService.java index 5443de70fc..9b81f4c6de 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/TbCoreDeviceRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/TbCoreDeviceRpcService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java index 47fdab1658..ad40176cef 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; @@ -31,7 +32,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.cluster.TbClusterService; @TbCoreComponent @Service diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java index 7464de6d03..1d460dca82 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/ToDeviceRpcRequestActorMsg.java b/application/src/main/java/org/thingsboard/server/service/rpc/ToDeviceRpcRequestActorMsg.java index 5c86292933..64c8887edb 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/ToDeviceRpcRequestActorMsg.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/ToDeviceRpcRequestActorMsg.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,10 @@ package org.thingsboard.server.service.rpc; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.ToString; -import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest; /** diff --git a/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java index 28a4e1e854..c2e425e14a 100644 --- a/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java +++ b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java b/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java index a8c3fed780..02b79544f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java +++ b/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java index 9b5a678d35..8a8c3213a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java index 3a88bb65a4..c8af61addf 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java index 08b2d5e245..0718c7383d 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -124,7 +124,7 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine(); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 0f32e31cf6..9bb0a40a78 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.OtaPackageInfo; @@ -67,6 +66,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.resource.ResourceService; @@ -75,11 +75,11 @@ import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.exception.ToErrorResponseEntity; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.AccessControlService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity; import javax.annotation.Nullable; import javax.annotation.PostConstruct; @@ -263,8 +263,7 @@ public class AccessValidator { if (currentUser.isSystemAdmin()) { callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); } else { - ListenableFuture deviceFuture = deviceService.findDeviceByIdAsync(currentUser.getTenantId(), new DeviceId(entityId.getId())); - Futures.addCallback(deviceFuture, getCallback(callback, device -> { + Futures.addCallback(Futures.immediateFuture(deviceService.findDeviceById(currentUser.getTenantId(), new DeviceId(entityId.getId()))), getCallback(callback, device -> { if (device == null) { return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND); } else { @@ -575,7 +574,7 @@ public class AccessValidator { ResponseEntity responseEntity; if (e instanceof ToErrorResponseEntity) { responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); - } else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException) { + } else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException || e instanceof DataValidationException) { responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); } else { responseEntity = new ResponseEntity<>(defaultErrorStatus); diff --git a/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java b/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java index ea90a7ae69..6c52d8e3ed 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java +++ b/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,10 @@ package org.thingsboard.server.service.security; import com.google.common.util.concurrent.FutureCallback; -import org.thingsboard.server.service.telemetry.exception.AccessDeniedException; -import org.thingsboard.server.service.telemetry.exception.EntityNotFoundException; -import org.thingsboard.server.service.telemetry.exception.InternalErrorException; -import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; +import org.thingsboard.server.exception.AccessDeniedException; +import org.thingsboard.server.exception.EntityNotFoundException; +import org.thingsboard.server.exception.InternalErrorException; +import org.thingsboard.server.exception.UnauthorizedException; /** * Created by ashvayka on 31.03.18. diff --git a/application/src/main/java/org/thingsboard/server/service/security/ValidationResult.java b/application/src/main/java/org/thingsboard/server/service/security/ValidationResult.java index 7b91c7634a..7b22faee0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/ValidationResult.java +++ b/application/src/main/java/org/thingsboard/server/service/security/ValidationResult.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/ValidationResultCode.java b/application/src/main/java/org/thingsboard/server/service/security/ValidationResultCode.java index cf2bc3dfe2..95710165b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/ValidationResultCode.java +++ b/application/src/main/java/org/thingsboard/server/service/security/ValidationResultCode.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractJwtAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractJwtAuthenticationToken.java index 563bc40b7b..3ed58951f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractJwtAuthenticationToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractJwtAuthenticationToken.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java index 74230d93ec..9f33466793 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.thingsboard.server.service.security.auth; import io.jsonwebtoken.Claims; -import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/JwtAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/JwtAuthenticationToken.java index 1b854313c4..7cce7fce5c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/JwtAuthenticationToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/JwtAuthenticationToken.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java index 8c70e69179..4d775b1e40 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/RefreshAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/RefreshAuthenticationToken.java index ca76984c0a..5fedfdad99 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/RefreshAuthenticationToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/RefreshAuthenticationToken.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index 02c1bf685a..20df639619 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java index 9ce2ae75fc..e0242126fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java index 164eb6df45..73cfe193e1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 27549752e4..e75d3aa4d1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java index 7937641d16..66c206a7ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.security.auth.jwt; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationServiceException; @@ -25,6 +24,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; @@ -42,14 +42,12 @@ public class RefreshTokenProcessingFilter extends AbstractAuthenticationProcessi private final AuthenticationSuccessHandler successHandler; private final AuthenticationFailureHandler failureHandler; - private final ObjectMapper objectMapper; public RefreshTokenProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, - AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { + AuthenticationFailureHandler failureHandler) { super(defaultProcessUrl); this.successHandler = successHandler; this.failureHandler = failureHandler; - this.objectMapper = mapper; } @Override @@ -64,7 +62,7 @@ public class RefreshTokenProcessingFilter extends AbstractAuthenticationProcessi RefreshTokenRequest refreshTokenRequest; try { - refreshTokenRequest = objectMapper.readValue(request.getReader(), RefreshTokenRequest.class); + refreshTokenRequest = JacksonUtil.fromReader(request.getReader(), RefreshTokenRequest.class); } catch (Exception e) { throw new AuthenticationServiceException("Invalid refresh token request payload"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java index bdc5d403f1..3d849b1d3e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java index 7c5822792a..b4c4843d1b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java index 0b90cd8047..2f01decc73 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java index cb40379c22..7a458ef052 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java index 26cbc97bdd..b1976dce9a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index 36beaecea3..d159c469c0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index bf83179b78..dddb011fd5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,10 +36,10 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator { if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { throw new DataValidationException("JWT token issuer should be specified!"); } - if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) { + if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) < TimeUnit.MINUTES.toSeconds(15)) { throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!"); } - if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) { + if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) < TimeUnit.MINUTES.toSeconds(1)) { throw new DataValidationException("JWT token expiration time should be at least 1 minute!"); } if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java index 6202e60c96..2081967677 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java index bf02be573c..7fc4306ec6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java index 06a82fc808..209ab22b78 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index 0f36273f41..a58c801583 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.thingsboard.server.service.security.auth.mfa; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.LockedException; import org.springframework.stereotype.Service; @@ -29,8 +30,9 @@ import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettin import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; -import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider; @@ -41,8 +43,6 @@ import java.util.Collection; import java.util.EnumMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; @Service @RequiredArgsConstructor @@ -52,14 +52,13 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private final TwoFaConfigManager configManager; private final SystemSecurityService systemSecurityService; private final UserService userService; + private final RateLimitService rateLimitService; private final Map> providers = new EnumMap<>(TwoFaProviderType.class); private static final ThingsboardException ACCOUNT_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS); private static final ThingsboardException PROVIDER_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS); private static final ThingsboardException PROVIDER_NOT_AVAILABLE_ERROR = new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.GENERAL); - - private final ConcurrentMap> verificationCodeSendingRateLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap> verificationCodeCheckingRateLimits = new ConcurrentHashMap<>(); + private static final ThingsboardException TOO_MANY_REQUESTS_ERROR = new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); @Override public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) { @@ -91,7 +90,10 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { if (minVerificationCodeSendPeriod != null && minVerificationCodeSendPeriod > 4) { rateLimit = "1:" + minVerificationCodeSendPeriod; } - checkRateLimits(user.getId(), accountConfig.getProviderType(), rateLimit, verificationCodeSendingRateLimits); + if (!rateLimitService.checkRateLimit(LimitedApi.TWO_FA_VERIFICATION_CODE_SEND, + Pair.of(user.getId(), accountConfig.getProviderType()), rateLimit)) { + throw TOO_MANY_REQUESTS_ERROR; + } } TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) @@ -116,7 +118,10 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { PlatformTwoFaSettings twoFaSettings = configManager.getPlatformTwoFaSettings(user.getTenantId(), true) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); if (checkLimits) { - checkRateLimits(user.getId(), accountConfig.getProviderType(), twoFaSettings.getVerificationCodeCheckRateLimit(), verificationCodeCheckingRateLimits); + if (!rateLimitService.checkRateLimit(LimitedApi.TWO_FA_VERIFICATION_CODE_CHECK, + Pair.of(user.getId(), accountConfig.getProviderType()), twoFaSettings.getVerificationCodeCheckRateLimit())) { + throw TOO_MANY_REQUESTS_ERROR; + } } TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); @@ -131,43 +136,28 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { try { systemSecurityService.validateTwoFaVerification(user, verificationSuccess, twoFaSettings); } catch (LockedException e) { - verificationCodeCheckingRateLimits.remove(user.getId()); - verificationCodeSendingRateLimits.remove(user.getId()); + cleanUpRateLimits(user.getId()); throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.AUTHENTICATION); } if (verificationSuccess) { - verificationCodeCheckingRateLimits.remove(user.getId()); - verificationCodeSendingRateLimits.remove(user.getId()); + cleanUpRateLimits(user.getId()); } } return verificationSuccess; } - private void checkRateLimits(UserId userId, TwoFaProviderType providerType, String rateLimitConfig, - ConcurrentMap> rateLimits) throws ThingsboardException { - if (StringUtils.isNotEmpty(rateLimitConfig)) { - ConcurrentMap providersRateLimits = rateLimits.computeIfAbsent(userId, i -> new ConcurrentHashMap<>()); - - TbRateLimits rateLimit = providersRateLimits.get(providerType); - if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) { - rateLimit = new TbRateLimits(rateLimitConfig, true); - providersRateLimits.put(providerType, rateLimit); - } - if (!rateLimit.tryConsume()) { - throw new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); - } - } else { - rateLimits.remove(userId); - } - } - - @Override public TwoFaAccountConfig generateNewAccountConfig(User user, TwoFaProviderType providerType) throws ThingsboardException { TwoFaProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType); return getTwoFaProvider(providerType).generateNewAccountConfig(user, providerConfig); } + private void cleanUpRateLimits(UserId userId) { + for (TwoFaProviderType providerType : TwoFaProviderType.values()) { + rateLimitService.cleanUp(LimitedApi.TWO_FA_VERIFICATION_CODE_SEND, Pair.of(userId, providerType)); + rateLimitService.cleanUp(LimitedApi.TWO_FA_VERIFICATION_CODE_CHECK, Pair.of(userId, providerType)); + } + } private TwoFaProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException { return configManager.getPlatformTwoFaSettings(tenantId, true) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index aabda27ec7..b1bdf82bbf 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java index ad2ecebd2c..4d85de7627 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java index c0e3200a4d..ead1fb7411 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java index 2522d3e41b..ff60a1c798 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java index ac8d76b52a..4a9c6c4e14 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,12 @@ package org.thingsboard.server.service.security.auth.mfa.provider.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.CollectionsUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.security.model.mfa.account.BackupCodeTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.BackupCodeTwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java index 085b5a9f45..d762d00819 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java index b75e81be6f..96f382959d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java index 419a60b3b9..65a197153f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,14 @@ import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; @@ -36,10 +38,12 @@ import java.util.Map; public class SmsTwoFaProvider extends OtpBasedTwoFaProvider { private final SmsService smsService; + private final AuditLogService auditLogService; - public SmsTwoFaProvider(CacheManager cacheManager, SmsService smsService) { + public SmsTwoFaProvider(CacheManager cacheManager, SmsService smsService, AuditLogService auditLogService) { super(cacheManager); this.smsService = smsService; + this.auditLogService = auditLogService; } @@ -56,8 +60,13 @@ public class SmsTwoFaProvider extends OtpBasedTwoFaProvider dashboardIdOpt = diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java index 5b2036ce26..9bbb3643e3 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java index e8e1011a47..393b652d63 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java index 42f44a712d..91de70e35c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java index e0f639b2f9..365eefafbf 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,30 @@ */ package org.thingsboard.server.service.security.auth.oauth2; -import org.springframework.util.SerializationUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.jackson2.SecurityJackson2Modules; + import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; import java.util.Base64; import java.util.Optional; +@Slf4j public class CookieUtils { + private static final ObjectMapper OBJECT_MAPPER; + + static { + ClassLoader loader = CookieUtils.class.getClassLoader(); + OBJECT_MAPPER = new ObjectMapper(); + OBJECT_MAPPER.registerModules(SecurityJackson2Modules.getModules(loader)); + } + public static Optional getCookie(HttpServletRequest request, String name) { Cookie[] cookies = request.getCookies(); @@ -49,7 +64,7 @@ public class CookieUtils { public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { Cookie[] cookies = request.getCookies(); if (cookies != null && cookies.length > 0) { - for (Cookie cookie: cookies) { + for (Cookie cookie : cookies) { if (cookie.getName().equals(name)) { cookie.setValue(""); cookie.setPath("/"); @@ -61,12 +76,22 @@ public class CookieUtils { } public static String serialize(Object object) { - return Base64.getUrlEncoder() - .encodeToString(SerializationUtils.serialize(object)); + try { + return Base64.getUrlEncoder() + .encodeToString(OBJECT_MAPPER.writeValueAsBytes(object)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("The given Json object value: " + + object + " cannot be transformed to a String", e); + } } public static T deserialize(Cookie cookie, Class cls) { - return cls.cast(SerializationUtils.deserialize( - Base64.getUrlDecoder().decode(cookie.getValue()))); + byte[] decodedBytes = Base64.getUrlDecoder().decode(cookie.getValue()); + try { + return OBJECT_MAPPER.readValue(decodedBytes, cls); + } catch (IOException e) { + throw new IllegalArgumentException("The given string value: " + + Arrays.toString(decodedBytes) + " cannot be transformed to Json object", e); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java index 69f654997e..9cac9b87a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,12 @@ package org.thingsboard.server.service.security.auth.oauth2; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; @@ -31,7 +30,6 @@ import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; -import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; @Service(value = "customOAuth2ClientMapper") @@ -40,19 +38,9 @@ import javax.servlet.http.HttpServletRequest; public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { private static final String PROVIDER_ACCESS_TOKEN = "provider-access-token"; - private static final ObjectMapper json = new ObjectMapper(); private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); - @PostConstruct - public void init() { - // Register time module to parse Instant objects. - // com.fasterxml.jackson.databind.exc.InvalidDefinitionException: - // Java 8 date/time type `java.time.Instant` not supported by default: - // add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling - json.registerModule(new JavaTimeModule()); - } - @Override public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { OAuth2MapperConfig config = registration.getMapperConfig(); @@ -71,7 +59,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme RestTemplate restTemplate = restTemplateBuilder.build(); String request; try { - request = json.writeValueAsString(token.getPrincipal()); + request = JacksonUtil.getObjectMapperWithJavaTimeModule().writeValueAsString(token.getPrincipal()); } catch (JsonProcessingException e) { log.error("Can't convert principal to JSON string", e); throw new RuntimeException("Can't convert principal to JSON string", e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java index d4d4d99005..0381e64298 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java index a6a5fa456f..d63f759dd3 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java index f84a2eef0a..23befd67c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java index 3c1ac4a5c6..3955cee0b3 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java index c12c6081f6..4251c4bad4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.thingsboard.server.service.security.auth.oauth2; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; @@ -27,7 +26,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.system.SystemSecurityService; -import org.thingsboard.server.utils.MiscUtils; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 7de6c3fe65..edfc826050 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,9 +30,9 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/TbOAuth2ParameterNames.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/TbOAuth2ParameterNames.java index 50f4424d2f..9211f06b98 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/TbOAuth2ParameterNames.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/TbOAuth2ParameterNames.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java index 43fbb3d1c4..78cf6b1f98 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java index 551868badb..8fdd443fe8 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java index 5f587e0974..1e01b4f975 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java index aedce8beae..1eafe66c8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import ua_parser.Client; import ua_parser.Parser; import javax.servlet.http.HttpServletRequest; -import java.io.IOException; import java.io.Serializable; @Data @@ -43,11 +42,7 @@ public class RestAuthenticationDetails implements Serializable { } private static Client getUserAgent(HttpServletRequest request) { - try { - Parser uaParser = new Parser(); - return uaParser.parse(request.getHeader("User-Agent")); - } catch (IOException e) { - return new Client(null, null, null); - } + Parser uaParser = new Parser(); + return uaParser.parse(request.getHeader("User-Agent")); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetailsSource.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetailsSource.java index affcd44d99..36ce529e24 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetailsSource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetailsSource.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index b3ef88dbff..7d612fef25 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java index 5a1ee92aaf..e5d20ac546 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java index f6d9fd8666..0229170781 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.security.auth.rest; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -23,10 +22,11 @@ import org.springframework.security.core.Authentication; import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; -import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -41,7 +41,6 @@ import java.util.concurrent.TimeUnit; @Component(value = "defaultAuthenticationSuccessHandler") @RequiredArgsConstructor public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { - private final ObjectMapper mapper; private final JwtTokenFactory tokenFactory; private final TwoFaConfigManager twoFaConfigManager; @@ -65,7 +64,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc response.setStatus(HttpStatus.OK.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); - mapper.writeValue(response.getWriter(), tokenPair); + JacksonUtil.writeValue(response.getWriter(), tokenPair); clearAuthenticationAttributes(request); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java index 16e6f50c94..b3ea3453b2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.security.auth.rest; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationDetailsSource; @@ -27,6 +26,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -45,14 +45,12 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF private final AuthenticationSuccessHandler successHandler; private final AuthenticationFailureHandler failureHandler; - private final ObjectMapper objectMapper; public RestLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, - AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { + AuthenticationFailureHandler failureHandler) { super(defaultProcessUrl); this.successHandler = successHandler; this.failureHandler = failureHandler; - this.objectMapper = mapper; } @Override @@ -67,7 +65,7 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF LoginRequest loginRequest; try { - loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class); + loginRequest = JacksonUtil.fromReader(request.getReader(), LoginRequest.class); } catch (Exception e) { throw new AuthenticationServiceException("Invalid login request payload"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java index d126739d18..23252c27ef 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.security.auth.rest; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationServiceException; @@ -26,6 +25,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -42,14 +42,12 @@ public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProce private final AuthenticationSuccessHandler successHandler; private final AuthenticationFailureHandler failureHandler; - private final ObjectMapper objectMapper; public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, - AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { + AuthenticationFailureHandler failureHandler) { super(defaultProcessUrl); this.successHandler = successHandler; this.failureHandler = failureHandler; - this.objectMapper = mapper; } @Override @@ -64,7 +62,7 @@ public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProce PublicLoginRequest loginRequest; try { - loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class); + loginRequest = JacksonUtil.fromReader(request.getReader(), PublicLoginRequest.class); } catch (Exception e) { throw new AuthenticationServiceException("Invalid public login request payload"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java index 872217b430..a278972335 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/exception/AuthMethodNotSupportedException.java b/application/src/main/java/org/thingsboard/server/service/security/exception/AuthMethodNotSupportedException.java index 556ecd0e96..bcfb7bd17e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/exception/AuthMethodNotSupportedException.java +++ b/application/src/main/java/org/thingsboard/server/service/security/exception/AuthMethodNotSupportedException.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java b/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java index ac95913070..87b7403a85 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java +++ b/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordExpiredException.java b/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordExpiredException.java index 6dd8a344a5..7f682ff6fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordExpiredException.java +++ b/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordExpiredException.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/ActivateUserRequest.java b/application/src/main/java/org/thingsboard/server/service/security/model/ActivateUserRequest.java index 50e918b0f4..aff95ea246 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/ActivateUserRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/ActivateUserRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/ChangePasswordRequest.java b/application/src/main/java/org/thingsboard/server/service/security/model/ChangePasswordRequest.java index 4a930403b4..4b21acef5e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/ChangePasswordRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/ChangePasswordRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordEmailRequest.java b/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordEmailRequest.java index 3b1563eb2d..93f9541986 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordEmailRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordEmailRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.security.model; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordRequest.java b/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordRequest.java index 8489f6f17c..3869da9b02 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java index b7f480dfab..7ae64ef2b2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java index 4eafe1673c..7af0954d53 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java index f96e0bfc33..c74954c88a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 71fd66231a..a222a7e83a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,10 +34,10 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; -import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java index bae8cbb1bb..fd2fc2079d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java index 86d66fb22c..f8a48d431f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/AbstractPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/AbstractPermissions.java index 2b36ff63e8..0d98b5cd88 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/AbstractPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/AbstractPermissions.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java index 3c79e4ab00..a702626644 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index cdd84b0478..1a8f87713e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -119,6 +119,15 @@ public class CustomerUserPermissions extends AbstractPermissions { if (!Authority.CUSTOMER_USER.equals(userEntity.getAuthority())) { return false; } + + if (!user.getCustomerId().equals(userEntity.getCustomerId())) { + return false; + } + + if (Operation.READ.equals(operation)) { + return true; + } + return user.getId().equals(userId); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java index 64c7f056b6..de733d8f55 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,9 +25,9 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.*; - -import static org.thingsboard.server.dao.service.Validator.validateId; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; @Service @Slf4j diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java index d261107719..9873f16447 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java b/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java index 217c488c7e..b2e795753c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,8 @@ */ package org.thingsboard.server.service.security.permission; -import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.security.model.SecurityUser; import java.util.Arrays; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Permissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Permissions.java index 9df6da09ab..53dd59603e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Permissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Permissions.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index a9484a2bd3..3f6ea7ae3b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,8 @@ package org.thingsboard.server.service.security.permission; import org.thingsboard.server.common.data.EntityType; -import java.util.Optional; +import java.util.Collections; +import java.util.Set; public enum Resource { ADMIN_SETTINGS(), @@ -43,25 +44,27 @@ public enum Resource { EDGE(EntityType.EDGE), RPC(EntityType.RPC), QUEUE(EntityType.QUEUE), - VERSION_CONTROL; + VERSION_CONTROL, + NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, + EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE); - private final EntityType entityType; + private final Set entityTypes; Resource() { - this.entityType = null; + this.entityTypes = Collections.emptySet(); } - Resource(EntityType entityType) { - this.entityType = entityType; + Resource(EntityType... entityTypes) { + this.entityTypes = Set.of(entityTypes); } - public Optional getEntityType() { - return Optional.ofNullable(entityType); + public Set getEntityTypes() { + return entityTypes; } public static Resource of(EntityType entityType) { for (Resource resource : Resource.values()) { - if (resource.getEntityType().orElse(null) == entityType) { + if (resource.getEntityTypes().contains(entityType)) { return resource; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index 20e2001c87..4169aa1c2e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); put(Resource.TB_RESOURCE, systemEntityPermissionChecker); put(Resource.QUEUE, systemEntityPermissionChecker); + put(Resource.NOTIFICATION, systemEntityPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index ef7991e5de..cb924cabca 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.RPC, tenantEntityPermissionChecker); put(Resource.QUEUE, queuePermissionChecker); put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker); + put(Resource.NOTIFICATION, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 97b433661a..726a5d98f0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -228,8 +228,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { if (userCredentials != null && isPositiveInteger(passwordPolicy.getPasswordReuseFrequencyDays())) { long passwordReuseFrequencyTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(passwordPolicy.getPasswordReuseFrequencyDays()); - User user = userService.findUserById(tenantId, userCredentials.getUserId()); - JsonNode additionalInfo = user.getAdditionalInfo(); + JsonNode additionalInfo = userCredentials.getAdditionalInfo(); if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) { JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY); Map userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {}); diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 39d5935906..64550c438e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,8 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.SecuritySettings; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.service.security.model.SecurityUser; import javax.servlet.http.HttpServletRequest; diff --git a/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java b/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java index 9e5667aa19..46c992e2d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java +++ b/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,22 +15,16 @@ */ package org.thingsboard.server.service.session; -import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.CachePut; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry; import org.thingsboard.server.queue.util.TbCoreComponent; -import java.io.Serializable; import java.util.Collections; -import static org.thingsboard.server.common.data.CacheConstants.SESSIONS_CACHE; - /** * Created by ashvayka on 29.10.18. */ @@ -52,7 +46,7 @@ public class DefaultDeviceSessionCacheService implements DeviceSessionCacheServi @Override public DeviceSessionsCacheEntry put(DeviceId deviceId, DeviceSessionsCacheEntry sessions) { log.debug("[{}] Pushing session data to cache: {}", deviceId, sessions); - cache.putIfAbsent(deviceId, sessions); + cache.put(deviceId, sessions); return sessions; } } diff --git a/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java b/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java index b01f94306a..c5ccd9ac78 100644 --- a/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java +++ b/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java index 92eba5c4e2..3daa7396a2 100644 --- a/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java +++ b/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,9 @@ package org.thingsboard.server.service.session; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.gen.transport.TransportProtos; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) diff --git a/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java b/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java index 27931a9a64..5d154532c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,14 @@ package org.thingsboard.server.service.session; import com.google.protobuf.InvalidProtocolBufferException; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.gen.transport.TransportProtos; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") diff --git a/application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java b/application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java new file mode 100644 index 0000000000..ee54a02620 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java @@ -0,0 +1,149 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.slack; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.slack.api.Slack; +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.SlackApiRequest; +import com.slack.api.methods.SlackApiTextResponse; +import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.methods.request.conversations.ConversationsListRequest; +import com.slack.api.methods.request.users.UsersListRequest; +import com.slack.api.methods.response.conversations.ConversationsListResponse; +import com.slack.api.methods.response.users.UsersListResponse; +import com.slack.api.model.ConversationType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.slack.SlackService; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; +import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; +import org.thingsboard.server.common.data.util.ThrowingBiFunction; +import org.thingsboard.server.dao.notification.NotificationSettingsService; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class DefaultSlackService implements SlackService { + + private final NotificationSettingsService notificationSettingsService; + + private final Slack slack = Slack.getInstance(); + private final Cache> cache = Caffeine.newBuilder() + .expireAfterWrite(20, TimeUnit.SECONDS) + .maximumSize(100) + .build(); + private static final int CONVERSATIONS_LOAD_LIMIT = 1000; + + @Override + public void sendMessage(TenantId tenantId, String token, String conversationId, String message) { + ChatPostMessageRequest request = ChatPostMessageRequest.builder() + .channel(conversationId) + .text(message) + .build(); + sendRequest(token, request, MethodsClient::chatPostMessage); + } + + @Override + public List listConversations(TenantId tenantId, String token, SlackConversationType conversationType) { + return cache.get(conversationType + ":" + token, k -> { + if (conversationType == SlackConversationType.DIRECT) { + UsersListRequest request = UsersListRequest.builder() + .limit(CONVERSATIONS_LOAD_LIMIT) + .build(); + + UsersListResponse response = sendRequest(token, request, MethodsClient::usersList); + return response.getMembers().stream() + .filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot()) + .map(user -> { + SlackConversation conversation = new SlackConversation(); + conversation.setType(conversationType); + conversation.setId(user.getId()); + conversation.setName(user.getName()); + conversation.setWholeName(user.getProfile() != null ? user.getProfile().getRealNameNormalized() : user.getRealName()); + conversation.setEmail(user.getProfile() != null ? user.getProfile().getEmail() : null); + return conversation; + }) + .collect(Collectors.toList()); + } else { + ConversationsListRequest request = ConversationsListRequest.builder() + .types(List.of(conversationType == SlackConversationType.PUBLIC_CHANNEL ? + ConversationType.PUBLIC_CHANNEL : + ConversationType.PRIVATE_CHANNEL)) + .limit(CONVERSATIONS_LOAD_LIMIT) + .excludeArchived(true) + .build(); + + ConversationsListResponse response = sendRequest(token, request, MethodsClient::conversationsList); + return response.getChannels().stream() + .filter(channel -> !channel.isArchived()) + .map(channel -> { + SlackConversation conversation = new SlackConversation(); + conversation.setType(conversationType); + conversation.setId(channel.getId()); + conversation.setName(channel.getName()); + conversation.setWholeName(channel.getNameNormalized()); + return conversation; + }) + .collect(Collectors.toList()); + } + }); + } + + @Override + public String getToken(TenantId tenantId) { + NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); + SlackNotificationDeliveryMethodConfig slackConfig = (SlackNotificationDeliveryMethodConfig) + settings.getDeliveryMethodsConfigs().get(NotificationDeliveryMethod.SLACK); + if (slackConfig != null) { + return slackConfig.getBotToken(); + } else { + return null; + } + } + + private R sendRequest(String token, T request, ThrowingBiFunction method) { + MethodsClient client = slack.methods(token); + R response; + try { + response = method.apply(client, request); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + + if (!response.isOk()) { + String error = response.getError(); + if (error == null) { + error = "unknown error"; + } else if (error.contains("missing_scope")) { + String neededScope = response.getNeeded(); + error = "bot token scope '" + neededScope + "' is needed"; + } + throw new RuntimeException("Slack API error: " + error); + } + + return response; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java b/application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java index 7997235ee1..73949552ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java index b42dcb6b83..6768259a84 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java index b51221b1ba..ff1304208d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,21 +19,22 @@ import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.springframework.core.NestedRuntimeException; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.sms.SmsSender; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.sms.config.SmsProviderConfiguration; -import org.thingsboard.server.common.data.sms.config.TestSmsRequest; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.sms.config.SmsProviderConfiguration; +import org.thingsboard.server.common.data.sms.config.TestSmsRequest; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.settings.AdminSettingsService; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.transport.DefaultTransportApiService; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -86,7 +87,7 @@ public class DefaultSmsService implements SmsService { } } - private int sendSms(String numberTo, String message) throws ThingsboardException { + protected int sendSms(String numberTo, String message) throws ThingsboardException { if (this.smsSender == null) { throw new ThingsboardException("Unable to send SMS: no SMS provider configured!", ThingsboardErrorCode.GENERAL); } @@ -130,7 +131,9 @@ public class DefaultSmsService implements SmsService { private int sendSms(SmsSender smsSender, String numberTo, String message) throws ThingsboardException { try { - return smsSender.sendSms(numberTo, message); + int sentSms = smsSender.sendSms(numberTo, message); + log.trace("Successfully sent sms to number: {}", numberTo); + return sentSms; } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/sms/SmsExecutorService.java b/application/src/main/java/org/thingsboard/server/service/sms/SmsExecutorService.java index 3e377ab6bb..2c022d2311 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/SmsExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/SmsExecutorService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java b/application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java index 456ddb7a07..07244bf85e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sms/smpp/SmppSmsSender.java b/application/src/main/java/org/thingsboard/server/service/sms/smpp/SmppSmsSender.java index 8509b4e72f..b5fd663e4b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/smpp/SmppSmsSender.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/smpp/SmppSmsSender.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java b/application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java index f9b010670d..dfd2d8477b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index d52c712074..e33e501fbc 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,19 +24,25 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -60,6 +66,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.sql.query.EntityQueryRepository; @@ -81,6 +88,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -107,6 +115,7 @@ import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @Service @TbCoreComponent @Slf4j +@RequiredArgsConstructor public class DefaultDeviceStateService extends AbstractPartitionBasedService implements DeviceStateService { public static final String ACTIVITY_STATE = "active"; @@ -138,18 +147,18 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService PERSISTENT_ENTITY_FIELDS = Arrays.asList( new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "type"), + new EntityKey(EntityKeyType.ENTITY_FIELD, "label"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); - private final TenantService tenantService; private final DeviceService deviceService; private final AttributesService attributesService; private final TimeseriesService tsService; private final TbClusterService clusterService; private final PartitionService partitionService; - private final TbServiceInfoProvider serviceInfoProvider; private final EntityQueryRepository entityQueryRepository; private final DbTypeInfoComponent dbTypeInfoComponent; - + private final TbApiUsageReportClient apiUsageReportClient; + @Autowired @Lazy private TelemetrySubscriptionService tsSubService; @Value("${state.defaultInactivityTimeoutInSec}") @@ -166,6 +175,10 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceStates = new ConcurrentHashMap<>(); - public DefaultDeviceStateService(TenantService tenantService, DeviceService deviceService, - AttributesService attributesService, TimeseriesService tsService, - TbClusterService clusterService, PartitionService partitionService, - TbServiceInfoProvider serviceInfoProvider, - EntityQueryRepository entityQueryRepository, - DbTypeInfoComponent dbTypeInfoComponent) { - this.tenantService = tenantService; - this.deviceService = deviceService; - this.attributesService = attributesService; - this.tsService = tsService; - this.clusterService = clusterService; - this.partitionService = partitionService; - this.serviceInfoProvider = serviceInfoProvider; - this.entityQueryRepository = entityQueryRepository; - this.dbTypeInfoComponent = dbTypeInfoComponent; - } - - @Autowired - public void setTsSubService(TelemetrySubscriptionService tsSubService) { - this.tsSubService = tsSubService; - } - @PostConstruct public void init() { super.init(); deviceStateExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool( Math.max(4, Runtime.getRuntime().availableProcessors()), "device-state")); - scheduledExecutor.scheduleAtFixedRate(this::updateInactivityStateIfExpired, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS); + scheduledExecutor.scheduleWithFixedDelay(this::checkStates, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS); + scheduledExecutor.scheduleWithFixedDelay(this::reportActivityStats, defaultActivityStatsIntervalInSec, defaultActivityStatsIntervalInSec, TimeUnit.SECONDS); } @PreDestroy @@ -289,7 +281,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService { log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size()); + Set idsFromRemovedTenant = new HashSet<>(); for (DeviceId deviceId : deviceIds) { + DeviceStateData stateData; try { - updateInactivityStateIfExpired(ts, deviceId); + stateData = getOrFetchDeviceStateData(deviceId); } catch (Exception e) { - log.warn("[{}] Failed to update inactivity state", deviceId, e); + log.error("[{}] Failed to get or fetch device state data", deviceId, e); + continue; + } + try { + updateInactivityStateIfExpired(ts, deviceId, stateData); + } catch (Exception e) { + if (e instanceof TenantNotFoundException) { + idsFromRemovedTenant.add(deviceId); + } else { + log.warn("[{}] Failed to update inactivity state [{}]", deviceId, e.getMessage()); + } } } + deviceIds.removeAll(idsFromRemovedTenant); }); } catch (Throwable t) { - log.warn("Failed to update inactivity states", t); + log.warn("Failed to check devices states", t); } } - void updateInactivityStateIfExpired(long ts, DeviceId deviceId) { - DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); - updateInactivityStateIfExpired(ts, deviceId, stateData); + void reportActivityStats() { + try{ + Map> stats = new HashMap<>(); + for (DeviceStateData stateData : deviceStates.values()) { + Pair tenantDevicesActivity = stats.computeIfAbsent(stateData.getTenantId(), + tenantId -> Pair.of(new AtomicInteger(), new AtomicInteger())); + if (stateData.getState().isActive()) { + tenantDevicesActivity.getLeft().incrementAndGet(); + } else { + tenantDevicesActivity.getRight().incrementAndGet(); + } + } + + stats.forEach((tenantId, tenantDevicesActivity) -> { + int active = tenantDevicesActivity.getLeft().get(); + int inactive = tenantDevicesActivity.getRight().get(); + apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.ACTIVE_DEVICES, active); + apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.INACTIVE_DEVICES, inactive); + if (active > 0) { + log.debug("[{}] Active devices: {}, inactive devices: {}", tenantId, active, inactive); + } + }); + } catch (Throwable t) { + log.warn("Failed to report activity states", t); + } } void updateInactivityStateIfExpired(long ts, DeviceId deviceId, DeviceStateData stateData) { @@ -585,6 +613,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService { - TenantId tenantId = TenantId.fromUUID(id); try { + TenantId tenantId = TenantId.fromUUID(id); AssetId serviceAssetId = getServiceAssetId(tenantId, queueName); if (stats.getTotalMsgCounter().get() > 0) { List tsList = stats.getCounters().entrySet().stream() @@ -89,19 +87,19 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS tsService.saveAndNotifyInternal(tenantId, serviceAssetId, tsList, CALLBACK); } } - } catch (DataValidationException e) { - if (!e.getMessage().equalsIgnoreCase("Asset is referencing to non-existent tenant!")) { - throw e; + } catch (Exception e) { + if (!"Asset is referencing to non-existent tenant!".equalsIgnoreCase(e.getMessage())) { + log.debug("[{}] Failed to store the statistics", id, e); } } }); ruleEngineStats.getTenantExceptions().forEach((tenantId, e) -> { - TsKvEntry tsKv = new BasicTsKvEntry(e.getTs(), new JsonDataEntry("ruleEngineException", e.toJsonString())); try { + TsKvEntry tsKv = new BasicTsKvEntry(e.getTs(), new JsonDataEntry("ruleEngineException", e.toJsonString())); tsService.saveAndNotifyInternal(tenantId, getServiceAssetId(tenantId, queueName), Collections.singletonList(tsKv), CALLBACK); - } catch (DataValidationException e2) { - if (!e2.getMessage().equalsIgnoreCase("Asset is referencing to non-existent tenant!")) { - throw e2; + } catch (Exception e2) { + if (!"Asset is referencing to non-existent tenant!".equalsIgnoreCase(e2.getMessage())) { + log.debug("[{}] Failed to store the statistics", tenantId, e2); } } }); diff --git a/application/src/main/java/org/thingsboard/server/service/stats/RuleEngineStatisticsService.java b/application/src/main/java/org/thingsboard/server/service/stats/RuleEngineStatisticsService.java index 6e82c00e1a..b7f0ceeec5 100644 --- a/application/src/main/java/org/thingsboard/server/service/stats/RuleEngineStatisticsService.java +++ b/application/src/main/java/org/thingsboard/server/service/stats/RuleEngineStatisticsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index cdec2ebfad..60fce13560 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ */ package org.thingsboard.server.service.subscription; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; @@ -25,10 +25,11 @@ import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; @@ -59,8 +60,11 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.state.DeviceStateService; -import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -81,34 +85,18 @@ import java.util.function.Predicate; @Slf4j @TbCoreComponent @Service +@RequiredArgsConstructor public class DefaultSubscriptionManagerService extends TbApplicationEventListener implements SubscriptionManagerService { - @Autowired - private AttributesService attrService; - - @Autowired - private TimeseriesService tsService; - - @Autowired - private NotificationsTopicService notificationsTopicService; - - @Autowired - private PartitionService partitionService; - - @Autowired - private TbServiceInfoProvider serviceInfoProvider; - - @Autowired - private TbQueueProducerProvider producerProvider; - - @Autowired - private TbLocalSubscriptionService localSubscriptionService; - - @Autowired - private DeviceStateService deviceStateService; - - @Autowired - private TbClusterService clusterService; + private final AttributesService attrService; + private final TimeseriesService tsService; + private final NotificationsTopicService notificationsTopicService; + private final PartitionService partitionService; + private final TbServiceInfoProvider serviceInfoProvider; + private final TbQueueProducerProvider producerProvider; + private final TbLocalSubscriptionService localSubscriptionService; + private final DeviceStateService deviceStateService; + private final TbClusterService clusterService; private final Map> subscriptionsByEntityId = new ConcurrentHashMap<>(); private final Map> subscriptionsByWsSessionId = new ConcurrentHashMap<>(); @@ -268,7 +256,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene updateDeviceInactivityTimeout(tenantId, entityId, attributes); } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, - new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) + new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) , null); } } @@ -292,7 +280,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } @Override - public void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { + public void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) { onLocalAlarmSubUpdate(entityId, s -> { if (TbSubscriptionType.ALARMS.equals(s.getType())) { @@ -301,15 +289,14 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene return null; } }, - s -> alarm.getCreatedTime() >= s.getTs(), - s -> alarm, - false + s -> alarm.getCreatedTime() >= s.getTs() || alarm.getAssignTs() >= s.getTs(), + alarm, false ); callback.onSuccess(); } @Override - public void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { + public void onAlarmDeleted(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) { onLocalAlarmSubUpdate(entityId, s -> { if (TbSubscriptionType.ALARMS.equals(s.getType())) { @@ -319,12 +306,63 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } }, s -> alarm.getCreatedTime() >= s.getTs(), - s -> alarm, - true + alarm, true ); callback.onSuccess(); } + @Override + public void onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate notificationUpdate, TbCallback callback) { + Set subscriptions = subscriptionsByEntityId.get(recipientId); + if (subscriptions != null) { + NotificationsSubscriptionUpdate subscriptionUpdate = new NotificationsSubscriptionUpdate(notificationUpdate); + log.trace("Handling notificationUpdate for user {}: {}", recipientId, notificationUpdate); + subscriptions.stream() + .filter(subscription -> subscription.getType() == TbSubscriptionType.NOTIFICATIONS + || subscription.getType() == TbSubscriptionType.NOTIFICATIONS_COUNT) + .forEach(subscription -> onNotificationsSubUpdate(subscriptionUpdate, subscription)); + } + callback.onSuccess(); + } + + @Override + public void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate notificationRequestUpdate, TbCallback callback) { + NotificationsSubscriptionUpdate subscriptionUpdate = new NotificationsSubscriptionUpdate(notificationRequestUpdate); + subscriptionsByEntityId.forEach((entityId, subscriptions) -> { + if (entityId.getEntityType() != EntityType.USER) { + return; + } + log.trace("Handling notificationRequestUpdate for user {}: {}", entityId, notificationRequestUpdate); + subscriptions.forEach(subscription -> { + if (subscription.getType() != TbSubscriptionType.NOTIFICATIONS && + subscription.getType() != TbSubscriptionType.NOTIFICATIONS_COUNT) { + return; + } + if (!subscription.getTenantId().equals(tenantId)) { + return; + } + onNotificationsSubUpdate(subscriptionUpdate, subscription); + }); + }); + callback.onSuccess(); + } + + private void onNotificationsSubUpdate(NotificationsSubscriptionUpdate subscriptionUpdate, TbSubscription subscription) { + if (serviceId.equals(subscription.getServiceId())) { + log.trace("[{}][{}][{}] Subscription session is managed by current service, forwarding to localSubscriptionService (update: {})", + subscription.getServiceId(), subscription.getEntityId(), subscription.getSessionId(), subscriptionUpdate); + localSubscriptionService.onSubscriptionUpdate(subscription.getSessionId(), + subscription.getSubscriptionId(), subscriptionUpdate, TbCallback.EMPTY); + } else { + log.trace("[{}][{}][{}] Subscription session is not managed by current service (update: {})", + subscription.getServiceId(), subscription.getEntityId(), subscription.getSessionId(), subscriptionUpdate); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId()); + ToCoreNotificationMsg updateProto = TbSubscriptionUtils.notificationsSubUpdateToProto(subscription, subscriptionUpdate); + TbProtoQueueMsg queueMsg = new TbProtoQueueMsg<>(subscription.getEntityId().getId(), updateProto); + toCoreNotificationsProducer.send(tpi, queueMsg, null); + } + } + @Override public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback callback) { onLocalTelemetrySubUpdate(entityId, @@ -354,7 +392,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene deleteDeviceInactivityTimeout(tenantId, entityId, keys); } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, - new DeviceId(entityId.getId()), scope, keys), null); + new DeviceId(entityId.getId()), scope, keys), null); } } callback.onSuccess(); @@ -414,19 +452,20 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene private void onLocalAlarmSubUpdate(EntityId entityId, Function castFunction, Predicate filterFunction, - Function processFunction, boolean deleted) { + AlarmInfo alarm, boolean deleted) { Set entitySubscriptions = subscriptionsByEntityId.get(entityId); + if (alarm == null) { + log.warn("[{}] empty alarm update!", entityId); + return; + } if (entitySubscriptions != null) { entitySubscriptions.stream().map(castFunction).filter(Objects::nonNull).filter(filterFunction).forEach(s -> { - Alarm alarm = processFunction.apply(s); - if (alarm != null) { - if (serviceId.equals(s.getServiceId())) { - AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted); - localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); - } else { - TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); - toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null); - } + if (serviceId.equals(s.getServiceId())) { + AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted); + localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); + } else { + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); + toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null); } }); } else { @@ -557,12 +596,12 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene }); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( - LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) + LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) .build(); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); } - private TbProtoQueueMsg toProto(TbSubscription subscription, Alarm alarm, boolean deleted) { + private TbProtoQueueMsg toProto(TbSubscription subscription, AlarmInfo alarm, boolean deleted) { TbAlarmSubscriptionUpdateProto.Builder builder = TbAlarmSubscriptionUpdateProto.newBuilder(); builder.setSessionId(subscription.getSessionId()); @@ -571,8 +610,8 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene builder.setDeleted(deleted); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( - LocalSubscriptionServiceMsgProto.newBuilder() - .setAlarmSubUpdate(builder.build()).build()) + LocalSubscriptionServiceMsgProto.newBuilder() + .setAlarmSubUpdate(builder.build()).build()) .build(); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index a5a45be5b2..4b37ba26b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -48,22 +49,22 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; -import org.thingsboard.server.service.telemetry.cmd.v2.AggHistoryCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AggKey; -import org.thingsboard.server.service.telemetry.cmd.v2.AggTimeSeriesCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.GetTsCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; +import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggTimeSeriesCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.GetTsCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -94,8 +95,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc private static final int DEFAULT_LIMIT = 100; private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>(); - @Autowired - private TelemetryWebSocketService wsService; + @Autowired @Lazy + private WebSocketService wsService; @Autowired private EntityService entityService; @@ -166,7 +167,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } @Override - public void handleCmd(TelemetryWebSocketSessionRef session, EntityDataCmd cmd) { + public void handleCmd(WebSocketSessionRef session, EntityDataCmd cmd) { TbEntityDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); if (ctx != null) { log.debug("[{}][{}] Updating existing subscriptions using: {}", session.getSessionId(), cmd.getCmdId(), cmd); @@ -357,7 +358,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } @Override - public void handleCmd(TelemetryWebSocketSessionRef session, EntityCountCmd cmd) { + public void handleCmd(WebSocketSessionRef session, EntityCountCmd cmd) { TbEntityCountSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); if (ctx == null) { ctx = createSubCtx(session, cmd); @@ -377,7 +378,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } @Override - public void handleCmd(TelemetryWebSocketSessionRef session, AlarmDataCmd cmd) { + public void handleCmd(WebSocketSessionRef session, AlarmDataCmd cmd) { TbAlarmDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); if (ctx == null) { log.debug("[{}][{}] Creating new alarm subscription using: {}", session.getSessionId(), cmd.getCmdId(), cmd); @@ -408,6 +409,26 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } } + @Override + public void handleCmd(WebSocketSessionRef session, AlarmCountCmd cmd) { + TbAlarmCountSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); + if (ctx == null) { + ctx = createSubCtx(session, cmd); + long start = System.currentTimeMillis(); + ctx.fetchData(); + long end = System.currentTimeMillis(); + stats.getAlarmQueryInvocationCnt().incrementAndGet(); + stats.getAlarmQueryTimeSpent().addAndGet(end - start); + TbAlarmCountSubCtx finalCtx = ctx; + ScheduledFuture task = scheduler.scheduleWithFixedDelay( + () -> refreshDynamicQuery(finalCtx), + dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); + finalCtx.setRefreshTask(task); + } else { + log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd); + } + } + private boolean validate(TbAbstractSubCtx finalCtx) { if (finalCtx.isStopped()) { log.warn("[{}][{}][{}] Received validation task for already stopped context.", finalCtx.getTenantId(), finalCtx.getSessionId(), finalCtx.getCmdId()); @@ -468,7 +489,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } } - private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { + private TbEntityDataSubCtx createSubCtx(WebSocketSessionRef sessionRef, EntityDataCmd cmd) { Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription); @@ -479,7 +500,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc return ctx; } - private TbEntityCountSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) { + private TbEntityCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, EntityCountCmd cmd) { Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); TbEntityCountSubCtx ctx = new TbEntityCountSubCtx(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmd.getCmdId()); @@ -491,7 +512,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } - private TbAlarmDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { + private TbAlarmDataSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) { Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription, @@ -501,6 +522,17 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc return ctx; } + private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService, + attributesService, stats, alarmService, sessionRef, cmd.getCmdId()); + if (cmd.getQuery() != null) { + ctx.setAndResolveQuery(cmd.getQuery()); + } + sessionSubs.put(cmd.getCmdId(), ctx); + return ctx; + } + @SuppressWarnings("unchecked") private T getSubCtx(String sessionId, int cmdId) { Map sessionSubs = subscriptionsBySessionId.get(sessionId); @@ -562,8 +594,13 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc if (queryResults != null) { for (ReadTsKvQueryResult queryResult : queryResults) { String queryKey = queriesKeys.get(queryResult.getQueryId()); - entityData.getTimeseries().put(queryKey, queryResult.toTsValues()); - lastTsMap.put(queryKey, queryResult.getLastEntryTs()); + if (queryKey != null) { + entityData.getTimeseries().merge(queryKey, queryResult.toTsValues(), ArrayUtils::addAll); + lastTsMap.merge(queryKey, queryResult.getLastEntryTs(), Math::max); + } else { + log.warn("ReadTsKvQueryResult for {} {} has queryId not matching the initial query", + entityData.getEntityId().getEntityType(), entityData.getEntityId()); + } } } // Populate with empty values if no data found. diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 968e15ea4b..64f1de8662 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,18 +21,19 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; -import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbApplicationEventListener; +import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -152,7 +153,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value)); break; } - subscriptionUpdateExecutor.submit(() -> subscription.getUpdateConsumer().accept(sessionId, update)); + subscriptionUpdateExecutor.submit(() -> subscription.getUpdateProcessor().accept(subscription, update)); } callback.onSuccess(); } @@ -163,7 +164,17 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer TbSubscription subscription = subscriptionsBySessionId .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId()); if (subscription != null && subscription.getType() == TbSubscriptionType.ALARMS) { - subscriptionUpdateExecutor.submit(() -> subscription.getUpdateConsumer().accept(sessionId, update)); + subscriptionUpdateExecutor.submit(() -> subscription.getUpdateProcessor().accept(subscription, update)); + } + callback.onSuccess(); + } + + @Override + public void onSubscriptionUpdate(String sessionId, int subscriptionId, NotificationsSubscriptionUpdate update, TbCallback callback) { + TbSubscription subscription = subscriptionsBySessionId.getOrDefault(sessionId, Collections.emptyMap()).get(subscriptionId); + if (subscription != null && (subscription.getType() == TbSubscriptionType.NOTIFICATIONS + || subscription.getType() == TbSubscriptionType.NOTIFICATIONS_COUNT)) { + subscriptionUpdateExecutor.submit(() -> subscription.getUpdateProcessor().accept(subscription, update)); } callback.onSuccess(); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java b/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java index 28ca2f6729..4cfd218893 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package org.thingsboard.server.service.subscription; import lombok.Data; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.service.telemetry.cmd.v2.AggKey; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey; @Data public class ReadTsKvQueryInfo { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionErrorCode.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionErrorCode.java new file mode 100644 index 0000000000..999a447d7a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionErrorCode.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +public enum SubscriptionErrorCode { + + NO_ERROR(0), INTERNAL_ERROR(1, "Internal Server error!"), BAD_REQUEST(2, "Bad request"), UNAUTHORIZED(3, "Unauthorized"); + + private final int code; + private final String defaultMsg; + + private SubscriptionErrorCode(int code) { + this(code, null); + } + + private SubscriptionErrorCode(int code, String defaultMsg) { + this.code = code; + this.defaultMsg = defaultMsg; + } + + public static SubscriptionErrorCode forCode(int code) { + for (SubscriptionErrorCode errorCode : SubscriptionErrorCode.values()) { + if (errorCode.getCode() == code) { + return errorCode; + } + } + throw new IllegalArgumentException("Invalid error code: " + code); + } + + public int getCode() { + return code; + } + + public String getDefaultMsg() { + return defaultMsg; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java index 15aab86327..736a251dff 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,16 @@ package org.thingsboard.server.service.subscription; import org.springframework.context.ApplicationListener; -import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; import java.util.List; @@ -42,9 +45,12 @@ public interface SubscriptionManagerService extends ApplicationListener keys, TbCallback callback); - void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); + void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback); - void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); + void onAlarmDeleted(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback); + void onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate notificationUpdate, TbCallback callback); + + void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate notificationRequestUpdate, TbCallback callback); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionServiceStatistics.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionServiceStatistics.java index 5c1bb937dd..6ede001d8e 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionServiceStatistics.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionServiceStatistics.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java index 26e97b6f2c..3f11679849 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.thingsboard.server.service.subscription; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AbstractDataQuery; import org.thingsboard.server.common.data.query.EntityData; @@ -28,9 +29,9 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; +import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.Arrays; @@ -49,10 +50,10 @@ public abstract class TbAbstractDataSubCtx data; - public TbAbstractDataSubCtx(String serviceId, TelemetryWebSocketService wsService, + public TbAbstractDataSubCtx(String serviceId, WebSocketService wsService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, SubscriptionServiceStatistics stats, - TelemetryWebSocketSessionRef sessionRef, int cmdId) { + WebSocketSessionRef sessionRef, int cmdId) { super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); this.subToEntityIdMap = new ConcurrentHashMap<>(); } @@ -184,7 +185,7 @@ public abstract class TbAbstractDataSubCtx sendWsMsg(s, subscriptionUpdate, keysType)) + .updateProcessor((sub, subscriptionUpdate) -> sendWsMsg(sub.getSessionId(), subscriptionUpdate, keysType)) .allKeys(false) .keyStates(keyStates) .scope(scope) @@ -197,6 +198,9 @@ public abstract class TbAbstractDataSubCtx { long ts = Arrays.stream(v).map(TsValue::getTs).max(Long::compareTo).orElse(0L); log.trace("[{}][{}] Updating key: {} with ts: {}", serviceId, cmdId, k, ts); + if (!Aggregation.NONE.equals(getCurrentAggregation()) && ts < endTs) { + ts = endTs; + } keyStates.put(k, ts); }); } @@ -215,7 +219,7 @@ public abstract class TbAbstractDataSubCtx sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues)) + .updateProcessor((sub, subscriptionUpdate) -> sendWsMsg(sub.getSessionId(), subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues)) .allKeys(false) .keyStates(keyStates) .latestValues(latestValues) @@ -247,4 +251,5 @@ public abstract class TbAbstractDataSubCtx { protected final Lock wsLock = new ReentrantLock(true); protected final String serviceId; protected final SubscriptionServiceStatistics stats; - private final TelemetryWebSocketService wsService; + private final WebSocketService wsService; protected final EntityService entityService; protected final TbLocalSubscriptionService localSubscriptionService; protected final AttributesService attributesService; - protected final TelemetryWebSocketSessionRef sessionRef; + protected final WebSocketSessionRef sessionRef; protected final int cmdId; protected final Set subToDynamicValueKeySet; @Getter @@ -80,10 +79,10 @@ public abstract class TbAbstractSubCtx { protected volatile ScheduledFuture refreshTask; protected volatile boolean stopped; - public TbAbstractSubCtx(String serviceId, TelemetryWebSocketService wsService, + public TbAbstractSubCtx(String serviceId, WebSocketService wsService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, SubscriptionServiceStatistics stats, - TelemetryWebSocketSessionRef sessionRef, int cmdId) { + WebSocketSessionRef sessionRef, int cmdId) { this.serviceId = serviceId; this.wsService = wsService; this.entityService = entityService; @@ -142,7 +141,7 @@ public abstract class TbAbstractSubCtx { .subscriptionId(subIdx) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) - .updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap)) + .updateProcessor((subscription, subscriptionUpdate) -> dynamicValueSubUpdate(subscription.getSessionId(), subscriptionUpdate, dynamicValueKeySubMap)) .allKeys(false) .keyStates(keyStates) .scope(TbAttributeSubscriptionScope.SERVER_SCOPE) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java new file mode 100644 index 0000000000..bb7b9350fb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.query.AlarmCountQuery; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; + +@Slf4j +@ToString(callSuper = true) +public class TbAlarmCountSubCtx extends TbAbstractSubCtx { + + private final AlarmService alarmService; + + @Getter + @Setter + private volatile int result; + + public TbAlarmCountSubCtx(String serviceId, WebSocketService wsService, + EntityService entityService, TbLocalSubscriptionService localSubscriptionService, + AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService, + WebSocketSessionRef sessionRef, int cmdId) { + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); + this.alarmService = alarmService; + } + + @Override + public void fetchData() { + result = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query); + sendWsMsg(new AlarmCountUpdate(cmdId, result)); + } + + @Override + protected void update() { + int newCount = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query); + if (newCount != result) { + result = newCount; + sendWsMsg(new AlarmCountUpdate(cmdId, result)); + } + } + + @Override + public boolean isDynamic() { + return true; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 5804002c77..c3ff54e0a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataPageLink; @@ -38,11 +39,11 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate; -import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; +import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.Collection; @@ -62,10 +63,8 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { private final AlarmService alarmService; @Getter - @Setter private final LinkedHashMap entitiesMap; @Getter - @Setter private final HashMap alarmsMap; private final int maxEntitiesPerAlarmSubscription; @@ -81,10 +80,10 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { private int alarmInvocationAttempts; - public TbAlarmDataSubCtx(String serviceId, TelemetryWebSocketService wsService, + public TbAlarmDataSubCtx(String serviceId, WebSocketService wsService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService, - TelemetryWebSocketSessionRef sessionRef, int cmdId, + WebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerAlarmSubscription, int maxAlarmQueriesPerRefreshInterval) { super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); this.maxEntitiesPerAlarmSubscription = maxEntitiesPerAlarmSubscription; @@ -175,7 +174,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { .subscriptionId(subIdx) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityData.getEntityId()) - .updateConsumer(this::sendWsMsg) + .updateProcessor((sub, update) -> sendWsMsg(sub.getSessionId(), update)) .ts(startTs) .build(); localSubscriptionService.addSubscription(subscription); @@ -205,6 +204,11 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { } } + @Override + protected Aggregation getCurrentAggregation() { + return Aggregation.NONE; + } + private void sendWsMsg(String sessionId, AlarmSubscriptionUpdate subscriptionUpdate) { Alarm alarm = subscriptionUpdate.getAlarm(); AlarmId alarmId = alarm.getId(); @@ -219,8 +223,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { boolean matchesFilter = filter(alarm); if (onCurrentPage) { if (matchesFilter) { - AlarmData updated = new AlarmData(alarm, current.getOriginatorName(), current.getEntityId()); - updated.getLatest().putAll(current.getLatest()); + AlarmData updated = new AlarmData(subscriptionUpdate.getAlarm(), current); alarmsMap.put(alarmId, updated); sendWsMsg(new AlarmDataUpdate(cmdId, null, Collections.singletonList(updated), maxEntitiesPerAlarmSubscription, data.getTotalElements())); } else { @@ -264,8 +267,24 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { if (filter.getStatusList() != null && !filter.getStatusList().isEmpty()) { boolean matches = false; for (AlarmSearchStatus status : filter.getStatusList()) { - if (status.getStatuses().contains(alarm.getStatus())) { - matches = true; + switch (status) { + case ANY: + matches = true; + break; + case ACK: + matches = alarm.isAcknowledged(); + break; + case UNACK: + matches = !alarm.isAcknowledged(); + break; + case CLEARED: + matches = alarm.isCleared(); + break; + case ACTIVE: + matches = !alarm.isCleared(); + break; + } + if (matches) { break; } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java index fba351f755..bc23d1a9b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,10 @@ package org.thingsboard.server.service.subscription; import lombok.Builder; import lombok.Getter; -import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; -import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; -import java.util.List; import java.util.function.BiConsumer; public class TbAlarmsSubscription extends TbSubscription { @@ -33,8 +30,8 @@ public class TbAlarmsSubscription extends TbSubscription updateConsumer, long ts) { - super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ALARMS, updateConsumer); + BiConsumer, AlarmSubscriptionUpdate> updateProcessor, long ts) { + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ALARMS, updateProcessor); this.ts = ts; } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java index ebd3fe548d..3746758caa 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import lombok.Builder; import lombok.Getter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.Map; import java.util.function.BiConsumer; @@ -32,9 +32,9 @@ public class TbAttributeSubscription extends TbSubscription updateConsumer, + BiConsumer, TelemetrySubscriptionUpdate> updateProcessor, boolean allKeys, Map keyStates, TbAttributeSubscriptionScope scope) { - super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES, updateConsumer); + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES, updateProcessor); this.allKeys = allKeys; this.keyStates = keyStates; this.scope = scope; diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscriptionScope.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscriptionScope.java index ae23d26218..b3874e28fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscriptionScope.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscriptionScope.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java index 7fc87eb17c..eac94cde7d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,18 +19,18 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate; +import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; @Slf4j public class TbEntityCountSubCtx extends TbAbstractSubCtx { private volatile int result; - public TbEntityCountSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService, + public TbEntityCountSubCtx(String serviceId, WebSocketService wsService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, - SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId) { + SubscriptionServiceStatistics stats, WebSocketSessionRef sessionRef, int cmdId) { super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java index 385661f177..924d30d5c9 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -27,13 +28,13 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; -import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; +import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.Collections; @@ -58,9 +59,9 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { private final int maxEntitiesPerDataSubscription; private Map> latestTsEntityData; - public TbEntityDataSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService, + public TbEntityDataSubCtx(String serviceId, WebSocketService wsService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, - SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerDataSubscription) { + SubscriptionServiceStatistics stats, WebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerDataSubscription) { super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); this.maxEntitiesPerDataSubscription = maxEntitiesPerDataSubscription; } @@ -86,6 +87,11 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { } } + @Override + protected Aggregation getCurrentAggregation() { + return (this.curTsCmd == null || this.curTsCmd.getAgg() == null) ? Aggregation.NONE : this.curTsCmd.getAgg(); + } + private void sendLatestWsMsg(EntityId entityId, String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType) { Map latestUpdate = new HashMap<>(); subscriptionUpdate.getData().forEach((k, v) -> { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java index 784fa7d842..097fa792f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,20 +15,22 @@ */ package org.thingsboard.server.service.subscription; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; public interface TbEntityDataSubscriptionService { - void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd); + void handleCmd(WebSocketSessionRef sessionId, EntityDataCmd cmd); - void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityCountCmd cmd); + void handleCmd(WebSocketSessionRef sessionId, EntityCountCmd cmd); - void handleCmd(TelemetryWebSocketSessionRef sessionId, AlarmDataCmd cmd); + void handleCmd(WebSocketSessionRef sessionId, AlarmDataCmd cmd); + + void handleCmd(WebSocketSessionRef sessionId, AlarmCountCmd cmd); void cancelSubscription(String sessionId, UnsubscribeCmd subscriptionId); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java index e398111f1f..7685c8fbed 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,12 @@ */ package org.thingsboard.server.service.subscription; +import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; public interface TbLocalSubscriptionService { @@ -33,6 +34,8 @@ public interface TbLocalSubscriptionService { void onSubscriptionUpdate(String sessionId, AlarmSubscriptionUpdate update, TbCallback callback); + void onSubscriptionUpdate(String sessionId, int subscriptionId, NotificationsSubscriptionUpdate update, TbCallback callback); + void onApplicationEvent(PartitionChangeEvent event); void onApplicationEvent(ClusterTopologyChangeEvent event); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java index 95078d8504..dfad6a41de 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ public abstract class TbSubscription { private final TenantId tenantId; private final EntityId entityId; private final TbSubscriptionType type; - private final BiConsumer updateConsumer; + private final BiConsumer, T> updateProcessor; @Override public boolean equals(Object o) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java index 86a98ba682..cd560bbbaf 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,5 +16,5 @@ package org.thingsboard.server.service.subscription; public enum TbSubscriptionType { - TIMESERIES, ATTRIBUTES, ALARMS + TIMESERIES, ATTRIBUTES, ALARMS, NOTIFICATIONS, NOTIFICATIONS_COUNT } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index f8359a21ee..8905b1b3ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package org.thingsboard.server.service.subscription; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -49,10 +50,15 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesDeletePr import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesSubscriptionProto; import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; -import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationsCountSubscription; +import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscription; +import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.HashMap; @@ -105,6 +111,17 @@ public class TbSubscriptionUtils { .setTs(alarmSub.getTs()); msgBuilder.setAlarmSub(alarmSubProto.build()); break; + case NOTIFICATIONS: + NotificationsSubscription notificationsSub = (NotificationsSubscription) subscription; + msgBuilder.setNotificationsSub(TransportProtos.NotificationsSubscriptionProto.newBuilder() + .setSub(subscriptionProto) + .setLimit(notificationsSub.getLimit())); + break; + case NOTIFICATIONS_COUNT: + NotificationsCountSubscription notificationsCountSub = (NotificationsCountSubscription) subscription; + msgBuilder.setNotificationsCountSub(TransportProtos.NotificationsCountSubscriptionProto.newBuilder() + .setSub(subscriptionProto)); + break; } return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } @@ -166,6 +183,29 @@ public class TbSubscriptionUtils { return builder.build(); } + public static NotificationsSubscription fromProto(TransportProtos.NotificationsSubscriptionProto notificationsSub) { + TbSubscriptionProto sub = notificationsSub.getSub(); + return NotificationsSubscription.builder() + .serviceId(sub.getServiceId()) + .sessionId(sub.getSessionId()) + .subscriptionId(sub.getSubscriptionId()) + .tenantId(TenantId.fromUUID(new UUID(sub.getTenantIdMSB(), sub.getTenantIdLSB()))) + .entityId(EntityIdFactory.getByTypeAndUuid(sub.getEntityType(), new UUID(sub.getEntityIdMSB(), sub.getEntityIdLSB()))) + .limit(notificationsSub.getLimit()) + .build(); + } + + public static NotificationsCountSubscription fromProto(TransportProtos.NotificationsCountSubscriptionProto notificationsCountSub) { + TbSubscriptionProto sub = notificationsCountSub.getSub(); + return NotificationsCountSubscription.builder() + .serviceId(sub.getServiceId()) + .sessionId(sub.getSessionId()) + .subscriptionId(sub.getSubscriptionId()) + .tenantId(TenantId.fromUUID(new UUID(sub.getTenantIdMSB(), sub.getTenantIdLSB()))) + .entityId(EntityIdFactory.getByTypeAndUuid(sub.getEntityType(), new UUID(sub.getEntityIdMSB(), sub.getEntityIdLSB()))) + .build(); + } + public static TelemetrySubscriptionUpdate fromProto(TbSubscriptionUpdateProto proto) { if (proto.getErrorCode() > 0) { return new TelemetrySubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); @@ -189,8 +229,8 @@ public class TbSubscriptionUtils { if (proto.getErrorCode() > 0) { return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); } else { - Alarm alarm = JacksonUtil.fromString(proto.getAlarm(), Alarm.class); - return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), alarm); + AlarmInfo alarm = JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class); + return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), alarm, proto.getDeleted()); } } @@ -316,7 +356,7 @@ public class TbSubscriptionUtils { return entry; } - public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, Alarm alarm) { + public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, AlarmInfo alarm) { TbAlarmUpdateProto.Builder builder = TbAlarmUpdateProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); @@ -329,7 +369,7 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } - public static ToCoreMsg toAlarmDeletedProto(TenantId tenantId, EntityId entityId, Alarm alarm) { + public static ToCoreMsg toAlarmDeletedProto(TenantId tenantId, EntityId entityId, AlarmInfo alarm) { TbAlarmDeleteProto.Builder builder = TbAlarmDeleteProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); @@ -341,4 +381,50 @@ public class TbSubscriptionUtils { msgBuilder.setAlarmDelete(builder); return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } + + public static ToCoreNotificationMsg notificationsSubUpdateToProto(TbSubscription subscription, NotificationsSubscriptionUpdate update) { + TransportProtos.NotificationsSubscriptionUpdateProto.Builder updateProto = TransportProtos.NotificationsSubscriptionUpdateProto.newBuilder() + .setSessionId(subscription.getSessionId()) + .setSubscriptionId(subscription.getSubscriptionId()); + if (update.getNotificationUpdate() != null) { + updateProto.setNotificationUpdate(JacksonUtil.toString(update.getNotificationUpdate())); + } + if (update.getNotificationRequestUpdate() != null) { + updateProto.setNotificationRequestUpdate(JacksonUtil.toString(update.getNotificationRequestUpdate())); + } + return ToCoreNotificationMsg.newBuilder() + .setToLocalSubscriptionServiceMsg(TransportProtos.LocalSubscriptionServiceMsgProto.newBuilder() + .setNotificationsSubUpdate(updateProto) + .build()) + .build(); + } + + public static ToCoreMsg notificationUpdateToProto(TenantId tenantId, UserId recipientId, NotificationUpdate notificationUpdate) { + TransportProtos.NotificationUpdateProto updateProto = TransportProtos.NotificationUpdateProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setRecipientIdMSB(recipientId.getId().getMostSignificantBits()) + .setRecipientIdLSB(recipientId.getId().getLeastSignificantBits()) + .setUpdate(JacksonUtil.toString(notificationUpdate)) + .build(); + return ToCoreMsg.newBuilder() + .setToSubscriptionMgrMsg(SubscriptionMgrMsgProto.newBuilder() + .setNotificationUpdate(updateProto) + .build()) + .build(); + } + + public static ToCoreNotificationMsg notificationRequestUpdateToProto(TenantId tenantId, NotificationRequestUpdate notificationRequestUpdate) { + TransportProtos.NotificationRequestUpdateProto updateProto = TransportProtos.NotificationRequestUpdateProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setUpdate(JacksonUtil.toString(notificationRequestUpdate)) + .build(); + return ToCoreNotificationMsg.newBuilder() + .setToSubscriptionMgrMsg(SubscriptionMgrMsgProto.newBuilder() + .setNotificationRequestUpdate(updateProto) + .build()) + .build(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java index e08f25a280..400cf07e7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import lombok.Builder; import lombok.Getter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.Map; import java.util.function.BiConsumer; @@ -39,9 +39,9 @@ public class TbTimeseriesSubscription extends TbSubscription updateConsumer, + BiConsumer, TelemetrySubscriptionUpdate> updateProcessor, boolean allKeys, Map keyStates, long startTime, long endTime, boolean latestValues) { - super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES, updateConsumer); + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES, updateProcessor); this.allKeys = allKeys; this.keyStates = keyStates; this.startTime = startTime; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index f4e4e3dc14..c8f8f945f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,13 +26,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.sync.ThrowingRunnable; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; +import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.apiusage.RateLimitService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; import org.thingsboard.server.service.sync.ie.exporting.impl.BaseEntityExportService; @@ -66,13 +67,14 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS protected static final List SUPPORTED_ENTITY_TYPES = List.of( EntityType.CUSTOMER, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.RULE_CHAIN, EntityType.DASHBOARD, EntityType.DEVICE_PROFILE, EntityType.DEVICE, - EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE + EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE, + EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ); @Override public , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx ctx, I entityId) throws ThingsboardException { - if (!rateLimitService.checkEntityExportLimit(ctx.getTenantId())) { + if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) { throw new ThingsboardException("Rate limit for entities export is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); } @@ -84,7 +86,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS @Override public , I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData exportData) throws ThingsboardException { - if (!rateLimitService.checkEntityImportLimit(ctx.getTenantId())) { + if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_IMPORT, ctx.getTenantId())) { throw new ThingsboardException("Rate limit for entities import is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); } if (exportData.getEntity() == null || exportData.getEntity().getId() == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java index 8657528dac..0747e4ce14 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java index b5f5e6afd9..3358e5182f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,36 +22,21 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; -import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; -import org.thingsboard.server.dao.asset.AssetProfileService; -import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.device.DeviceProfileService; -import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.AccessControlService; import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.function.BiConsumer; @Service @TbCoreComponent @@ -60,11 +45,10 @@ import java.util.function.BiConsumer; public class DefaultExportableEntitiesService implements ExportableEntitiesService { private final Map> daos = new HashMap<>(); - private final Map> removers = new HashMap<>(); + private final EntityServiceRegistry entityServiceRegistry; private final AccessControlService accessControlService; - @Override public , I extends EntityId> E findEntityByTenantIdAndExternalId(TenantId tenantId, I externalId) { EntityType entityType = externalId.getEntityType(); @@ -152,11 +136,11 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi @Override public void removeById(TenantId tenantId, I id) { EntityType entityType = id.getEntityType(); - BiConsumer entityRemover = removers.get(entityType); - if (entityRemover == null) { + EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(entityType); + if (entityService == null) { throw new IllegalArgumentException("Unsupported entity type " + entityType); } - entityRemover.accept(tenantId, id); + entityService.deleteEntity(tenantId, id); } private > ExportableEntityDao getExportableEntityDao(EntityType entityType) { @@ -182,34 +166,4 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi }); } - @Autowired - private void setRemovers(CustomerService customerService, AssetService assetService, RuleChainService ruleChainService, - DashboardService dashboardService, DeviceProfileService deviceProfileService, - AssetProfileService assetProfileService, DeviceService deviceService, WidgetsBundleService widgetsBundleService) { - removers.put(EntityType.CUSTOMER, (tenantId, entityId) -> { - customerService.deleteCustomer(tenantId, (CustomerId) entityId); - }); - removers.put(EntityType.ASSET, (tenantId, entityId) -> { - assetService.deleteAsset(tenantId, (AssetId) entityId); - }); - removers.put(EntityType.RULE_CHAIN, (tenantId, entityId) -> { - ruleChainService.deleteRuleChainById(tenantId, (RuleChainId) entityId); - }); - removers.put(EntityType.DASHBOARD, (tenantId, entityId) -> { - dashboardService.deleteDashboard(tenantId, (DashboardId) entityId); - }); - removers.put(EntityType.DEVICE_PROFILE, (tenantId, entityId) -> { - deviceProfileService.deleteDeviceProfile(tenantId, (DeviceProfileId) entityId); - }); - removers.put(EntityType.ASSET_PROFILE, (tenantId, entityId) -> { - assetProfileService.deleteAssetProfile(tenantId, (AssetProfileId) entityId); - }); - removers.put(EntityType.DEVICE, (tenantId, entityId) -> { - deviceService.deleteDevice(tenantId, (DeviceId) entityId); - }); - removers.put(EntityType.WIDGETS_BUNDLE, (tenantId, entityId) -> { - widgetsBundleService.deleteWidgetsBundle(tenantId, (WidgetsBundleId) entityId); - }); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java index 505d1b2a3f..09db98853f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java index 0a99979e8c..c5c26c4476 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java index 5ae496558e..fbbaa51bd2 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java index a333767b33..340298db7a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java index 01626368dc..09b96d68fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,11 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; +import java.util.Collection; import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Stream; public abstract class BaseEntityExportService, D extends EntityExportData> extends DefaultEntityExportService { @@ -47,4 +51,11 @@ public abstract class BaseEntityExportService getExternalIdOrElseInternalByUuid(ctx, uuid)); } + protected Stream toExternalIds(Collection internalIds, Function entityIdCreator, + EntitiesExportCtx ctx) { + return internalIds.stream().map(entityIdCreator) + .map(entityId -> getExternalIdOrElseInternal(ctx, entityId)) + .map(EntityId::getId); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java index 9a3b195b57..b408aadd68 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.thingsboard.server.service.sync.ie.exporting.impl; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.collect.Lists; import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; @@ -27,12 +25,9 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; -import org.thingsboard.common.util.RegexUtils; -import java.util.ArrayList; import java.util.Collections; import java.util.Set; -import java.util.UUID; @Service @TbCoreComponent diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index a00a7c09aa..a9e0cf1166 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java index bc5136bee6..ddb6d98135 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java index 46423c66ef..82ce17893a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java index 3fff890d3a..715c910efe 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java new file mode 100644 index 0000000000..0906fb83ef --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@TbCoreComponent +public class NotificationRuleExportService, D extends EntityExportData> extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, NotificationRule notificationRule, EntityExportData exportData) { + notificationRule.setTemplateId(getExternalIdOrElseInternal(ctx, notificationRule.getTemplateId())); + + NotificationRuleTriggerConfig ruleTriggerConfig = notificationRule.getTriggerConfig(); + switch (ruleTriggerConfig.getTriggerType()) { + case DEVICE_ACTIVITY: { + DeviceActivityNotificationRuleTriggerConfig triggerConfig = (DeviceActivityNotificationRuleTriggerConfig) ruleTriggerConfig; + Set devices = triggerConfig.getDevices(); + if (devices != null) { + triggerConfig.setDevices(toExternalIds(devices, DeviceId::new, ctx).collect(Collectors.toSet())); + } + + Set deviceProfiles = triggerConfig.getDeviceProfiles(); + if (deviceProfiles != null) { + triggerConfig.setDeviceProfiles(toExternalIds(deviceProfiles, DeviceProfileId::new, ctx).collect(Collectors.toSet())); + } + break; + } + case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: + RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig; + Set ruleChains = triggerConfig.getRuleChains(); + if (ruleChains != null) { + triggerConfig.setRuleChains(toExternalIds(ruleChains, RuleChainId::new, ctx).collect(Collectors.toSet())); + } + break; + } + + NotificationRuleRecipientsConfig ruleRecipientsConfig = notificationRule.getRecipientsConfig(); + switch (ruleTriggerConfig.getTriggerType()) { + case ALARM: { + EscalatedNotificationRuleRecipientsConfig recipientsConfig = (EscalatedNotificationRuleRecipientsConfig) ruleRecipientsConfig; + Map> escalationTable = new LinkedHashMap<>(recipientsConfig.getEscalationTable()); + escalationTable.replaceAll((delay, targets) -> { + return toExternalIds(targets, NotificationTargetId::new, ctx).collect(Collectors.toList()); + }); + recipientsConfig.setEscalationTable(escalationTable); + break; + } + default: { + DefaultNotificationRuleRecipientsConfig recipientsConfig = (DefaultNotificationRuleRecipientsConfig) ruleRecipientsConfig; + List targets = recipientsConfig.getTargets(); + targets = toExternalIds(targets, NotificationTargetId::new, ctx).collect(Collectors.toList()); + recipientsConfig.setTargets(targets); + break; + } + } + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.NOTIFICATION_RULE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTargetExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTargetExportService.java new file mode 100644 index 0000000000..f0a5da3efc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTargetExportService.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.Set; + +@Service +@TbCoreComponent +public class NotificationTargetExportService extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, NotificationTarget notificationTarget, EntityExportData exportData) { + if (notificationTarget.getConfiguration().getType() == NotificationTargetType.PLATFORM_USERS) { + UsersFilter usersFilter = ((PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration()).getUsersFilter(); + switch (usersFilter.getType()) { + case CUSTOMER_USERS: + CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) usersFilter; + customerUsersFilter.setCustomerId(getExternalIdOrElseInternal(ctx, new CustomerId(customerUsersFilter.getCustomerId())).getId()); + break; + } + } + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.NOTIFICATION_TARGET); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTemplateExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTemplateExportService.java new file mode 100644 index 0000000000..d28f352bae --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTemplateExportService.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.Set; + +@Service +@TbCoreComponent +public class NotificationTemplateExportService extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, NotificationTemplate notificationTemplate, EntityExportData exportData) { + + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.NOTIFICATION_TEMPLATE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java index 802a03ab94..83a0be5be2 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,8 @@ */ package org.thingsboard.server.service.sync.ie.exporting.impl; -import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.rule.RuleChain; @@ -27,12 +25,10 @@ import org.thingsboard.server.common.data.sync.ie.RuleChainExportData; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; -import org.thingsboard.common.util.RegexUtils; import java.util.Collections; import java.util.Optional; import java.util.Set; -import java.util.UUID; @Service @TbCoreComponent diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java index d3dd23910f..7024f36623 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java index 9437f21e60..a3b8cd98a9 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index 37d3cd2e71..45805c5939 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ public abstract class AbstractBulkImportService importedEntityInfo = saveEntity(entityData.getFields(), user); E entity = importedEntityInfo.getEntity(); - saveKvs(user, entity, entityData.getKvs()); - + if (request.getMapping().getUpdate() || !importedEntityInfo.isUpdated()) { + saveKvs(user, entity, entityData.getKvs()); + } return importedEntityInfo; }, importedEntityInfo -> { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java index d48e9a3d23..bc9446b39d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java index fa3558520e..9ba81a3f8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java index c6b33a5bd2..7f0af64ed7 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 4fdd6cf6ab..610f1ee0b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; -import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; @@ -87,7 +86,6 @@ public abstract class BaseEntityImportService importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException { EntityImportResult importResult = new EntityImportResult<>(); @@ -336,27 +334,37 @@ public abstract class BaseEntityImportService externalIdOpt = buildEntityId(entityType, externalUuid); - if (!externalIdOpt.isPresent()) { + Optional externalId = buildEntityId(entityType, externalUuid); + if (externalId.isEmpty()) { continue; } - EntityId internalId = ctx.getInternalId(externalIdOpt.get()); + EntityId internalId = ctx.getInternalId(externalId.get()); if (internalId != null) { return Optional.of(internalId); } } if (fetchAllUUIDs) { - for (EntityType entityType : hints) { - Optional internalId = lookupInDb(externalUuid, entityType); - if (internalId.isPresent()) return internalId; - } + Set processLast = Set.of(EntityType.TENANT); + List entityTypes = new ArrayList<>(hints); for (EntityType entityType : EntityType.values()) { - if (hints.contains(entityType)) { + if (!hints.contains(entityType) && !processLast.contains(entityType)) { + entityTypes.add(entityType); + } + } + entityTypes.addAll(processLast); + + for (EntityType entityType : entityTypes) { + Optional externalId = buildEntityId(entityType, externalUuid); + if (externalId.isEmpty() || ctx.isNotFound(externalId.get())) { continue; } - Optional internalId = lookupInDb(externalUuid, entityType); - if (internalId.isPresent()) return internalId; + EntityId internalId = getInternalId(externalId.get(), false); + if (internalId != null) { + return Optional.of(internalId); + } else { + ctx.registerNotFound(externalId.get()); + } } } @@ -364,20 +372,6 @@ public abstract class BaseEntityImportService lookupInDb(UUID externalUuid, EntityType entityType) { - Optional externalIdOpt = buildEntityId(entityType, externalUuid); - if (externalIdOpt.isEmpty() || ctx.isNotFound(externalIdOpt.get())) { - return Optional.empty(); - } - EntityId internalId = getInternalId(externalIdOpt.get(), false); - if (internalId != null) { - return Optional.of(internalId); - } else { - ctx.registerNotFound(externalIdOpt.get()); - } - return Optional.empty(); - } - private Optional buildEntityId(EntityType entityType, UUID externalUuid) { try { return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, externalUuid)); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java index d12ed6ecff..d6795e0834 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java index 5aa97a6f4d..250026c71c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,6 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ShortCustomerInfo; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; @@ -68,10 +65,10 @@ public class DashboardImportService extends BaseEntityImportService exportData, IdProvider idProvider) { for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) { - replaceIdsRecursively(ctx, idProvider, entityAlias, Collections.emptySet(), HINTS); + replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), HINTS); } for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) { - replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.singleton("id"), HINTS); + replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), HINTS); } return dashboard; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index 2cf2c57c11..1579909463 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java index 72be8e6bf7..3cc0310079 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java index 6856323ecc..9ce682a3fc 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java index 1e869429d8..994cea1bdc 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java index a0a961bfef..1de6f06c49 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java new file mode 100644 index 0000000000..a7d7670c8c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java @@ -0,0 +1,150 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +public class NotificationRuleImportService extends BaseEntityImportService> { + + private final NotificationRuleService notificationRuleService; + + @Override + protected void setOwner(TenantId tenantId, NotificationRule notificationRule, IdProvider idProvider) { + notificationRule.setTenantId(tenantId); + } + + @Override + protected NotificationRule prepare(EntitiesImportCtx ctx, NotificationRule notificationRule, NotificationRule oldNotificationRule, EntityExportData exportData, IdProvider idProvider) { + notificationRule.setTemplateId(idProvider.getInternalId(notificationRule.getTemplateId())); + + NotificationRuleTriggerConfig ruleTriggerConfig = notificationRule.getTriggerConfig(); + NotificationRuleTriggerType triggerType = ruleTriggerConfig.getTriggerType(); + switch (triggerType) { + case DEVICE_ACTIVITY: { + DeviceActivityNotificationRuleTriggerConfig triggerConfig = (DeviceActivityNotificationRuleTriggerConfig) ruleTriggerConfig; + Set devices = triggerConfig.getDevices(); + if (devices != null) { + triggerConfig.setDevices(devices.stream().map(DeviceId::new) + .map(idProvider::getInternalId).map(UUIDBased::getId) + .collect(Collectors.toSet())); + } + + Set deviceProfiles = triggerConfig.getDeviceProfiles(); + if (deviceProfiles != null) { + triggerConfig.setDeviceProfiles(deviceProfiles.stream().map(DeviceProfileId::new) + .map(idProvider::getInternalId).map(UUIDBased::getId) + .collect(Collectors.toSet())); + } + break; + } + case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: + RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig; + Set ruleChains = triggerConfig.getRuleChains(); + if (ruleChains != null) { + triggerConfig.setRuleChains(ruleChains.stream().map(RuleChainId::new) + .map(idProvider::getInternalId).map(UUIDBased::getId) + .collect(Collectors.toSet())); + } + break; + } + if (!triggerType.isTenantLevel()) { + throw new IllegalArgumentException("Trigger type " + triggerType + " is not available for tenants"); + } + + NotificationRuleRecipientsConfig ruleRecipientsConfig = notificationRule.getRecipientsConfig(); + switch (triggerType) { + case ALARM: { + EscalatedNotificationRuleRecipientsConfig recipientsConfig = (EscalatedNotificationRuleRecipientsConfig) ruleRecipientsConfig; + Map> escalationTable = new LinkedHashMap<>(recipientsConfig.getEscalationTable()); + escalationTable.replaceAll((delay, targets) -> targets.stream() + .map(NotificationTargetId::new).map(idProvider::getInternalId) + .map(UUIDBased::getId).collect(Collectors.toList())); + recipientsConfig.setEscalationTable(escalationTable); + break; + } + default: { + DefaultNotificationRuleRecipientsConfig recipientsConfig = (DefaultNotificationRuleRecipientsConfig) ruleRecipientsConfig; + List targets = recipientsConfig.getTargets().stream() + .map(NotificationTargetId::new).map(idProvider::getInternalId) + .map(UUIDBased::getId).collect(Collectors.toList()); + recipientsConfig.setTargets(targets); + break; + } + } + return notificationRule; + } + + @Override + protected NotificationRule saveOrUpdate(EntitiesImportCtx ctx, NotificationRule notificationRule, EntityExportData exportData, IdProvider idProvider) { + ConstraintValidator.validateFields(notificationRule); + return notificationRuleService.saveNotificationRule(ctx.getTenantId(), notificationRule); + } + + @Override + protected void onEntitySaved(User user, NotificationRule savedEntity, NotificationRule oldEntity) throws ThingsboardException { + entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null, + oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null); + clusterService.broadcastEntityStateChangeEvent(user.getTenantId(), savedEntity.getId(), + oldEntity == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + } + + @Override + protected NotificationRule deepCopy(NotificationRule notificationRule) { + return new NotificationRule(notificationRule); + } + + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_RULE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java new file mode 100644 index 0000000000..a950a1592e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +import java.util.stream.Collectors; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +public class NotificationTargetImportService extends BaseEntityImportService> { + + private final NotificationTargetService notificationTargetService; + + @Override + protected void setOwner(TenantId tenantId, NotificationTarget notificationTarget, IdProvider idProvider) { + notificationTarget.setTenantId(tenantId); + } + + @Override + protected NotificationTarget prepare(EntitiesImportCtx ctx, NotificationTarget notificationTarget, NotificationTarget oldNotificationTarget, EntityExportData exportData, IdProvider idProvider) { + if (notificationTarget.getConfiguration().getType() == NotificationTargetType.PLATFORM_USERS) { + UsersFilter usersFilter = ((PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration()).getUsersFilter(); + switch (usersFilter.getType()) { + case CUSTOMER_USERS: + CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) usersFilter; + customerUsersFilter.setCustomerId(idProvider.getInternalId(new CustomerId(customerUsersFilter.getCustomerId())).getId()); + break; + case USER_LIST: + UserListFilter userListFilter = (UserListFilter) usersFilter; + userListFilter.setUsersIds(userListFilter.getUsersIds().stream() + .map(UserId::new).map(idProvider::getInternalId) + .map(UUIDBased::getId).collect(Collectors.toList()) + ); + break; + case TENANT_ADMINISTRATORS: + if (CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantsIds()) || + CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantProfilesIds())) { + throw new IllegalArgumentException("Permission denied"); + } + break; + case SYSTEM_ADMINISTRATORS: + throw new AccessDeniedException("Permission denied"); + } + } + return notificationTarget; + } + + @Override + protected NotificationTarget saveOrUpdate(EntitiesImportCtx ctx, NotificationTarget notificationTarget, EntityExportData exportData, IdProvider idProvider) { + ConstraintValidator.validateFields(notificationTarget); + return notificationTargetService.saveNotificationTarget(ctx.getTenantId(), notificationTarget); + } + + @Override + protected void onEntitySaved(User user, NotificationTarget savedEntity, NotificationTarget oldEntity) throws ThingsboardException { + entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null, + oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null); + } + + @Override + protected NotificationTarget deepCopy(NotificationTarget notificationTarget) { + return new NotificationTarget(notificationTarget); + } + + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_TARGET; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java new file mode 100644 index 0000000000..b98c17a412 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +public class NotificationTemplateImportService extends BaseEntityImportService> { + + private final NotificationTemplateService notificationTemplateService; + + @Override + protected void setOwner(TenantId tenantId, NotificationTemplate notificationTemplate, IdProvider idProvider) { + notificationTemplate.setTenantId(tenantId); + } + + @Override + protected NotificationTemplate prepare(EntitiesImportCtx ctx, NotificationTemplate notificationTemplate, NotificationTemplate oldEntity, EntityExportData exportData, IdProvider idProvider) { + return notificationTemplate; + } + + @Override + protected NotificationTemplate saveOrUpdate(EntitiesImportCtx ctx, NotificationTemplate notificationTemplate, EntityExportData exportData, IdProvider idProvider) { + ConstraintValidator.validateFields(notificationTemplate); + return notificationTemplateService.saveNotificationTemplate(ctx.getTenantId(), notificationTemplate); + } + + @Override + protected void onEntitySaved(User user, NotificationTemplate savedEntity, NotificationTemplate oldEntity) throws ThingsboardException { + entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null, + oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null); + } + + @Override + protected NotificationTemplate deepCopy(NotificationTemplate notificationTemplate) { + return new NotificationTemplate(notificationTemplate); + } + + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_TEMPLATE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java index f01eb15337..eb54fce21e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java index adbbcf6a09..8dc748c43f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 8cf17fd5b0..a63600619a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; @@ -32,7 +33,6 @@ import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.sync.ThrowingRunnable; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; @@ -69,6 +68,7 @@ import org.thingsboard.server.common.data.sync.vc.request.load.EntityTypeVersion import org.thingsboard.server.common.data.sync.vc.request.load.SingleEntityVersionLoadRequest; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadConfig; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest; +import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; @@ -101,6 +101,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import static com.google.common.util.concurrent.Futures.transform; +import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName; @Service @TbCoreComponent @@ -138,6 +139,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @SuppressWarnings("UnstableApiUsage") @Override public ListenableFuture saveEntitiesVersion(User user, VersionCreateRequest request) throws Exception { + checkBranchName(request.getBranch()); var pendingCommit = gitServiceQueue.prepareCommit(user, request); DonAsynchron.withCallback(pendingCommit, commit -> { cachePut(commit.getTxId(), new VersionCreationResult()); @@ -259,7 +261,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont return gitServiceQueue.listEntitiesAtVersion(tenantId, versionId); } - @SuppressWarnings({"UnstableApiUsage", "rawtypes"}) + @SuppressWarnings({"rawtypes"}) @Override public UUID loadEntitiesVersion(User user, VersionLoadRequest request) throws Exception { EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), user, request.getVersionId()); @@ -402,7 +404,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>()) .add(importResult.getSavedEntity().getId()); } - log.debug("Imported {} pack for tenant {}", entityType, ctx.getTenantId()); + log.debug("Imported {} pack ({}) for tenant {}", entityType, entityDataList.size(), ctx.getTenantId()); offset += limit; } while (entityDataList.size() == limit); } @@ -504,6 +506,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override public ListenableFuture saveVersionControlSettings(TenantId tenantId, RepositorySettings versionControlSettings) { + checkBranchName(versionControlSettings.getDefaultBranch()); var restoredSettings = this.repositorySettingsService.restore(tenantId, versionControlSettings); try { var future = gitServiceQueue.initRepository(tenantId, restoredSettings); @@ -525,6 +528,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override public ListenableFuture checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws ThingsboardException { + checkBranchName(settings.getDefaultBranch()); settings = this.repositorySettingsService.restore(tenantId, settings); try { return gitServiceQueue.testRepository(tenantId, settings); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java index 413980d119..feff708983 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.CollectionsUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; @@ -46,6 +45,7 @@ import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.CommitRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.EntitiesContentRequestMsg; @@ -64,7 +64,6 @@ import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.ClearRepositoryGitRequest; import org.thingsboard.server.service.sync.vc.data.CommitGitRequest; -import org.thingsboard.server.service.sync.vc.data.ContentsDiffGitRequest; import org.thingsboard.server.service.sync.vc.data.EntitiesContentGitRequest; import org.thingsboard.server.service.sync.vc.data.EntityContentGitRequest; import org.thingsboard.server.service.sync.vc.data.ListBranchesGitRequest; @@ -78,11 +77,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java index 8a2410e932..d43a2c45e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/GitVersionControlQueueService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/GitVersionControlQueueService.java index feba7db76d..78311ac23f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/GitVersionControlQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/GitVersionControlQueueService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/LoadEntityException.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/LoadEntityException.java index 13d8280046..055eb707cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/LoadEntityException.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/LoadEntityException.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/TbAbstractVersionControlSettingsService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/TbAbstractVersionControlSettingsService.java index 3a674eece0..7d0aaa5fc9 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/TbAbstractVersionControlSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/TbAbstractVersionControlSettingsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCacheEntry.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCacheEntry.java index dcf3ef3a70..4524f8efb8 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCacheEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCacheEntry.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCaffeineCache.java index e58ceba8c5..a870c4109b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCaffeineCache.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCaffeineCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,6 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.gen.transport.TransportProtos; import java.util.UUID; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskRedisCache.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskRedisCache.java index 6215744f7a..5fccbb3cbb 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskRedisCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsCaffeineCache.java index 47b9e5f5d2..9d4e63201f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsCaffeineCache.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsCaffeineCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsRedisCache.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsRedisCache.java index e3c918818c..b3c47cf2a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsRedisCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/DefaultTbAutoCommitSettingsService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/DefaultTbAutoCommitSettingsService.java index b6e8d45ec2..7c13b8f46e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/DefaultTbAutoCommitSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/DefaultTbAutoCommitSettingsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/TbAutoCommitSettingsService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/TbAutoCommitSettingsService.java index 51978481e1..655acbb551 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/TbAutoCommitSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/TbAutoCommitSettingsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.thingsboard.server.service.sync.vc.autocommit; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings; -import org.thingsboard.server.common.data.sync.vc.RepositorySettings; public interface TbAutoCommitSettingsService { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ClearRepositoryGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ClearRepositoryGitRequest.java index 09245eb895..3942151fc8 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ClearRepositoryGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ClearRepositoryGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/CommitGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/CommitGitRequest.java index a989439d10..0d1b80d69c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/CommitGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/CommitGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ComplexEntitiesExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ComplexEntitiesExportCtx.java index e8577b049d..4b7cc3430b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ComplexEntitiesExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ComplexEntitiesExportCtx.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ContentsDiffGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ContentsDiffGitRequest.java index 7c3fb7a600..9a744abcca 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ContentsDiffGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ContentsDiffGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesContentGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesContentGitRequest.java index ad653edd0c..e1d68c2c2b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesContentGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesContentGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java index 4c89c91271..9046d66b30 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java index fddc91883e..20c29aea10 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,10 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.sync.ThrowingRunnable; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.sync.ie.EntityImportSettings; import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult; +import org.thingsboard.server.common.data.util.ThrowingRunnable; import java.util.ArrayList; import java.util.Collection; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityContentGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityContentGitRequest.java index 6a1d60a065..3353240366 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityContentGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityContentGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java index 80d90590a1..ecaddcdea4 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListBranchesGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListBranchesGitRequest.java index 4d89efa14d..73f13764f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListBranchesGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListBranchesGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListEntitiesGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListEntitiesGitRequest.java index 3fc531735e..d29fb295fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListEntitiesGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListEntitiesGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListVersionsGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListVersionsGitRequest.java index d40a589646..c81dda1906 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListVersionsGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListVersionsGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/PendingGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/PendingGitRequest.java index b34806a6ca..6bcb74b62b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/PendingGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/PendingGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ReimportTask.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ReimportTask.java index 97432adbb8..c4f4ae5bcd 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ReimportTask.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/ReimportTask.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/SimpleEntitiesExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/SimpleEntitiesExportCtx.java index 4cf5d93765..c163fb7fa3 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/SimpleEntitiesExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/SimpleEntitiesExportCtx.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/VersionsDiffGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/VersionsDiffGitRequest.java index e73706609f..7e48f2a3e5 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/VersionsDiffGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/VersionsDiffGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/VoidGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/VoidGitRequest.java index c48bfa7a03..e411f3cf39 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/VoidGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/VoidGitRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/DefaultTbRepositorySettingsService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/DefaultTbRepositorySettingsService.java index 555490bd06..cd8247d03e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/DefaultTbRepositorySettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/DefaultTbRepositorySettingsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,8 @@ package org.thingsboard.server.service.sync.vc.repository; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod; +import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.TbAbstractVersionControlSettingsService; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsCaffeineCache.java index 60b7f50e12..17c8819669 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsCaffeineCache.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsCaffeineCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsRedisCache.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsRedisCache.java index a49ace3ec2..27fba3459c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsRedisCache.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/TbRepositorySettingsService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/TbRepositorySettingsService.java index 946d06c87c..6498669405 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/TbRepositorySettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/repository/TbRepositorySettingsService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java new file mode 100644 index 0000000000..80d4cede03 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java @@ -0,0 +1,235 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.system; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.ProtocolStringList; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.SmsService; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.FeaturesInfo; +import org.thingsboard.server.common.data.SystemInfo; +import org.thingsboard.server.common.data.SystemInfoData; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.stats.TbApiUsageStateClient; +import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; +import org.thingsboard.server.queue.discovery.DiscoveryService; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbApplicationEventListener; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import javax.annotation.Nullable; +import javax.annotation.PreDestroy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.common.util.SystemUtil.getCpuCount; +import static org.thingsboard.common.util.SystemUtil.getCpuUsage; +import static org.thingsboard.common.util.SystemUtil.getDiscSpaceUsage; +import static org.thingsboard.common.util.SystemUtil.getMemoryUsage; +import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace; +import static org.thingsboard.common.util.SystemUtil.getTotalMemory; + +@TbCoreComponent +@Service +@RequiredArgsConstructor +@Slf4j +public class DefaultSystemInfoService extends TbApplicationEventListener implements SystemInfoService { + + public static final FutureCallback CALLBACK = new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Integer result) { + } + + @Override + public void onFailure(Throwable t) { + log.warn("Failed to persist system info", t); + } + }; + + private final TbServiceInfoProvider serviceInfoProvider; + private final PartitionService partitionService; + private final DiscoveryService discoveryService; + private final TelemetrySubscriptionService telemetryService; + private final TbApiUsageStateClient apiUsageStateClient; + private final AdminSettingsService adminSettingsService; + private final OAuth2Service oAuth2Service; + private final MailService mailService; + private final SmsService smsService; + private volatile ScheduledExecutorService scheduler; + + @Override + protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) { + if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { + boolean myPartition = partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition(); + synchronized (this) { + if (myPartition) { + if (scheduler == null) { + scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-system-info-scheduler")); + scheduler.scheduleAtFixedRate(this::saveCurrentSystemInfo, 0, 1, TimeUnit.MINUTES); + } + } else { + destroy(); + } + } + } + } + + @Override + public SystemInfo getSystemInfo() { + SystemInfo systemInfo = new SystemInfo(); + + if (discoveryService.isMonolith()) { + systemInfo.setMonolith(true); + systemInfo.setSystemData(Collections.singletonList(createSystemInfoData(serviceInfoProvider.generateNewServiceInfoWithCurrentSystemInfo()))); + } else { + systemInfo.setSystemData(getSystemData(serviceInfoProvider.getServiceInfo())); + } + + return systemInfo; + } + + protected void saveCurrentSystemInfo() { + if (discoveryService.isMonolith()) { + saveCurrentMonolithSystemInfo(); + } else { + saveCurrentClusterSystemInfo(); + } + } + + @Override + public FeaturesInfo getFeaturesInfo() { + FeaturesInfo featuresInfo = new FeaturesInfo(); + featuresInfo.setEmailEnabled(isEmailEnabled()); + featuresInfo.setSmsEnabled(smsService.isConfigured(TenantId.SYS_TENANT_ID)); + featuresInfo.setOauthEnabled(oAuth2Service.findOAuth2Info().isEnabled()); + featuresInfo.setTwoFaEnabled(isTwoFaEnabled()); + featuresInfo.setNotificationEnabled(isSlackEnabled()); + return featuresInfo; + } + + private boolean isEmailEnabled() { + try { + mailService.testConnection(TenantId.SYS_TENANT_ID); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean isTwoFaEnabled() { + AdminSettings twoFaSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "twoFaSettings"); + if (twoFaSettings != null) { + var providers = twoFaSettings.getJsonValue().get("providers"); + if (providers != null) { + return providers.size() > 0; + } + } + return false; + } + + private boolean isSlackEnabled() { + AdminSettings notifications = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "notifications"); + if (notifications != null) { + return notifications.getJsonValue().get("deliveryMethodsConfigs").has("SLACK"); + } + return false; + } + + private void saveCurrentClusterSystemInfo() { + long ts = System.currentTimeMillis(); + List clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo()); + BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData))); + doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv)); + } + + private void saveCurrentMonolithSystemInfo() { + long ts = System.currentTimeMillis(); + List tsList = new ArrayList<>(); + tsList.add(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", false))); + getCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", (long) v)))); + getMemoryUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", (long) v)))); + getDiscSpaceUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", (long) v)))); + + getCpuCount().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuCount", (long) v)))); + getTotalMemory().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("totalMemory", v)))); + getTotalDiscSpace().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("totalDiscSpace", v)))); + + doSave(tsList); + } + + private void doSave(List telemetry) { + ApiUsageState apiUsageState = apiUsageStateClient.getApiUsageState(TenantId.SYS_TENANT_ID); + telemetryService.saveAndNotifyInternal(TenantId.SYS_TENANT_ID, apiUsageState.getId(), telemetry, CALLBACK); + } + + private List getSystemData(ServiceInfo serviceInfo) { + List clusterSystemData = new ArrayList<>(); + clusterSystemData.add(createSystemInfoData(serviceInfo)); + this.discoveryService.getOtherServers() + .stream() + .map(this::createSystemInfoData) + .forEach(clusterSystemData::add); + return clusterSystemData; + } + + private SystemInfoData createSystemInfoData(ServiceInfo serviceInfo) { + ProtocolStringList serviceTypes = serviceInfo.getServiceTypesList(); + SystemInfoData infoData = new SystemInfoData(); + infoData.setServiceId(serviceInfo.getServiceId()); + infoData.setServiceType(serviceTypes.size() > 1 ? "MONOLITH" : serviceTypes.get(0)); + + infoData.setCpuUsage(serviceInfo.getSystemInfo().getCpuUsage()); + infoData.setMemoryUsage(serviceInfo.getSystemInfo().getMemoryUsage()); + infoData.setDiscUsage(serviceInfo.getSystemInfo().getDiskUsage()); + + infoData.setCpuCount(serviceInfo.getSystemInfo().getCpuCount()); + infoData.setTotalMemory(serviceInfo.getSystemInfo().getTotalMemory()); + infoData.setTotalDiscSpace(serviceInfo.getSystemInfo().getTotalDiscSpace()); + + return infoData; + } + + @PreDestroy + private void destroy() { + if (scheduler != null) { + scheduler.shutdownNow(); + scheduler = null; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java new file mode 100644 index 0000000000..1ca0fb8fe0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.system; + +import org.thingsboard.server.common.data.FeaturesInfo; +import org.thingsboard.server.common.data.SystemInfo; + +public interface SystemInfoService { + SystemInfo getSystemInfo(); + + FeaturesInfo getFeaturesInfo(); +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java index 7645465b19..145f43fa3f 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,15 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbApplicationEventListener; -import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.service.subscription.SubscriptionManagerService; import javax.annotation.Nullable; @@ -38,33 +41,26 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; +import java.util.function.Supplier; /** * Created by ashvayka on 27.03.18. */ @Slf4j -public abstract class AbstractSubscriptionService extends TbApplicationEventListener{ +public abstract class AbstractSubscriptionService extends TbApplicationEventListener { protected final Set currentPartitions = ConcurrentHashMap.newKeySet(); - protected final TbClusterService clusterService; - protected final PartitionService partitionService; + @Autowired + protected TbClusterService clusterService; + @Autowired + protected PartitionService partitionService; + @Autowired protected Optional subscriptionManagerService; protected ExecutorService wsCallBackExecutor; - public AbstractSubscriptionService(TbClusterService clusterService, - PartitionService partitionService) { - this.clusterService = clusterService; - this.partitionService = partitionService; - } - - @Autowired(required = false) - public void setSubscriptionManagerService(Optional subscriptionManagerService) { - this.subscriptionManagerService = subscriptionManagerService; - } - - abstract String getExecutorPrefix(); + protected abstract String getExecutorPrefix(); @PostConstruct public void initExecutor() { @@ -86,6 +82,22 @@ public abstract class AbstractSubscriptionService extends TbApplicationEventList } } + protected void forwardToSubscriptionManagerService(TenantId tenantId, EntityId entityId, + Consumer toSubscriptionManagerService, + Supplier toCore) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); + if (currentPartitions.contains(tpi)) { + if (subscriptionManagerService.isPresent()) { + toSubscriptionManagerService.accept(subscriptionManagerService.get()); + } else { + log.warn("Possible misconfiguration because subscriptionManagerService is null!"); + } + } else { + TransportProtos.ToCoreMsg toCoreMsg = toCore.get(); + clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); + } + } + protected void addWsCallback(ListenableFuture saveFuture, Consumer callback) { Futures.addCallback(saveFuture, new FutureCallback() { @Override @@ -98,4 +110,5 @@ public abstract class AbstractSubscriptionService extends TbApplicationEventList } }, wsCallBackExecutor); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AlarmSubscriptionService.java index a80ec0f3f4..ff1228a27b 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/AlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AlarmSubscriptionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AttributeData.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AttributeData.java index 5eb258c2a6..b7c7b281ca 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/AttributeData.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AttributeData.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 956dde8024..fec9d10a18 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,70 +19,98 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; -import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.service.subscription.SubscriptionManagerService; +import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.Collection; -import java.util.Optional; /** * Created by ashvayka on 27.03.18. */ @Service @Slf4j +@RequiredArgsConstructor public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService { private final AlarmService alarmService; + private final TbAlarmCommentService alarmCommentService; private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageStateService apiUsageStateService; + private final NotificationRuleProcessor notificationRuleProcessor; - public DefaultAlarmSubscriptionService(TbClusterService clusterService, - PartitionService partitionService, - AlarmService alarmService, - TbApiUsageReportClient apiUsageClient, - TbApiUsageStateService apiUsageStateService) { - super(clusterService, partitionService); - this.alarmService = alarmService; - this.apiUsageClient = apiUsageClient; - this.apiUsageStateService = apiUsageStateService; + @Override + protected String getExecutorPrefix() { + return "alarm"; } - @Autowired(required = false) - public void setSubscriptionManagerService(Optional subscriptionManagerService) { - this.subscriptionManagerService = subscriptionManagerService; + @Override + public AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request) { + boolean creationEnabled = apiUsageStateService.getApiUsageState(request.getTenantId()).isAlarmCreationEnabled(); + var result = alarmService.createAlarm(request, creationEnabled); + if (result.isCreated()) { + apiUsageClient.report(request.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT); + } + return withWsCallback(request, result); } @Override - String getExecutorPrefix() { - return "alarm"; + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + return withWsCallback(alarmService.updateAlarm(request)); + } + + @Override + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { + return withWsCallback(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs)); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { + return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details)); + } + + @Override + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs) { + return withWsCallback(alarmService.assignAlarm(tenantId, alarmId, assigneeId, assignTs)); + } + + @Override + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs) { + return withWsCallback(alarmService.unassignAlarm(tenantId, alarmId, assignTs)); } @Override @@ -90,6 +118,19 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled()); if (result.isSuccessful()) { onAlarmUpdated(result); + AlarmSeverity oldSeverity = result.getOldSeverity(); + if (oldSeverity != null && !oldSeverity.equals(result.getAlarm().getSeverity())) { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm severity was updated from %s to %s", oldSeverity, result.getAlarm().getSeverity()))) + .build(); + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment, null); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + } } if (result.isCreated()) { apiUsageClient.report(alarm.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT); @@ -99,29 +140,29 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService @Override public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) { - AlarmOperationResult result = alarmService.deleteAlarm(tenantId, alarmId); + AlarmApiCallResult result = alarmService.delAlarm(tenantId, alarmId); onAlarmDeleted(result); return result.isSuccessful(); } @Override public ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { - ListenableFuture result = alarmService.ackAlarm(tenantId, alarmId, ackTs); + ListenableFuture result = Futures.immediateFuture(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs)); Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); - return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + return Futures.transform(result, AlarmApiCallResult::isSuccessful, wsCallBackExecutor); } @Override public ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { - ListenableFuture result = clearAlarmForResult(tenantId, alarmId, details, clearTs); - return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details); + return Futures.transform(Futures.immediateFuture(result), AlarmApiCallResult::isSuccessful, wsCallBackExecutor); } @Override public ListenableFuture clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { - ListenableFuture result = alarmService.clearAlarm(tenantId, alarmId, details, clearTs); - Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); - return result; + AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details); + Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor); + return Futures.immediateFuture(new AlarmOperationResult(result)); } @Override @@ -135,8 +176,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } @Override - public ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { - return alarmService.findAlarmInfoByIdAsync(tenantId, alarmId); + public AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId) { + return alarmService.findAlarmInfoById(tenantId, alarmId); } @Override @@ -150,8 +191,18 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } @Override - public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus) { - return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus); + public ListenableFuture> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) { + return alarmService.findAlarmsV2(tenantId, query); + } + + @Override + public ListenableFuture> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) { + return alarmService.findCustomerAlarmsV2(tenantId, customerId, query); + } + + @Override + public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { + return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId); } @Override @@ -159,54 +210,70 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return alarmService.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); } + @Override + public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type); + } + @Override public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { return alarmService.findLatestByOriginatorAndType(tenantId, originator, type); } + @Deprecated private void onAlarmUpdated(AlarmOperationResult result) { wsCallBackExecutor.submit(() -> { - Alarm alarm = result.getAlarm(); - TenantId tenantId = result.getAlarm().getTenantId(); + AlarmInfo alarm = new AlarmInfo(result.getAlarm()); + TenantId tenantId = alarm.getTenantId(); for (EntityId entityId : result.getPropagatedEntitiesList()) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); - if (currentPartitions.contains(tpi)) { - if (subscriptionManagerService.isPresent()) { - subscriptionManagerService.get().onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY); - } else { - log.warn("Possible misconfiguration because subscriptionManagerService is null!"); - } - } else { - TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm); - clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); - } + forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { + subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY); + }, () -> { + return TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm); + }); } }); } - private void onAlarmDeleted(AlarmOperationResult result) { + private void onAlarmUpdated(AlarmApiCallResult result) { wsCallBackExecutor.submit(() -> { - Alarm alarm = result.getAlarm(); - TenantId tenantId = result.getAlarm().getTenantId(); + AlarmInfo alarm = result.getAlarm(); + TenantId tenantId = alarm.getTenantId(); for (EntityId entityId : result.getPropagatedEntitiesList()) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); - if (currentPartitions.contains(tpi)) { - if (subscriptionManagerService.isPresent()) { - subscriptionManagerService.get().onAlarmDeleted(tenantId, entityId, alarm, TbCallback.EMPTY); - } else { - log.warn("Possible misconfiguration because subscriptionManagerService is null!"); - } - } else { - TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmDeletedProto(tenantId, entityId, alarm); - clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); - } + forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { + subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY); + }, () -> { + return TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm); + }); + } + notificationRuleProcessor.process(AlarmTrigger.builder() + .tenantId(tenantId) + .alarmUpdate(result) + .build()); + }); + } + + private void onAlarmDeleted(AlarmApiCallResult result) { + wsCallBackExecutor.submit(() -> { + AlarmInfo alarm = result.getAlarm(); + TenantId tenantId = alarm.getTenantId(); + for (EntityId entityId : result.getPropagatedEntitiesList()) { + forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { + subscriptionManagerService.onAlarmDeleted(tenantId, entityId, alarm, TbCallback.EMPTY); + }, () -> { + return TbSubscriptionUtils.toAlarmDeletedProto(tenantId, entityId, alarm); + }); } + notificationRuleProcessor.process(AlarmTrigger.builder() + .tenantId(tenantId) + .alarmUpdate(result) + .build()); }); } - private class AlarmUpdateCallback implements FutureCallback { + private class AlarmUpdateCallback implements FutureCallback { @Override - public void onSuccess(@Nullable AlarmOperationResult result) { + public void onSuccess(@Nullable AlarmApiCallResult result) { onAlarmUpdated(result); } @@ -216,4 +283,31 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } } + private AlarmApiCallResult withWsCallback(AlarmApiCallResult result) { + return withWsCallback(null, result); + } + + private AlarmApiCallResult withWsCallback(AlarmModificationRequest request, AlarmApiCallResult result) { + if (result.isSuccessful() && result.isModified()) { + Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor); + if (result.isSeverityChanged()) { + AlarmInfo alarm = result.getAlarm(); + AlarmComment.AlarmCommentBuilder alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", + String.format("Alarm severity was updated from %s to %s", result.getOldSeverity(), alarm.getSeverity()))); + if (request != null && request.getUserId() != null) { + alarmComment.userId(request.getUserId()); + } + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment.build(), null); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + } + } + return result; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 667ad5b881..3f5e52796a 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; @@ -40,14 +39,11 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; -import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.dao.util.KvUtils; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; @@ -85,11 +81,8 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer public DefaultTelemetrySubscriptionService(AttributesService attrService, TimeseriesService tsService, @Lazy TbEntityViewService tbEntityViewService, - TbClusterService clusterService, - PartitionService partitionService, TbApiUsageReportClient apiUsageClient, TbApiUsageStateService apiUsageStateService) { - super(clusterService, partitionService); this.attrService = attrService; this.tsService = tsService; this.tbEntityViewService = tbEntityViewService; @@ -142,6 +135,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer checkInternalEntity(entityId); boolean sysTenant = TenantId.SYS_TENANT_ID.equals(tenantId) || tenantId == null; if (sysTenant || apiUsageStateService.getApiUsageState(tenantId).isDbStorageEnabled()) { + KvUtils.validate(ts); if (saveLatest) { saveAndNotifyInternal(tenantId, entityId, ts, ttl, getCallback(tenantId, customerId, sysTenant, callback)); } else { @@ -375,76 +369,53 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); - if (currentPartitions.contains(tpi)) { - if (subscriptionManagerService.isPresent()) { - subscriptionManagerService.get().onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY); - } else { - log.warn("Possible misconfiguration because subscriptionManagerService is null!"); - } - } else { - TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAttributesUpdateProto(tenantId, entityId, scope, attributes); - clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); - } + forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { + subscriptionManagerService.onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY); + }, () -> { + return TbSubscriptionUtils.toAttributesUpdateProto(tenantId, entityId, scope, attributes); + }); } private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); - if (currentPartitions.contains(tpi)) { - if (subscriptionManagerService.isPresent()) { - subscriptionManagerService.get().onAttributesDelete(tenantId, entityId, scope, keys, notifyDevice, TbCallback.EMPTY); - } else { - log.warn("Possible misconfiguration because subscriptionManagerService is null!"); - } - } else { - TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys, notifyDevice); - clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); - } + forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { + subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, notifyDevice, TbCallback.EMPTY); + }, () -> { + return TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys, notifyDevice); + }); } private void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List ts) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); - if (currentPartitions.contains(tpi)) { - if (subscriptionManagerService.isPresent()) { - subscriptionManagerService.get().onTimeSeriesUpdate(tenantId, entityId, ts, TbCallback.EMPTY); - } else { - log.warn("Possible misconfiguration because subscriptionManagerService is null!"); - } - } else { - TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toTimeseriesUpdateProto(tenantId, entityId, ts); - clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); - } + forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { + subscriptionManagerService.onTimeSeriesUpdate(tenantId, entityId, ts, TbCallback.EMPTY); + }, () -> { + return TbSubscriptionUtils.toTimeseriesUpdateProto(tenantId, entityId, ts); + }); } private void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List keys, List ts) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); - if (currentPartitions.contains(tpi)) { - if (subscriptionManagerService.isPresent()) { - List updated = new ArrayList<>(); - List deleted = new ArrayList<>(); - - ts.stream().filter(Objects::nonNull).forEach(res -> { - if (res.isRemoved()) { - if (res.getData() != null) { - updated.add(res.getData()); - } else { - deleted.add(res.getKey()); - } + forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { + List updated = new ArrayList<>(); + List deleted = new ArrayList<>(); + + ts.stream().filter(Objects::nonNull).forEach(res -> { + if (res.isRemoved()) { + if (res.getData() != null) { + updated.add(res.getData()); + } else { + deleted.add(res.getKey()); } - }); + } + }); - subscriptionManagerService.get().onTimeSeriesUpdate(tenantId, entityId, updated, TbCallback.EMPTY); - subscriptionManagerService.get().onTimeSeriesDelete(tenantId, entityId, deleted, TbCallback.EMPTY); - } else { - log.warn("Possible misconfiguration because subscriptionManagerService is null!"); - } - } else { - TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toTimeseriesDeleteProto(tenantId, entityId, keys); - clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); - } + subscriptionManagerService.onTimeSeriesUpdate(tenantId, entityId, updated, TbCallback.EMPTY); + subscriptionManagerService.onTimeSeriesDelete(tenantId, entityId, deleted, TbCallback.EMPTY); + }, () -> { + return TbSubscriptionUtils.toTimeseriesDeleteProto(tenantId, entityId, keys); + }); } private void addVoidCallback(ListenableFuture saveFuture, final FutureCallback callback) { + if (callback == null) return; Futures.addCallback(saveFuture, new FutureCallback() { @Override public void onSuccess(@Nullable S result) { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java deleted file mode 100644 index d9beb1ed33..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java +++ /dev/null @@ -1,935 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Function; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.socket.CloseStatus; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.kv.Aggregation; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.util.TenantRateLimitException; -import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.AccessValidator; -import org.thingsboard.server.service.security.ValidationCallback; -import org.thingsboard.server.service.security.ValidationResult; -import org.thingsboard.server.service.security.ValidationResultCode; -import org.thingsboard.server.service.security.model.UserPrincipal; -import org.thingsboard.server.service.security.permission.Operation; -import org.thingsboard.server.service.subscription.TbAttributeSubscription; -import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; -import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService; -import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; -import org.thingsboard.server.service.subscription.TbTimeseriesSubscription; -import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; -import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd; -import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd; -import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd; -import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd; -import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; -import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; -import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; - -import javax.annotation.Nullable; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -/** - * Created by ashvayka on 27.03.18. - */ -@Service -@TbCoreComponent -@Slf4j -public class DefaultTelemetryWebSocketService implements TelemetryWebSocketService { - - public static final int NUMBER_OF_PING_ATTEMPTS = 3; - - private static final int DEFAULT_LIMIT = 100; - private static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE; - private static final int UNKNOWN_SUBSCRIPTION_ID = 0; - private static final String PROCESSING_MSG = "[{}] Processing: {}"; - private static final ObjectMapper jsonMapper = new ObjectMapper(); - private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; - private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; - private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; - private static final String FAILED_TO_PARSE_WS_COMMAND = "Failed to parse websocket command!"; - - private final ConcurrentMap wsSessionsMap = new ConcurrentHashMap<>(); - - @Autowired - private TbLocalSubscriptionService oldSubService; - - @Autowired - private TbEntityDataSubscriptionService entityDataSubService; - - @Autowired - private TelemetryWebSocketMsgEndpoint msgEndpoint; - - @Autowired - private AccessValidator accessValidator; - - @Autowired - private AttributesService attributesService; - - @Autowired - private TimeseriesService tsService; - - @Autowired - private TbServiceInfoProvider serviceInfoProvider; - - @Autowired - private TbTenantProfileCache tenantProfileCache; - - @Value("${server.ws.ping_timeout:30000}") - private long pingTimeout; - - private ConcurrentMap> tenantSubscriptionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> customerSubscriptionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> regularUserSubscriptionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> publicUserSubscriptionsMap = new ConcurrentHashMap<>(); - - private ExecutorService executor; - private String serviceId; - - private ScheduledExecutorService pingExecutor; - - @PostConstruct - public void initExecutor() { - serviceId = serviceInfoProvider.getServiceId(); - executor = ThingsBoardExecutors.newWorkStealingPool(50, getClass()); - - pingExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("telemetry-web-socket-ping")); - pingExecutor.scheduleWithFixedDelay(this::sendPing, pingTimeout / NUMBER_OF_PING_ATTEMPTS, pingTimeout / NUMBER_OF_PING_ATTEMPTS, TimeUnit.MILLISECONDS); - } - - @PreDestroy - public void shutdownExecutor() { - if (pingExecutor != null) { - pingExecutor.shutdownNow(); - } - - if (executor != null) { - executor.shutdownNow(); - } - } - - @Override - public void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) { - String sessionId = sessionRef.getSessionId(); - log.debug(PROCESSING_MSG, sessionId, event); - switch (event.getEventType()) { - case ESTABLISHED: - wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef)); - break; - case ERROR: - log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null)); - break; - case CLOSED: - wsSessionsMap.remove(sessionId); - oldSubService.cancelAllSessionSubscriptions(sessionId); - entityDataSubService.cancelAllSessionSubscriptions(sessionId); - processSessionClose(sessionRef); - break; - } - } - - @Override - public void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg) { - if (log.isTraceEnabled()) { - log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg); - } - - try { - TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class); - if (cmdsWrapper != null) { - if (cmdsWrapper.getAttrSubCmds() != null) { - cmdsWrapper.getAttrSubCmds().forEach(cmd -> { - if (processSubscription(sessionRef, cmd)) { - handleWsAttributesSubscriptionCmd(sessionRef, cmd); - } - }); - } - if (cmdsWrapper.getTsSubCmds() != null) { - cmdsWrapper.getTsSubCmds().forEach(cmd -> { - if (processSubscription(sessionRef, cmd)) { - handleWsTimeseriesSubscriptionCmd(sessionRef, cmd); - } - }); - } - if (cmdsWrapper.getHistoryCmds() != null) { - cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd)); - } - if (cmdsWrapper.getEntityDataCmds() != null) { - cmdsWrapper.getEntityDataCmds().forEach(cmd -> handleWsEntityDataCmd(sessionRef, cmd)); - } - if (cmdsWrapper.getAlarmDataCmds() != null) { - cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd)); - } - if (cmdsWrapper.getEntityCountCmds() != null) { - cmdsWrapper.getEntityCountCmds().forEach(cmd -> handleWsEntityCountCmd(sessionRef, cmd)); - } - if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) { - cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); - } - if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) { - cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); - } - if (cmdsWrapper.getEntityCountUnsubscribeCmds() != null) { - cmdsWrapper.getEntityCountUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); - } - } - } catch (IOException e) { - log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.BAD_REQUEST, FAILED_TO_PARSE_WS_COMMAND)); - } - } - - private void handleWsEntityDataCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { - String sessionId = sessionRef.getSessionId(); - log.debug("[{}] Processing: {}", sessionId, cmd); - - if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) - && validateSubscriptionCmd(sessionRef, cmd)) { - entityDataSubService.handleCmd(sessionRef, cmd); - } - } - - private void handleWsEntityCountCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) { - String sessionId = sessionRef.getSessionId(); - log.debug("[{}] Processing: {}", sessionId, cmd); - - if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) - && validateSubscriptionCmd(sessionRef, cmd)) { - entityDataSubService.handleCmd(sessionRef, cmd); - } - } - - private void handleWsAlarmDataCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { - String sessionId = sessionRef.getSessionId(); - log.debug("[{}] Processing: {}", sessionId, cmd); - - if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) - && validateSubscriptionCmd(sessionRef, cmd)) { - entityDataSubService.handleCmd(sessionRef, cmd); - } - } - - private void handleWsDataUnsubscribeCmd(TelemetryWebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { - String sessionId = sessionRef.getSessionId(); - log.debug("[{}] Processing: {}", sessionId, cmd); - - if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) { - entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd); - } - } - - @Override - public void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update) { - sendWsMsg(sessionId, update.getSubscriptionId(), update); - } - - @Override - public void sendWsMsg(String sessionId, CmdUpdate update) { - sendWsMsg(sessionId, update.getCmdId(), update); - } - - private void sendWsMsg(String sessionId, int cmdId, T update) { - WsSessionMetaData md = wsSessionsMap.get(sessionId); - if (md != null) { - sendWsMsg(md.getSessionRef(), cmdId, update); - } - } - - @Override - public void close(String sessionId, CloseStatus status) { - WsSessionMetaData md = wsSessionsMap.get(sessionId); - if (md != null) { - try { - msgEndpoint.close(md.getSessionRef(), status); - } catch (IOException e) { - log.warn("[{}] Failed to send session close: {}", sessionId, e); - } - } - } - - private void processSessionClose(TelemetryWebSocketSessionRef sessionRef) { - var tenantProfileConfiguration = tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration(); - if (tenantProfileConfiguration != null) { - String sessionId = "[" + sessionRef.getSessionId() + "]"; - - if (tenantProfileConfiguration.getMaxWsSubscriptionsPerTenant() > 0) { - Set tenantSubscriptions = tenantSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); - synchronized (tenantSubscriptions) { - tenantSubscriptions.removeIf(subId -> subId.startsWith(sessionId)); - } - } - if (sessionRef.getSecurityCtx().isCustomerUser()) { - if (tenantProfileConfiguration.getMaxWsSubscriptionsPerCustomer() > 0) { - Set customerSessions = customerSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet()); - synchronized (customerSessions) { - customerSessions.removeIf(subId -> subId.startsWith(sessionId)); - } - } - if (tenantProfileConfiguration.getMaxWsSubscriptionsPerRegularUser() > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { - Set regularUserSessions = regularUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); - synchronized (regularUserSessions) { - regularUserSessions.removeIf(subId -> subId.startsWith(sessionId)); - } - } - if (tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { - Set publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); - synchronized (publicUserSessions) { - publicUserSessions.removeIf(subId -> subId.startsWith(sessionId)); - } - } - } - } - } - - private boolean processSubscription(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) { - var tenantProfileConfiguration = (DefaultTenantProfileConfiguration) tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration(); - - String subId = "[" + sessionRef.getSessionId() + "]:[" + cmd.getCmdId() + "]"; - try { - if (tenantProfileConfiguration.getMaxWsSubscriptionsPerTenant() > 0) { - Set tenantSubscriptions = tenantSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); - synchronized (tenantSubscriptions) { - if (cmd.isUnsubscribe()) { - tenantSubscriptions.remove(subId); - } else if (tenantSubscriptions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerTenant()) { - tenantSubscriptions.add(subId); - } else { - log.info("[{}][{}][{}] Failed to start subscription. Max tenant subscriptions limit reached" - , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId); - msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max tenant subscriptions limit reached!")); - return false; - } - } - } - - if (sessionRef.getSecurityCtx().isCustomerUser()) { - if (tenantProfileConfiguration.getMaxWsSubscriptionsPerCustomer() > 0) { - Set customerSessions = customerSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet()); - synchronized (customerSessions) { - if (cmd.isUnsubscribe()) { - customerSessions.remove(subId); - } else if (customerSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerCustomer()) { - customerSessions.add(subId); - } else { - log.info("[{}][{}][{}] Failed to start subscription. Max customer subscriptions limit reached" - , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId); - msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max customer subscriptions limit reached")); - return false; - } - } - } - if (tenantProfileConfiguration.getMaxWsSubscriptionsPerRegularUser() > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { - Set regularUserSessions = regularUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); - synchronized (regularUserSessions) { - if (regularUserSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerRegularUser()) { - regularUserSessions.add(subId); - } else { - log.info("[{}][{}][{}] Failed to start subscription. Max regular user subscriptions limit reached" - , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId); - msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max regular user subscriptions limit reached")); - return false; - } - } - } - if (tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { - Set publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); - synchronized (publicUserSessions) { - if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser()) { - publicUserSessions.add(subId); - } else { - log.info("[{}][{}][{}] Failed to start subscription. Max public user subscriptions limit reached" - , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId); - msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max public user subscriptions limit reached")); - return false; - } - } - } - } - } catch (IOException e) { - log.warn("[{}] Failed to send session close: {}", sessionRef.getSessionId(), e); - return false; - } - return true; - } - - private void handleWsAttributesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) { - String sessionId = sessionRef.getSessionId(); - log.debug("[{}] Processing: {}", sessionId, cmd); - - if (validateSessionMetadata(sessionRef, cmd, sessionId)) { - if (cmd.isUnsubscribe()) { - unsubscribe(sessionRef, cmd, sessionId); - } else if (validateSubscriptionCmd(sessionRef, cmd)) { - EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); - log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId); - Optional> keysOptional = getKeys(cmd); - if (keysOptional.isPresent()) { - List keys = new ArrayList<>(keysOptional.get()); - handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys); - } else { - handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId); - } - } - } - } - - private void handleWsAttributesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef, - AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId, - List keys) { - FutureCallback> callback = new FutureCallback<>() { - @Override - public void onSuccess(List data) { - List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); - - Map subState = new HashMap<>(keys.size()); - keys.forEach(key -> subState.put(key, 0L)); - attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); - - TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope()); - - TbAttributeSubscription sub = TbAttributeSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(cmd.getCmdId()) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .allKeys(false) - .keyStates(subState) - .scope(scope) - .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) - .build(); - oldSubService.addSubscription(sub); - } - - @Override - public void onFailure(Throwable e) { - log.error(FAILED_TO_FETCH_ATTRIBUTES, e); - TelemetrySubscriptionUpdate update; - if (e instanceof UnauthorizedException) { - update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, - SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); - } else { - update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, - FAILED_TO_FETCH_ATTRIBUTES); - } - sendWsMsg(sessionRef, update); - } - }; - - if (StringUtils.isEmpty(cmd.getScope())) { - accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_ATTRIBUTES, entityId, getAttributesFetchCallback(sessionRef.getSecurityCtx().getTenantId(), entityId, keys, callback)); - } else { - accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_ATTRIBUTES, entityId, getAttributesFetchCallback(sessionRef.getSecurityCtx().getTenantId(), entityId, cmd.getScope(), keys, callback)); - } - } - - private void handleWsHistoryCmd(TelemetryWebSocketSessionRef sessionRef, GetHistoryCmd cmd) { - String sessionId = sessionRef.getSessionId(); - WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); - if (sessionMD == null) { - log.warn("[{}] Session meta data not found. ", sessionId); - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, - SESSION_META_DATA_NOT_FOUND); - sendWsMsg(sessionRef, update); - return; - } - if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) { - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, - "Device id is empty!"); - sendWsMsg(sessionRef, update); - return; - } - if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) { - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, - "Keys are empty!"); - sendWsMsg(sessionRef, update); - return; - } - EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); - List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); - List queries = keys.stream().map(key -> new BaseReadTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))) - .collect(Collectors.toList()); - - FutureCallback> callback = new FutureCallback>() { - @Override - public void onSuccess(List data) { - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); - } - - @Override - public void onFailure(Throwable e) { - TelemetrySubscriptionUpdate update; - if (UnauthorizedException.class.isInstance(e)) { - update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, - SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); - } else { - update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, - FAILED_TO_FETCH_DATA); - } - sendWsMsg(sessionRef, update); - } - }; - accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, - on(r -> Futures.addCallback(tsService.findAll(sessionRef.getSecurityCtx().getTenantId(), entityId, queries), callback, executor), callback::onFailure)); - } - - private void handleWsAttributesSubscription(TelemetryWebSocketSessionRef sessionRef, - AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) { - FutureCallback> callback = new FutureCallback<>() { - @Override - public void onSuccess(List data) { - List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); - - Map subState = new HashMap<>(attributesData.size()); - attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); - - TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope()); - - TbAttributeSubscription sub = TbAttributeSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(cmd.getCmdId()) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .allKeys(true) - .keyStates(subState) - .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) - .scope(scope).build(); - oldSubService.addSubscription(sub); - } - - @Override - public void onFailure(Throwable e) { - log.error(FAILED_TO_FETCH_ATTRIBUTES, e); - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, - FAILED_TO_FETCH_ATTRIBUTES); - sendWsMsg(sessionRef, update); - } - }; - - - if (StringUtils.isEmpty(cmd.getScope())) { - accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_ATTRIBUTES, entityId, getAttributesFetchCallback(sessionRef.getSecurityCtx().getTenantId(), entityId, callback)); - } else { - accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_ATTRIBUTES, entityId, getAttributesFetchCallback(sessionRef.getSecurityCtx().getTenantId(), entityId, cmd.getScope(), callback)); - } - } - - private void handleWsTimeseriesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) { - String sessionId = sessionRef.getSessionId(); - log.debug("[{}] Processing: {}", sessionId, cmd); - - if (validateSessionMetadata(sessionRef, cmd, sessionId)) { - if (cmd.isUnsubscribe()) { - unsubscribe(sessionRef, cmd, sessionId); - } else if (validateSubscriptionCmd(sessionRef, cmd)) { - EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); - Optional> keysOptional = getKeys(cmd); - - if (keysOptional.isPresent()) { - handleWsTimeseriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId); - } else { - handleWsTimeseriesSubscription(sessionRef, cmd, sessionId, entityId); - } - } - } - } - - private void handleWsTimeseriesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef, - TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) { - long startTs; - if (cmd.getTimeWindow() > 0) { - List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); - log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), entityId); - startTs = cmd.getStartTs(); - long endTs = cmd.getStartTs() + cmd.getTimeWindow(); - List queries = keys.stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, cmd.getInterval(), - getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); - - final FutureCallback> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys); - accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, - on(r -> Futures.addCallback(tsService.findAll(sessionRef.getSecurityCtx().getTenantId(), entityId, queries), callback, executor), callback::onFailure)); - } else { - List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); - startTs = System.currentTimeMillis(); - log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), entityId); - final FutureCallback> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys); - accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, - on(r -> Futures.addCallback(tsService.findLatest(sessionRef.getSecurityCtx().getTenantId(), entityId, keys), callback, executor), callback::onFailure)); - } - } - - private void handleWsTimeseriesSubscription(TelemetryWebSocketSessionRef sessionRef, - TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) { - FutureCallback> callback = new FutureCallback>() { - @Override - public void onSuccess(List data) { - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); - Map subState = new HashMap<>(data.size()); - data.forEach(v -> subState.put(v.getKey(), v.getTs())); - - TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(cmd.getCmdId()) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) - .allKeys(true) - .keyStates(subState).build(); - oldSubService.addSubscription(sub); - } - - @Override - public void onFailure(Throwable e) { - TelemetrySubscriptionUpdate update; - if (UnauthorizedException.class.isInstance(e)) { - update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, - SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); - } else { - update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, - FAILED_TO_FETCH_DATA); - } - sendWsMsg(sessionRef, update); - } - }; - accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, - on(r -> Futures.addCallback(tsService.findAllLatest(sessionRef.getSecurityCtx().getTenantId(), entityId), callback, executor), callback::onFailure)); - } - - private FutureCallback> getSubscriptionCallback(final TelemetryWebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List keys) { - return new FutureCallback<>() { - @Override - public void onSuccess(List data) { - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); - Map subState = new HashMap<>(keys.size()); - keys.forEach(key -> subState.put(key, startTs)); - data.forEach(v -> subState.put(v.getKey(), v.getTs())); - - TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(cmd.getCmdId()) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) - .allKeys(false) - .keyStates(subState).build(); - oldSubService.addSubscription(sub); - } - - @Override - public void onFailure(Throwable e) { - if (e instanceof TenantRateLimitException || e.getCause() instanceof TenantRateLimitException) { - log.trace("[{}] Tenant rate limit detected for subscription: [{}]:{}", sessionRef.getSecurityCtx().getTenantId(), entityId, cmd); - } else { - log.info(FAILED_TO_FETCH_DATA, e); - } - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, - FAILED_TO_FETCH_DATA); - sendWsMsg(sessionRef, update); - } - }; - } - - private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { - if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { - oldSubService.cancelAllSessionSubscriptions(sessionId); - } else { - oldSubService.cancelSubscription(sessionId, cmd.getCmdId()); - } - } - - private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { - if (cmd.getCmdId() < 0) { - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, - "Cmd id is negative value!"); - sendWsMsg(sessionRef, update); - return false; - } else if (cmd.getQuery() == null && !cmd.hasAnyCmd()) { - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, - "Query is empty!"); - sendWsMsg(sessionRef, update); - return false; - } - return true; - } - - private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) { - if (cmd.getCmdId() < 0) { - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, - "Cmd id is negative value!"); - sendWsMsg(sessionRef, update); - return false; - } else if (cmd.getQuery() == null) { - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Query is empty!"); - sendWsMsg(sessionRef, update); - return false; - } - return true; - } - - private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { - if (cmd.getCmdId() < 0) { - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, - "Cmd id is negative value!"); - sendWsMsg(sessionRef, update); - return false; - } else if (cmd.getQuery() == null) { - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, - "Query is empty!"); - sendWsMsg(sessionRef, update); - return false; - } - return true; - } - - private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) { - if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, - "Device id is empty!"); - sendWsMsg(sessionRef, update); - return false; - } - return true; - } - - private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { - return validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId); - } - - private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, int cmdId, String sessionId) { - WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); - if (sessionMD == null) { - log.warn("[{}] Session meta data not found. ", sessionId); - TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmdId, SubscriptionErrorCode.INTERNAL_ERROR, - SESSION_META_DATA_NOT_FOUND); - sendWsMsg(sessionRef, update); - return false; - } else { - return true; - } - } - - private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, EntityDataUpdate update) { - sendWsMsg(sessionRef, update.getCmdId(), update); - } - - private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, TelemetrySubscriptionUpdate update) { - sendWsMsg(sessionRef, update.getSubscriptionId(), update); - } - - private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, int cmdId, Object update) { - try { - String msg = jsonMapper.writeValueAsString(update); - executor.submit(() -> { - try { - msgEndpoint.send(sessionRef, cmdId, msg); - } catch (IOException e) { - log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e); - } - }); - } catch (JsonProcessingException e) { - log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); - } - } - - private void sendPing() { - long currentTime = System.currentTimeMillis(); - wsSessionsMap.values().forEach(md -> - executor.submit(() -> { - try { - msgEndpoint.sendPing(md.getSessionRef(), currentTime); - } catch (IOException e) { - log.warn("[{}] Failed to send ping: {}", md.getSessionRef().getSessionId(), e); - } - })); - } - - private static Optional> getKeys(TelemetryPluginCmd cmd) { - if (!StringUtils.isEmpty(cmd.getKeys())) { - Set keys = new HashSet<>(); - Collections.addAll(keys, cmd.getKeys().split(",")); - return Optional.of(keys); - } else { - return Optional.empty(); - } - } - - private ListenableFuture> mergeAllAttributesFutures(List>> futures) { - return Futures.transform(Futures.successfulAsList(futures), - (Function>, ? extends List>) input -> { - List tmp = new ArrayList<>(); - if (input != null) { - input.forEach(tmp::addAll); - } - return tmp; - }, executor); - } - - private FutureCallback getAttributesFetchCallback(final TenantId tenantId, final EntityId entityId, final List keys, final FutureCallback> callback) { - return new FutureCallback() { - @Override - public void onSuccess(@Nullable ValidationResult result) { - List>> futures = new ArrayList<>(); - for (String scope : DataConstants.allScopes()) { - futures.add(attributesService.find(tenantId, entityId, scope, keys)); - } - - ListenableFuture> future = mergeAllAttributesFutures(futures); - Futures.addCallback(future, callback, MoreExecutors.directExecutor()); - } - - @Override - public void onFailure(Throwable t) { - callback.onFailure(t); - } - }; - } - - private FutureCallback getAttributesFetchCallback(final TenantId tenantId, final EntityId entityId, final String scope, final List keys, final FutureCallback> callback) { - return new FutureCallback() { - @Override - public void onSuccess(@Nullable ValidationResult result) { - Futures.addCallback(attributesService.find(tenantId, entityId, scope, keys), callback, MoreExecutors.directExecutor()); - } - - @Override - public void onFailure(Throwable t) { - callback.onFailure(t); - } - }; - } - - private FutureCallback getAttributesFetchCallback(final TenantId tenantId, final EntityId entityId, final FutureCallback> callback) { - return new FutureCallback() { - @Override - public void onSuccess(@Nullable ValidationResult result) { - List>> futures = new ArrayList<>(); - for (String scope : DataConstants.allScopes()) { - futures.add(attributesService.findAll(tenantId, entityId, scope)); - } - - ListenableFuture> future = mergeAllAttributesFutures(futures); - Futures.addCallback(future, callback, MoreExecutors.directExecutor()); - } - - @Override - public void onFailure(Throwable t) { - callback.onFailure(t); - } - }; - } - - private FutureCallback getAttributesFetchCallback(final TenantId tenantId, final EntityId entityId, final String scope, final FutureCallback> callback) { - return new FutureCallback() { - @Override - public void onSuccess(@Nullable ValidationResult result) { - Futures.addCallback(attributesService.findAll(tenantId, entityId, scope), callback, MoreExecutors.directExecutor()); - } - - @Override - public void onFailure(Throwable t) { - callback.onFailure(t); - } - }; - } - - private FutureCallback on(Consumer success, Consumer failure) { - return new FutureCallback() { - @Override - public void onSuccess(@Nullable ValidationResult result) { - ValidationResultCode resultCode = result.getResultCode(); - if (resultCode == ValidationResultCode.OK) { - success.accept(null); - } else { - onFailure(ValidationCallback.getException(result)); - } - } - - @Override - public void onFailure(Throwable t) { - failure.accept(t); - } - }; - } - - - public static Aggregation getAggregation(String agg) { - return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg); - } - - private int getLimit(int limit) { - return limit == 0 ? DEFAULT_LIMIT : limit; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java index 298d894460..301376b16a 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,4 @@ public interface InternalTelemetryService extends RuleEngineTelemetryService { void deleteLatestInternal(TenantId tenantId, EntityId entityId, List keys, FutureCallback callback); - - } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/SessionEvent.java b/application/src/main/java/org/thingsboard/server/service/telemetry/SessionEvent.java deleted file mode 100644 index 527f5dc667..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/SessionEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry; - -import lombok.Getter; -import lombok.ToString; - -import java.util.Optional; - -@ToString -public class SessionEvent { - - public enum SessionEventType { - ESTABLISHED, CLOSED, ERROR - }; - - @Getter - private final SessionEventType eventType; - @Getter - private final Optional error; - - private SessionEvent(SessionEventType eventType, Throwable error) { - super(); - this.eventType = eventType; - this.error = Optional.ofNullable(error); - } - - public static SessionEvent onEstablished() { - return new SessionEvent(SessionEventType.ESTABLISHED, null); - } - - public static SessionEvent onClosed() { - return new SessionEvent(SessionEventType.CLOSED, null); - } - - public static SessionEvent onError(Throwable t) { - return new SessionEvent(SessionEventType.ERROR, t); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryFeature.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryFeature.java deleted file mode 100644 index b0a63970a5..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryFeature.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry; - -/** - * Created by ashvayka on 08.05.17. - */ -public enum TelemetryFeature { - - ATTRIBUTES, TIMESERIES; - - public static TelemetryFeature forName(String name) { - return TelemetryFeature.valueOf(name.toUpperCase()); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java index db022966f0..8ec3b31b94 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java deleted file mode 100644 index d71777ae49..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry; - -import org.springframework.web.socket.CloseStatus; - -import java.io.IOException; - -/** - * Created by ashvayka on 27.03.18. - */ -public interface TelemetryWebSocketMsgEndpoint { - - void send(TelemetryWebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException; - - void sendPing(TelemetryWebSocketSessionRef sessionRef, long currentTime) throws IOException; - - void close(TelemetryWebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException; -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java deleted file mode 100644 index 510283b31d..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry; - -import org.springframework.web.socket.CloseStatus; -import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate; -import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; - -/** - * Created by ashvayka on 27.03.18. - */ -public interface TelemetryWebSocketService { - - void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent sessionEvent); - - void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg); - - void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update); - - void sendWsMsg(String sessionId, CmdUpdate update); - - void close(String sessionId, CloseStatus status); -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java deleted file mode 100644 index da8a8962e6..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry; - -import lombok.Getter; -import org.thingsboard.server.service.security.model.SecurityUser; - -import java.net.InetSocketAddress; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Created by ashvayka on 27.03.18. - */ -public class TelemetryWebSocketSessionRef { - - private static final long serialVersionUID = 1L; - - @Getter - private final String sessionId; - @Getter - private final SecurityUser securityCtx; - @Getter - private final InetSocketAddress localAddress; - @Getter - private final InetSocketAddress remoteAddress; - @Getter - private final AtomicInteger sessionSubIdSeq; - - public TelemetryWebSocketSessionRef(String sessionId, SecurityUser securityCtx, InetSocketAddress localAddress, InetSocketAddress remoteAddress) { - this.sessionId = sessionId; - this.securityCtx = securityCtx; - this.localAddress = localAddress; - this.remoteAddress = remoteAddress; - this.sessionSubIdSeq = new AtomicInteger(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TelemetryWebSocketSessionRef that = (TelemetryWebSocketSessionRef) o; - return Objects.equals(sessionId, that.sessionId); - } - - @Override - public int hashCode() { - return Objects.hash(sessionId); - } - - @Override - public String toString() { - return "TelemetryWebSocketSessionRef{" + - "sessionId='" + sessionId + '\'' + - ", localAddress=" + localAddress + - ", remoteAddress=" + remoteAddress + - '}'; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java deleted file mode 100644 index 44f2ea0943..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry; - -import lombok.Data; - -/** - * Created by ashvayka on 27.03.18. - */ -@Data -public class TelemetryWebSocketTextMsg { - - private final TelemetryWebSocketSessionRef sessionRef; - private final String payload; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TsData.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TsData.java index 56d1785d45..d0af1080d3 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TsData.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TsData.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java b/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java deleted file mode 100644 index ab353c0581..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry; - - -/** - * Created by ashvayka on 27.03.18. - */ -public class WsSessionMetaData { - private TelemetryWebSocketSessionRef sessionRef; - private long lastActivityTime; - - public WsSessionMetaData(TelemetryWebSocketSessionRef sessionRef) { - super(); - this.sessionRef = sessionRef; - this.lastActivityTime = System.currentTimeMillis(); - } - - public TelemetryWebSocketSessionRef getSessionRef() { - return sessionRef; - } - - public void setSessionRef(TelemetryWebSocketSessionRef sessionRef) { - this.sessionRef = sessionRef; - } - - public long getLastActivityTime() { - return lastActivityTime; - } - - public void setLastActivityTime(long lastActivityTime) { - this.lastActivityTime = lastActivityTime; - } - - @Override - public String toString() { - return "WsSessionMetaData [sessionRef=" + sessionRef + ", lastActivityTime=" + lastActivityTime + "]"; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java deleted file mode 100644 index 0c46f90610..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd; - -import lombok.Data; -import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd; -import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd; -import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUnsubscribeCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; - -import java.util.List; - -/** - * @author Andrew Shvayka - */ -@Data -public class TelemetryPluginCmdsWrapper { - - private List attrSubCmds; - - private List tsSubCmds; - - private List historyCmds; - - private List entityDataCmds; - - private List entityDataUnsubscribeCmds; - - private List alarmDataCmds; - - private List alarmDataUnsubscribeCmds; - - private List entityCountCmds; - - private List entityCountUnsubscribeCmds; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/AttributesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/AttributesSubscriptionCmd.java deleted file mode 100644 index f5e2630af8..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/AttributesSubscriptionCmd.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v1; - -import lombok.NoArgsConstructor; -import org.thingsboard.server.service.telemetry.TelemetryFeature; - -/** - * @author Andrew Shvayka - */ -@NoArgsConstructor -public class AttributesSubscriptionCmd extends SubscriptionCmd { - - @Override - public TelemetryFeature getType() { - return TelemetryFeature.ATTRIBUTES; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/GetHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/GetHistoryCmd.java deleted file mode 100644 index b74f2f7ea7..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/GetHistoryCmd.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v1; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author Andrew Shvayka - */ -@NoArgsConstructor -@AllArgsConstructor -@Data -public class GetHistoryCmd implements TelemetryPluginCmd { - - private int cmdId; - private String entityType; - private String entityId; - private String keys; - private long startTs; - private long endTs; - private long interval; - private int limit; - private String agg; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/SubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/SubscriptionCmd.java deleted file mode 100644 index 0c912fac30..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/SubscriptionCmd.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v1; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.thingsboard.server.service.telemetry.TelemetryFeature; - -@NoArgsConstructor -@AllArgsConstructor -@Data -public abstract class SubscriptionCmd implements TelemetryPluginCmd { - - private int cmdId; - private String entityType; - private String entityId; - private String keys; - private String scope; - private boolean unsubscribe; - - public abstract TelemetryFeature getType(); - - @Override - public String toString() { - return "SubscriptionCmd [entityType=" + entityType + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]"; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TelemetryPluginCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TelemetryPluginCmd.java deleted file mode 100644 index 7423e9e4b0..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TelemetryPluginCmd.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v1; - -/** - * @author Andrew Shvayka - */ -public interface TelemetryPluginCmd { - - int getCmdId(); - - void setCmdId(int cmdId); - - String getKeys(); - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java deleted file mode 100644 index 309a9297d6..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v1; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.thingsboard.server.service.telemetry.TelemetryFeature; - -/** - * @author Andrew Shvayka - */ -@NoArgsConstructor -@AllArgsConstructor -@Data -public class TimeseriesSubscriptionCmd extends SubscriptionCmd { - - private long startTs; - private long timeWindow; - private long interval; - private int limit; - private String agg; - - @Override - public TelemetryFeature getType() { - return TelemetryFeature.TIMESERIES; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java deleted file mode 100644 index 423c55bd0d..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Data; - -import java.util.List; - -@Data -public class AggHistoryCmd { - - private List keys; - private long startTs; - private long endTs; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java deleted file mode 100644 index d567149d01..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Data; -import org.thingsboard.server.common.data.kv.Aggregation; - -@Data -public class AggKey { - - private int id; - private String key; - private Aggregation agg; - - private Long previousStartTs; - private Long previousEndTs; - private Boolean previousValueOnly; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java deleted file mode 100644 index 1f4d88d2c3..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Data; - -import java.util.List; - -@Data -public class AggTimeSeriesCmd { - - private List keys; - private long startTs; - private long timeWindow; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataCmd.java deleted file mode 100644 index c2c01f8f77..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataCmd.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import org.thingsboard.server.common.data.query.AlarmDataQuery; - -public class AlarmDataCmd extends DataCmd { - - @Getter - private final AlarmDataQuery query; - - @JsonCreator - public AlarmDataCmd(@JsonProperty("cmdId") int cmdId, @JsonProperty("query") AlarmDataQuery query) { - super(cmdId); - this.query = query; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java deleted file mode 100644 index 153935b7a7..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Data; - -@Data -public class AlarmDataUnsubscribeCmd implements UnsubscribeCmd { - - private final int cmdId; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java deleted file mode 100644 index afd9d12b16..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.ToString; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.query.AlarmData; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; - -import java.util.List; - -@ToString -public class AlarmDataUpdate extends DataUpdate { - - @Getter - private long allowedEntities; - @Getter - private long totalEntities; - - public AlarmDataUpdate(int cmdId, PageData data, List update, long allowedEntities, long totalEntities) { - super(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); - this.allowedEntities = allowedEntities; - this.totalEntities = totalEntities; - } - - public AlarmDataUpdate(int cmdId, int errorCode, String errorMsg) { - super(cmdId, null, null, errorCode, errorMsg); - } - - @Override - public CmdUpdateType getCmdUpdateType() { - return CmdUpdateType.ALARM_DATA; - } - - @JsonCreator - public AlarmDataUpdate(@JsonProperty("cmdId") int cmdId, - @JsonProperty("data") PageData data, - @JsonProperty("update") List update, - @JsonProperty("errorCode") int errorCode, - @JsonProperty("errorMsg") String errorMsg, - @JsonProperty("allowedEntities") long allowedEntities, - @JsonProperty("totalEntities") long totalEntities) { - super(cmdId, data, update, errorCode, errorMsg); - this.allowedEntities = allowedEntities; - this.totalEntities = totalEntities; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java deleted file mode 100644 index 075c80b92b..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -@JsonIgnoreProperties(ignoreUnknown = true) -public abstract class CmdUpdate { - - private final int cmdId; - private final int errorCode; - private final String errorMsg; - - public abstract CmdUpdateType getCmdUpdateType(); - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java deleted file mode 100644 index cd78e760a6..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -public enum CmdUpdateType { - ENTITY_DATA, - ALARM_DATA, - COUNT_DATA -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataCmd.java deleted file mode 100644 index b93af17d89..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataCmd.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Data -public class DataCmd { - - @Getter - private final int cmdId; - - public DataCmd(int cmdId) { - this.cmdId = cmdId; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java deleted file mode 100644 index eda60b87b4..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Getter; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; - -import java.util.List; - -public abstract class DataUpdate extends CmdUpdate { - - @Getter - private final PageData data; - @Getter - private final List update; - - public DataUpdate(int cmdId, PageData data, List update, int errorCode, String errorMsg) { - super(cmdId, errorCode, errorMsg); - this.data = data; - this.update = update; - } - - public DataUpdate(int cmdId, PageData data, List update) { - this(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); - } - - public DataUpdate(int cmdId, int errorCode, String errorMsg) { - this(cmdId, null, null, errorCode, errorMsg); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java deleted file mode 100644 index c71884df38..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import org.thingsboard.server.common.data.query.EntityCountQuery; -import org.thingsboard.server.common.data.query.EntityDataQuery; - -public class EntityCountCmd extends DataCmd { - - @Getter - private final EntityCountQuery query; - - @JsonCreator - public EntityCountCmd(@JsonProperty("cmdId") int cmdId, - @JsonProperty("query") EntityCountQuery query) { - super(cmdId); - this.query = query; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java deleted file mode 100644 index c8daf30833..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Data; - -@Data -public class EntityCountUnsubscribeCmd implements UnsubscribeCmd { - - private final int cmdId; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java deleted file mode 100644 index de61d0e5fb..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.ToString; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.query.EntityData; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; - -import java.util.List; - -@ToString -public class EntityCountUpdate extends CmdUpdate { - - @Getter - private int count; - - public EntityCountUpdate(int cmdId, int count) { - super(cmdId, SubscriptionErrorCode.NO_ERROR.getCode(), null); - this.count = count; - } - - public EntityCountUpdate(int cmdId, int errorCode, String errorMsg) { - super(cmdId, errorCode, errorMsg); - } - - @Override - public CmdUpdateType getCmdUpdateType() { - return CmdUpdateType.COUNT_DATA; - } - - @JsonCreator - public EntityCountUpdate(@JsonProperty("cmdId") int cmdId, - @JsonProperty("count") int count, - @JsonProperty("errorCode") int errorCode, - @JsonProperty("errorMsg") String errorMsg) { - super(cmdId, errorCode, errorMsg); - this.count = count; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java deleted file mode 100644 index 8a187afa7b..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import org.thingsboard.server.common.data.query.EntityDataQuery; - -public class EntityDataCmd extends DataCmd { - - @Getter - private final EntityDataQuery query; - @Getter - private final EntityHistoryCmd historyCmd; - @Getter - private final LatestValueCmd latestCmd; - @Getter - private final TimeSeriesCmd tsCmd; - @Getter - private final AggHistoryCmd aggHistoryCmd; - @Getter - private final AggTimeSeriesCmd aggTsCmd; - - public EntityDataCmd(int cmdId, EntityDataQuery query, EntityHistoryCmd historyCmd, LatestValueCmd latestCmd, TimeSeriesCmd tsCmd) { - this(cmdId, query, historyCmd, latestCmd, tsCmd, null, null); - } - - @JsonCreator - public EntityDataCmd(@JsonProperty("cmdId") int cmdId, - @JsonProperty("query") EntityDataQuery query, - @JsonProperty("historyCmd") EntityHistoryCmd historyCmd, - @JsonProperty("latestCmd") LatestValueCmd latestCmd, - @JsonProperty("tsCmd") TimeSeriesCmd tsCmd, - @JsonProperty("aggHistoryCmd") AggHistoryCmd aggHistoryCmd, - @JsonProperty("aggTsCmd") AggTimeSeriesCmd aggTsCmd) { - super(cmdId); - this.query = query; - this.historyCmd = historyCmd; - this.latestCmd = latestCmd; - this.tsCmd = tsCmd; - this.aggHistoryCmd = aggHistoryCmd; - this.aggTsCmd = aggTsCmd; - } - - @JsonIgnore - public boolean hasAnyCmd() { - return historyCmd != null || latestCmd != null || tsCmd != null || aggHistoryCmd != null || aggTsCmd != null; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java deleted file mode 100644 index 45aa0c5216..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Data; - -@Data -public class EntityDataUnsubscribeCmd implements UnsubscribeCmd { - - private final int cmdId; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java deleted file mode 100644 index 8da696ce9c..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.ToString; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.query.EntityData; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; - -import java.util.List; - -@ToString -public class EntityDataUpdate extends DataUpdate { - - @Getter - private long allowedEntities; - - public EntityDataUpdate(int cmdId, PageData data, List update, long allowedEntities) { - super(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); - this.allowedEntities = allowedEntities; - } - - public EntityDataUpdate(int cmdId, int errorCode, String errorMsg) { - super(cmdId, null, null, errorCode, errorMsg); - } - - @Override - public CmdUpdateType getCmdUpdateType() { - return CmdUpdateType.ENTITY_DATA; - } - - @JsonCreator - public EntityDataUpdate(@JsonProperty("cmdId") int cmdId, - @JsonProperty("data") PageData data, - @JsonProperty("update") List update, - @JsonProperty("errorCode") int errorCode, - @JsonProperty("errorMsg") String errorMsg) { - super(cmdId, data, update, errorCode, errorMsg); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java deleted file mode 100644 index 946775889f..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Data; -import org.thingsboard.server.common.data.kv.Aggregation; - -import java.util.List; - -@Data -public class EntityHistoryCmd implements GetTsCmd { - - private List keys; - private long startTs; - private long endTs; - private long interval; - private int limit; - private Aggregation agg; - private boolean fetchLatestPreviousPoint; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/GetTsCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/GetTsCmd.java deleted file mode 100644 index 724c2d6fd2..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/GetTsCmd.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import org.thingsboard.server.common.data.kv.Aggregation; - -import java.util.List; - -public interface GetTsCmd { - - long getStartTs(); - - long getEndTs(); - - List getKeys(); - - long getInterval(); - - int getLimit(); - - Aggregation getAgg(); - - boolean isFetchLatestPreviousPoint(); - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/LatestValueCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/LatestValueCmd.java deleted file mode 100644 index 184026ac54..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/LatestValueCmd.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import lombok.Data; -import org.thingsboard.server.common.data.query.EntityKey; - -import java.util.List; - -@Data -public class LatestValueCmd { - - private List keys; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/TimeSeriesCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/TimeSeriesCmd.java deleted file mode 100644 index daf9c32fbd..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/TimeSeriesCmd.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Data; -import org.thingsboard.server.common.data.kv.Aggregation; - -import java.util.List; - -@Data -public class TimeSeriesCmd implements GetTsCmd { - - private List keys; - private long startTs; - private long timeWindow; - private long interval; - private int limit; - private Aggregation agg; - private boolean fetchLatestPreviousPoint; - - @JsonIgnore - @Override - public long getEndTs() { - return startTs + timeWindow; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java deleted file mode 100644 index eec564e7a8..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd.v2; - -public interface UnsubscribeCmd { - - int getCmdId(); - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/AccessDeniedException.java b/application/src/main/java/org/thingsboard/server/service/telemetry/exception/AccessDeniedException.java deleted file mode 100644 index 43a7f8c56f..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/AccessDeniedException.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -public class AccessDeniedException extends Exception implements ToErrorResponseEntity { - - public AccessDeniedException(String message) { - super(message); - } - - @Override - public ResponseEntity toErrorResponseEntity() { - return new ResponseEntity<>(getMessage(), HttpStatus.FORBIDDEN); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/EntityNotFoundException.java b/application/src/main/java/org/thingsboard/server/service/telemetry/exception/EntityNotFoundException.java deleted file mode 100644 index 3ede4e7e70..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/EntityNotFoundException.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -public class EntityNotFoundException extends Exception implements ToErrorResponseEntity { - - public EntityNotFoundException(String message) { - super(message); - } - - @Override - public ResponseEntity toErrorResponseEntity() { - return new ResponseEntity<>(getMessage(), HttpStatus.NOT_FOUND); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InternalErrorException.java b/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InternalErrorException.java deleted file mode 100644 index ae68ffdc3d..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InternalErrorException.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -public class InternalErrorException extends Exception implements ToErrorResponseEntity { - - public InternalErrorException(String message) { - super(message); - } - - @Override - public ResponseEntity toErrorResponseEntity() { - return new ResponseEntity<>(getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InvalidParametersException.java b/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InvalidParametersException.java deleted file mode 100644 index 7452d25a56..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InvalidParametersException.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -public class InvalidParametersException extends Exception implements ToErrorResponseEntity { - - public InvalidParametersException(String message) { - super(message); - } - - @Override - public ResponseEntity toErrorResponseEntity() { - return new ResponseEntity<>(getMessage(), HttpStatus.BAD_REQUEST); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/ToErrorResponseEntity.java b/application/src/main/java/org/thingsboard/server/service/telemetry/exception/ToErrorResponseEntity.java deleted file mode 100644 index 769c5ffe5e..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/ToErrorResponseEntity.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.exception; - -import org.springframework.http.ResponseEntity; - -import java.io.Serializable; - -public interface ToErrorResponseEntity extends Serializable { - - ResponseEntity toErrorResponseEntity(); - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UnauthorizedException.java b/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UnauthorizedException.java deleted file mode 100644 index b597a84e29..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UnauthorizedException.java +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -/** - * Created by ashvayka on 21.02.17. - */ -public class UnauthorizedException extends Exception implements ToErrorResponseEntity { - - public UnauthorizedException(String message) { - super(message); - } - - @Override - public ResponseEntity toErrorResponseEntity() { - return new ResponseEntity<>(getMessage(), HttpStatus.UNAUTHORIZED); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UncheckedApiException.java b/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UncheckedApiException.java deleted file mode 100644 index 8ee4bfa753..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UncheckedApiException.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.exception; - -import org.springframework.http.ResponseEntity; - -import java.util.Objects; - -public class UncheckedApiException extends RuntimeException implements ToErrorResponseEntity { - - private final ToErrorResponseEntity cause; - - public UncheckedApiException(T cause) { - super(cause.getMessage(), Objects.requireNonNull(cause)); - this.cause = cause; - } - - @Override - public ResponseEntity toErrorResponseEntity() { - return cause.toErrorResponseEntity(); - } -} - diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java deleted file mode 100644 index 3f4bc9ce1e..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.sub; - -import lombok.Getter; -import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.query.AlarmData; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.stream.Collectors; - -public class AlarmSubscriptionUpdate { - - @Getter - private int subscriptionId; - @Getter - private int errorCode; - @Getter - private String errorMsg; - @Getter - private Alarm alarm; - @Getter - private boolean alarmDeleted; - - public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm) { - this(subscriptionId, alarm, false); - } - - public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm, boolean alarmDeleted) { - super(); - this.subscriptionId = subscriptionId; - this.alarm = alarm; - this.alarmDeleted = alarmDeleted; - } - - public AlarmSubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) { - this(subscriptionId, errorCode, null); - } - - public AlarmSubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) { - super(); - this.subscriptionId = subscriptionId; - this.errorCode = errorCode.getCode(); - this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg(); - } - - @Override - public String toString() { - return "AlarmUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", alarm=" - + alarm + "]"; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java deleted file mode 100644 index e5fe16de01..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.sub; - -public enum SubscriptionErrorCode { - - NO_ERROR(0), INTERNAL_ERROR(1, "Internal Server error!"), BAD_REQUEST(2, "Bad request"), UNAUTHORIZED(3, "Unauthorized"); - - private final int code; - private final String defaultMsg; - - private SubscriptionErrorCode(int code) { - this(code, null); - } - - private SubscriptionErrorCode(int code, String defaultMsg) { - this.code = code; - this.defaultMsg = defaultMsg; - } - - public static SubscriptionErrorCode forCode(int code) { - for (SubscriptionErrorCode errorCode : SubscriptionErrorCode.values()) { - if (errorCode.getCode() == code) { - return errorCode; - } - } - throw new IllegalArgumentException("Invalid error code: " + code); - } - - public int getCode() { - return code; - } - - public String getDefaultMsg() { - return defaultMsg; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java deleted file mode 100644 index 1343527444..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.sub; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.service.telemetry.TelemetryFeature; - -import java.util.Map; - -/** - * @author Andrew Shvayka - */ -@AllArgsConstructor -public class SubscriptionState { - - @Getter private final String wsSessionId; - @Getter private final int subscriptionId; - @Getter private final TenantId tenantId; - @Getter private final EntityId entityId; - @Getter private final TelemetryFeature type; - @Getter private final boolean allKeys; - @Getter private final Map keyStates; - @Getter private final String scope; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - SubscriptionState that = (SubscriptionState) o; - - if (subscriptionId != that.subscriptionId) return false; - if (wsSessionId != null ? !wsSessionId.equals(that.wsSessionId) : that.wsSessionId != null) return false; - if (entityId != null ? !entityId.equals(that.entityId) : that.entityId != null) return false; - return type == that.type; - } - - @Override - public int hashCode() { - int result = wsSessionId != null ? wsSessionId.hashCode() : 0; - result = 31 * result + subscriptionId; - result = 31 * result + (entityId != null ? entityId.hashCode() : 0); - result = 31 * result + (type != null ? type.hashCode() : 0); - return result; - } - - @Override - public String toString() { - return "SubscriptionState{" + - "type=" + type + - ", entityId=" + entityId + - ", subscriptionId=" + subscriptionId + - ", wsSessionId='" + wsSessionId + '\'' + - '}'; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/TelemetrySubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/TelemetrySubscriptionUpdate.java deleted file mode 100644 index cbbce43405..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/TelemetrySubscriptionUpdate.java +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.sub; - -import org.thingsboard.server.common.data.kv.TsKvEntry; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.stream.Collectors; - -public class TelemetrySubscriptionUpdate { - - private int subscriptionId; - private int errorCode; - private String errorMsg; - private Map> data; - - public TelemetrySubscriptionUpdate(int subscriptionId, List data) { - super(); - this.subscriptionId = subscriptionId; - this.data = new TreeMap<>(); - if (data != null) { - for (TsKvEntry tsEntry : data) { - List values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>()); - Object[] value = new Object[2]; - value[0] = tsEntry.getTs(); - value[1] = tsEntry.getValueAsString(); - values.add(value); - } - } - } - - public TelemetrySubscriptionUpdate(int subscriptionId, Map> data) { - super(); - this.subscriptionId = subscriptionId; - this.data = data; - } - - public TelemetrySubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) { - this(subscriptionId, errorCode, null); - } - - public TelemetrySubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) { - super(); - this.subscriptionId = subscriptionId; - this.errorCode = errorCode.getCode(); - this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg(); - } - - public int getSubscriptionId() { - return subscriptionId; - } - - public Map> getData() { - return data; - } - - public Map getLatestValues() { - if (data == null) { - return Collections.emptyMap(); - } else { - return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> { - List data = e.getValue(); - Object[] latest = (Object[]) data.get(data.size() - 1); - return (long) latest[0]; - })); - } - } - - public int getErrorCode() { - return errorCode; - } - - public String getErrorMsg() { - return errorMsg; - } - - @Override - public String toString() { - return "TsSubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data=" - + data + "]"; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/transport/BasicCredentialsValidationResult.java b/application/src/main/java/org/thingsboard/server/service/transport/BasicCredentialsValidationResult.java index e4c96ce653..940459571c 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/BasicCredentialsValidationResult.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/BasicCredentialsValidationResult.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java index 0228743076..b87758ace5 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index f8b42ecc1e..d0d996fe1f 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.thingsboard.server.service.transport; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -33,6 +32,7 @@ import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; @@ -65,13 +65,14 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -95,6 +96,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; @@ -108,6 +110,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH; @@ -122,12 +125,13 @@ import static org.thingsboard.server.service.transport.BasicCredentialsValidatio @RequiredArgsConstructor public class DefaultTransportApiService implements TransportApiService { - private static final ObjectMapper mapper = new ObjectMapper(); + private static final Pattern X509_CERTIFICATE_TRIM_CHAIN_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----"); private final TbDeviceProfileCache deviceProfileCache; private final TbTenantProfileCache tenantProfileCache; private final TbApiUsageStateService apiUsageStateService; private final DeviceService deviceService; + private final DeviceProfileService deviceProfileService; private final RelationService relationService; private final DeviceCredentialsService deviceCredentialsService; private final DbCallbackExecutorService dbCallbackExecutorService; @@ -159,6 +163,9 @@ public class DefaultTransportApiService implements TransportApiService { } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE); + } else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) { + TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg(); + result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain()); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { @@ -226,6 +233,35 @@ public class DefaultTransportApiService implements TransportApiService { } } + protected ListenableFuture validateOrCreateDeviceX509Certificate(String certificateChain) { + List chain = X509_CERTIFICATE_TRIM_CHAIN_PATTERN.matcher(certificateChain).results().map(match -> + EncryptionUtil.certTrimNewLines(match.group())).collect(Collectors.toList()); + for (String certificateValue : chain) { + String certificateHash = EncryptionUtil.getSha3Hash(certificateValue); + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash); + if (credentials != null && DeviceCredentialsType.X509_CERTIFICATE.equals(credentials.getCredentialsType())) { + return getDeviceInfo(credentials); + } + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(certificateHash); + if (deviceProfile != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType())) { + String updatedDeviceProvisionSecret = chain.get(0); + ProvisionRequest provisionRequest = createProvisionRequest(updatedDeviceProvisionSecret); + try { + ProvisionResponse provisionResponse = deviceProvisionService.provisionDeviceViaX509Chain(deviceProfile, provisionRequest); + if (ProvisionResponseStatus.SUCCESS.equals(provisionResponse.getResponseStatus())) { + return getDeviceInfo(provisionResponse.getDeviceCredentials()); + } + } catch (ProvisionFailedException e) { + log.debug("[{}][{}] Failed to provision device with cert chain: {}", deviceProfile.getTenantId(), deviceProfile.getId(), provisionRequest, e); + return getEmptyTransportApiResponseFuture(); + } + } else if (deviceProfile != null) { + log.warn("[{}][{}] Device Profile provision configuration mismatched: expected {}, actual {}", deviceProfile.getTenantId(), deviceProfile.getId(), DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN, deviceProfile.getProvisionType()); + } + } + return getEmptyTransportApiResponseFuture(); + } + private ListenableFuture validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) { DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); if (credentials != null) { @@ -290,6 +326,7 @@ public class DefaultTransportApiService implements TransportApiService { device.setType(requestMsg.getDeviceType()); device.setCustomerId(gateway.getCustomerId()); DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); + device.setDeviceProfileId(deviceProfile.getId()); ObjectNode additionalInfo = JacksonUtil.newObjectNode(); additionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString()); @@ -308,8 +345,8 @@ public class DefaultTransportApiService implements TransportApiService { metaData.putValue("gatewayId", gatewayId.toString()); DeviceId deviceId = device.getId(); - ObjectNode entityNode = mapper.valueToTree(device); - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, mapper.writeValueAsString(entityNode)); + JsonNode entityNode = JacksonUtil.valueToTree(device); + TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null); } else { JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); @@ -519,7 +556,7 @@ public class DefaultTransportApiService implements TransportApiService { .setDeviceType(device.getType()) .setDeviceProfileIdMSB(device.getDeviceProfileId().getId().getMostSignificantBits()) .setDeviceProfileIdLSB(device.getDeviceProfileId().getId().getLeastSignificantBits()) - .setAdditionalInfo(mapper.writeValueAsString(device.getAdditionalInfo())); + .setAdditionalInfo(JacksonUtil.toString(device.getAdditionalInfo())); PowerSavingConfiguration psmConfiguration = null; switch (device.getDeviceData().getTransportConfiguration().getType()) { @@ -664,4 +701,11 @@ public class DefaultTransportApiService implements TransportApiService { private Long checkLong(Long l) { return l != null ? l : 0; } + + private ProvisionRequest createProvisionRequest(String certificateValue) { + return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE, + new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue), + null); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreToTransportService.java b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreToTransportService.java index 5f37758f58..ad4e09a1d6 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreToTransportService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreToTransportService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java index 323ca2528c..a2afe6b433 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,27 +18,25 @@ package org.thingsboard.server.service.transport; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueResponseTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueResponseTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.concurrent.*; +import java.util.concurrent.ExecutorService; /** * Created by ashvayka on 05.10.18. diff --git a/application/src/main/java/org/thingsboard/server/service/transport/TransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/TransportApiService.java index debb0ec3f1..4d26d174f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/TransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/TransportApiService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,10 @@ */ package org.thingsboard.server.service.transport; -import org.thingsboard.server.queue.TbQueueHandler; -import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; +import org.thingsboard.server.queue.TbQueueHandler; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; /** * Created by ashvayka on 05.10.18. diff --git a/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java b/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java index 6fa0e58a6f..a976f4ed52 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,8 @@ import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.TenantAwareMsg; -import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import java.io.Serializable; import java.util.UUID; diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java index c01b894fda..01295bda32 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,19 +17,10 @@ package org.thingsboard.server.service.ttl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.queue.discovery.PartitionService; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.SQLWarning; -import java.sql.Statement; - @Slf4j @RequiredArgsConstructor diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java index e178055706..5154829ea6 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.queue.ServiceType; @@ -60,46 +61,48 @@ public class AlarmsCleanUpService { @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.alarms.checking_interval})}", fixedDelayString = "${sql.ttl.alarms.checking_interval}") public void cleanUp() { - PageLink tenantsBatchRequest = new PageLink(10_000, 0); - PageLink removalBatchRequest = new PageLink(removalBatchSize, 0 ); - PageData tenantsIds; - do { - tenantsIds = tenantService.findTenantsIds(tenantsBatchRequest); - for (TenantId tenantId : tenantsIds.getData()) { - if (!partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) { - continue; - } - - Optional tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration(); - if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getAlarmsTtlDays() == 0) { - continue; - } + PageDataIterable tenants = new PageDataIterable<>(tenantService::findTenantsIds, 10_000); + for (TenantId tenantId : tenants) { + try { + cleanUp(tenantId); + } catch (Exception e) { + log.warn("Failed to clean up alarms by ttl for tenant {}", tenantId, e); + } + } + } - long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getAlarmsTtlDays()); - long expirationTime = System.currentTimeMillis() - ttl; + private void cleanUp(TenantId tenantId) { + if (!partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) { + return; + } - long totalRemoved = 0; - while (true) { - PageData toRemove = alarmDao.findAlarmsIdsByEndTsBeforeAndTenantId(expirationTime, tenantId, removalBatchRequest); - toRemove.getData().forEach(alarmId -> { - relationService.deleteEntityRelations(tenantId, alarmId); - Alarm alarm = alarmService.deleteAlarm(tenantId, alarmId).getAlarm(); - entityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null); - }); + Optional tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration(); + if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getAlarmsTtlDays() == 0) { + return; + } - totalRemoved += toRemove.getTotalElements(); - if (!toRemove.hasNext()) { - break; - } - } + long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getAlarmsTtlDays()); + long expirationTime = System.currentTimeMillis() - ttl; - if (totalRemoved > 0) { - log.info("Removed {} outdated alarm(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime)); + PageLink removalBatchRequest = new PageLink(removalBatchSize, 0); + long totalRemoved = 0; + while (true) { + PageData toRemove = alarmDao.findAlarmsIdsByEndTsBeforeAndTenantId(expirationTime, tenantId, removalBatchRequest); + for (AlarmId alarmId : toRemove.getData()) { + relationService.deleteEntityRelations(tenantId, alarmId); + Alarm alarm = alarmService.delAlarm(tenantId, alarmId).getAlarm(); + if (alarm != null) { + entityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null); + totalRemoved++; } } - - tenantsBatchRequest = tenantsBatchRequest.nextPageLink(); - } while (tenantsIds.hasNext()); + if (!toRemove.hasNext()) { + break; + } + } + if (totalRemoved > 0) { + log.info("Removed {} outdated alarm(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime)); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AuditLogsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AuditLogsCleanUpService.java index 11b1751c0d..d040676c4c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AuditLogsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AuditLogsCleanUpService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import java.util.concurrent.TimeUnit; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_TABLE_NAME; @Service @ConditionalOnExpression("${sql.ttl.audit_logs.enabled:true} && ${sql.ttl.audit_logs.ttl:0} > 0") @@ -54,7 +54,7 @@ public class AuditLogsCleanUpService extends AbstractCleanUpService { if (isSystemTenantPartitionMine()) { auditLogDao.cleanUpAuditLogs(auditLogsExpTime); } else { - partitioningRepository.cleanupPartitionsCache(AUDIT_LOG_COLUMN_FAMILY_NAME, auditLogsExpTime, TimeUnit.HOURS.toMillis(partitionSizeInHours)); + partitioningRepository.cleanupPartitionsCache(AUDIT_LOG_TABLE_NAME, auditLogsExpTime, TimeUnit.HOURS.toMillis(partitionSizeInHours)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java index a56b1963d6..aaac7cea55 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,12 +27,12 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.concurrent.TimeUnit; -import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TABLE_NAME; @TbCoreComponent @Slf4j @Service -@ConditionalOnExpression("${sql.ttl.edge_events.enabled:true} && ${sql.ttl.edge_events.edge_event_ttl:0} > 0") +@ConditionalOnExpression("${sql.ttl.edge_events.enabled:true} && ${sql.ttl.edge_events.edge_events_ttl:0} > 0") public class EdgeEventsCleanUpService extends AbstractCleanUpService { public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION = @@ -63,7 +63,7 @@ public class EdgeEventsCleanUpService extends AbstractCleanUpService { if (ttlTaskExecutionEnabled && isSystemTenantPartitionMine()) { edgeEventService.cleanupEvents(edgeEventsExpTime); } else { - partitioningRepository.cleanupPartitionsCache(EDGE_EVENT_COLUMN_FAMILY_NAME, edgeEventsExpTime, TimeUnit.HOURS.toMillis(partitionSizeInHours)); + partitioningRepository.cleanupPartitionsCache(EDGE_EVENT_TABLE_NAME, edgeEventsExpTime, TimeUnit.HOURS.toMillis(partitionSizeInHours)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java index 3dc66b6210..0f6bbcf83a 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.concurrent.TimeUnit; diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java new file mode 100644 index 0000000000..65b04ef0ab --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ttl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.notification.NotificationRequestConfig; +import org.thingsboard.server.dao.notification.NotificationRequestDao; +import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; +import org.thingsboard.server.queue.discovery.PartitionService; + +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.server.dao.model.ModelConstants.NOTIFICATION_TABLE_NAME; + +@Service +@ConditionalOnExpression("${sql.ttl.notifications.enabled:true} && ${sql.ttl.notifications.ttl:0} > 0") +@Slf4j +public class NotificationsCleanUpService extends AbstractCleanUpService { + + private final SqlPartitioningRepository partitioningRepository; + private final NotificationRequestDao notificationRequestDao; + + @Value("${sql.ttl.notifications.ttl:2592000}") + private long ttlInSec; + @Value("${sql.notifications.partition_size:168}") + private int partitionSizeInHours; + + public NotificationsCleanUpService(PartitionService partitionService, SqlPartitioningRepository partitioningRepository, + NotificationRequestDao notificationRequestDao) { + super(partitionService); + this.partitioningRepository = partitioningRepository; + this.notificationRequestDao = notificationRequestDao; + } + + @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.notifications.checking_interval_ms:86400000})}", + fixedDelayString = "${sql.ttl.notifications.checking_interval_ms:86400000}") + public void cleanUp() { + long expTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttlInSec); + long partitionDurationMs = TimeUnit.HOURS.toMillis(partitionSizeInHours); + if (!isSystemTenantPartitionMine()) { + partitioningRepository.cleanupPartitionsCache(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs); + return; + } + + long lastRemovedNotificationTs = partitioningRepository.dropPartitionsBefore(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs); + if (lastRemovedNotificationTs > 0) { + long gap = TimeUnit.MINUTES.toMillis(10); + long requestExpTime = lastRemovedNotificationTs - TimeUnit.SECONDS.toMillis(NotificationRequestConfig.MAX_SENDING_DELAY) - gap; + // TODO: double-check this + notificationRequestDao.removeAllByCreatedTimeBefore(requestExpTime); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/TimeseriesCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/TimeseriesCleanUpService.java index 00b869dede..86b69dbb07 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/TimeseriesCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/TimeseriesCleanUpService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.ttl.AbstractCleanUpService; @TbCoreComponent @Slf4j diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java index 1c21cc6029..7db4585acd 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index bc1e04f277..ef066a73f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,24 @@ */ package org.thingsboard.server.service.update; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.info.BuildProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.UpdateMessage; +import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; +import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; -import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.IOException; import java.nio.file.Files; @@ -53,10 +59,16 @@ public class DefaultUpdateService implements UpdateService { @Value("${updates.enabled}") private boolean updatesEnabled; + @Autowired(required = false) + private BuildProperties buildProperties; + + @Autowired + private NotificationRuleProcessor notificationRuleProcessor; + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("tb-update-service")); - private ScheduledFuture checkUpdatesFuture = null; - private RestTemplate restClient = new RestTemplate(); + private ScheduledFuture checkUpdatesFuture = null; + private final RestTemplate restClient = new RestTemplate(); private UpdateMessage updateMessage; @@ -64,16 +76,15 @@ public class DefaultUpdateService implements UpdateService { private String version; private UUID instanceId = null; - @PostConstruct - private void init() { - updateMessage = new UpdateMessage("", false); + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void init() { + version = buildProperties != null ? buildProperties.getVersion() : "unknown"; + updateMessage = new UpdateMessage(false, version, "", "", + "https://thingsboard.io/docs/reference/releases", + "https://thingsboard.io/docs/reference/releases"); if (updatesEnabled) { try { platform = System.getProperty("platform", "unknown"); - version = getClass().getPackage().getImplementationVersion(); - if (version == null) { - version = "unknown"; - } instanceId = parseInstanceId(); checkUpdatesFuture = scheduler.scheduleAtFixedRate(checkUpdatesRunnable, 0, 1, TimeUnit.HOURS); } catch (Exception e) { @@ -87,7 +98,7 @@ public class DefaultUpdateService implements UpdateService { Path instanceIdPath = Paths.get(INSTANCE_ID_FILE); if (instanceIdPath.toFile().exists()) { byte[] data = Files.readAllBytes(instanceIdPath); - if (data != null && data.length > 0) { + if (data.length > 0) { try { result = UUID.fromString(new String(data)); } catch (IllegalArgumentException e) { @@ -117,15 +128,19 @@ public class DefaultUpdateService implements UpdateService { Runnable checkUpdatesRunnable = () -> { try { log.trace("Executing check update method for instanceId [{}], platform [{}] and version [{}]", instanceId, platform, version); - ObjectNode request = new ObjectMapper().createObjectNode(); + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + ObjectNode request = JacksonUtil.newObjectNode(); request.put(PLATFORM_PARAM, platform); request.put(VERSION_PARAM, version); request.put(INSTANCE_ID_PARAM, instanceId.toString()); - JsonNode response = restClient.postForObject(UPDATE_SERVER_BASE_URL+"/api/thingsboard/updates", request, JsonNode.class); - updateMessage = new UpdateMessage( - response.get("message").asText(), - response.get("updateAvailable").asBoolean() - ); + UpdateMessage prevUpdateMessage = updateMessage; + updateMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v2/thingsboard/updates", new HttpEntity<>(request.toString(), headers), UpdateMessage.class); + if (updateMessage != null && updateMessage.isUpdateAvailable() && !updateMessage.equals(prevUpdateMessage)) { + notificationRuleProcessor.process(NewPlatformVersionTrigger.builder() + .updateInfo(updateMessage) + .build()); + } } catch (Exception e) { log.trace(e.getMessage()); } diff --git a/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java index fce051dfde..370634564c 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java new file mode 100644 index 0000000000..21d69d6b42 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -0,0 +1,1091 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.base.Function; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.socket.CloseStatus; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.util.TenantRateLimitException; +import org.thingsboard.server.exception.UnauthorizedException; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.AccessValidator; +import org.thingsboard.server.service.security.ValidationCallback; +import org.thingsboard.server.service.security.ValidationResult; +import org.thingsboard.server.service.security.ValidationResultCode; +import org.thingsboard.server.service.security.model.UserPrincipal; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; +import org.thingsboard.server.service.subscription.TbAttributeSubscription; +import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; +import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService; +import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; +import org.thingsboard.server.service.subscription.TbTimeseriesSubscription; +import org.thingsboard.server.service.ws.notification.NotificationCommandsHandler; +import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper; +import org.thingsboard.server.service.ws.notification.cmd.WsCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryPluginCmdsWrapper; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.SubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.TelemetryPluginCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Created by ashvayka on 27.03.18. + */ +@Service +@TbCoreComponent +@Slf4j +@RequiredArgsConstructor +public class DefaultWebSocketService implements WebSocketService { + + public static final int NUMBER_OF_PING_ATTEMPTS = 3; + + private static final int DEFAULT_LIMIT = 100; + private static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE; + private static final int UNKNOWN_SUBSCRIPTION_ID = 0; + private static final String PROCESSING_MSG = "[{}] Processing: {}"; + private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; + private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; + private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; + private static final String FAILED_TO_PARSE_WS_COMMAND = "Failed to parse websocket command!"; + + private final ConcurrentMap wsSessionsMap = new ConcurrentHashMap<>(); + + private final TbLocalSubscriptionService oldSubService; + private final TbEntityDataSubscriptionService entityDataSubService; + private final NotificationCommandsHandler notificationCmdsHandler; + private final WebSocketMsgEndpoint msgEndpoint; + private final AccessValidator accessValidator; + private final AttributesService attributesService; + private final TimeseriesService tsService; + private final TbServiceInfoProvider serviceInfoProvider; + private final TbTenantProfileCache tenantProfileCache; + + @Value("${server.ws.ping_timeout:30000}") + private long pingTimeout; + + private final ConcurrentMap> tenantSubscriptionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> customerSubscriptionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> regularUserSubscriptionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> publicUserSubscriptionsMap = new ConcurrentHashMap<>(); + + private ExecutorService executor; + private ScheduledExecutorService pingExecutor; + private String serviceId; + + private List> telemetryCmdsHandlers; + private List> notificationCmdsHandlers; + + @PostConstruct + public void init() { + serviceId = serviceInfoProvider.getServiceId(); + executor = ThingsBoardExecutors.newWorkStealingPool(50, getClass()); + + pingExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("telemetry-web-socket-ping")); + pingExecutor.scheduleWithFixedDelay(this::sendPing, pingTimeout / NUMBER_OF_PING_ATTEMPTS, pingTimeout / NUMBER_OF_PING_ATTEMPTS, TimeUnit.MILLISECONDS); + + telemetryCmdsHandlers = List.of( + newCmdsHandler(TelemetryPluginCmdsWrapper::getAttrSubCmds, this::handleWsAttributesSubscriptionCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getTsSubCmds, this::handleWsTimeseriesSubscriptionCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getHistoryCmds, this::handleWsHistoryCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataCmds, this::handleWsEntityDataCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataCmds, this::handleWsAlarmDataCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountCmds, this::handleWsEntityCountCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmCountCmds, this::handleWsAlarmCountCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd) + ); + notificationCmdsHandlers = List.of( + newCmdHandler(NotificationCmdsWrapper::getUnreadSubCmd, notificationCmdsHandler::handleUnreadNotificationsSubCmd), + newCmdHandler(NotificationCmdsWrapper::getUnreadCountSubCmd, notificationCmdsHandler::handleUnreadNotificationsCountSubCmd), + newCmdHandler(NotificationCmdsWrapper::getMarkAsReadCmd, notificationCmdsHandler::handleMarkAsReadCmd), + newCmdHandler(NotificationCmdsWrapper::getMarkAllAsReadCmd, notificationCmdsHandler::handleMarkAllAsReadCmd), + newCmdHandler(NotificationCmdsWrapper::getUnsubCmd, notificationCmdsHandler::handleUnsubCmd) + ); + } + + @PreDestroy + public void shutdownExecutor() { + if (pingExecutor != null) { + pingExecutor.shutdownNow(); + } + + if (executor != null) { + executor.shutdownNow(); + } + } + + @Override + public void handleWebSocketSessionEvent(WebSocketSessionRef sessionRef, SessionEvent event) { + String sessionId = sessionRef.getSessionId(); + log.debug(PROCESSING_MSG, sessionId, event); + switch (event.getEventType()) { + case ESTABLISHED: + wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef)); + break; + case ERROR: + log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null)); + break; + case CLOSED: + wsSessionsMap.remove(sessionId); + oldSubService.cancelAllSessionSubscriptions(sessionId); + entityDataSubService.cancelAllSessionSubscriptions(sessionId); + processSessionClose(sessionRef); + break; + } + } + + @Override + public void handleWebSocketMsg(WebSocketSessionRef sessionRef, String msg) { + if (log.isTraceEnabled()) { + log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg); + } + + try { + switch (sessionRef.getSessionType()) { + case TELEMETRY: + processTelemetryCmds(sessionRef, msg); + break; + case NOTIFICATIONS: + processNotificationCmds(sessionRef, msg); + break; + } + } catch (IOException e) { + log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.BAD_REQUEST, FAILED_TO_PARSE_WS_COMMAND)); + } + } + + private void processTelemetryCmds(WebSocketSessionRef sessionRef, String msg) throws JsonProcessingException { + TelemetryPluginCmdsWrapper cmdsWrapper = JacksonUtil.fromString(msg, TelemetryPluginCmdsWrapper.class); + if (cmdsWrapper == null) { + return; + } + for (WsCmdListHandler cmdHandler : telemetryCmdsHandlers) { + List cmds = cmdHandler.extractCmds(cmdsWrapper); + if (cmds != null) { + cmdHandler.handle(sessionRef, cmds); + } + } + } + + private void processNotificationCmds(WebSocketSessionRef sessionRef, String msg) throws IOException { + NotificationCmdsWrapper cmdsWrapper = JacksonUtil.fromString(msg, NotificationCmdsWrapper.class); + for (WsCmdHandler cmdHandler : notificationCmdsHandlers) { + WsCmd cmd = cmdHandler.extractCmd(cmdsWrapper); + if (cmd != null) { + String sessionId = sessionRef.getSessionId(); + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) { + try { + cmdHandler.handle(sessionRef, cmd); + } catch (Exception e) { + log.error("[sessionId: {}, tenantId: {}, userId: {}] Failed to handle WS cmd: {}", sessionId, + sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), cmd, e); + } + } + } + } + } + + private void handleWsEntityDataCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) + && validateSubscriptionCmd(sessionRef, cmd)) { + entityDataSubService.handleCmd(sessionRef, cmd); + } + } + + private void handleWsEntityCountCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) + && validateSubscriptionCmd(sessionRef, cmd)) { + entityDataSubService.handleCmd(sessionRef, cmd); + } + } + + private void handleWsAlarmDataCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) + && validateSubscriptionCmd(sessionRef, cmd)) { + entityDataSubService.handleCmd(sessionRef, cmd); + } + } + + private void handleWsDataUnsubscribeCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) { + entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd); + } + } + + private void handleWsAlarmCountCmd(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) + && validateSubscriptionCmd(sessionRef, cmd)) { + entityDataSubService.handleCmd(sessionRef, cmd); + } + } + + @Override + public void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update) { + sendWsMsg(sessionId, update.getSubscriptionId(), update); + } + + @Override + public void sendWsMsg(String sessionId, CmdUpdate update) { + sendWsMsg(sessionId, update.getCmdId(), update); + } + + private void sendWsMsg(String sessionId, int cmdId, T update) { + WsSessionMetaData md = wsSessionsMap.get(sessionId); + if (md != null) { + sendWsMsg(md.getSessionRef(), cmdId, update); + } + } + + @Override + public void close(String sessionId, CloseStatus status) { + WsSessionMetaData md = wsSessionsMap.get(sessionId); + if (md != null) { + try { + msgEndpoint.close(md.getSessionRef(), status); + } catch (IOException e) { + log.warn("[{}] Failed to send session close: {}", sessionId, e); + } + } + } + + private void processSessionClose(WebSocketSessionRef sessionRef) { + var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); + if (tenantProfileConfiguration != null) { + String sessionId = "[" + sessionRef.getSessionId() + "]"; + + if (tenantProfileConfiguration.getMaxWsSubscriptionsPerTenant() > 0) { + Set tenantSubscriptions = tenantSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); + synchronized (tenantSubscriptions) { + tenantSubscriptions.removeIf(subId -> subId.startsWith(sessionId)); + } + } + if (sessionRef.getSecurityCtx().isCustomerUser()) { + if (tenantProfileConfiguration.getMaxWsSubscriptionsPerCustomer() > 0) { + Set customerSessions = customerSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet()); + synchronized (customerSessions) { + customerSessions.removeIf(subId -> subId.startsWith(sessionId)); + } + } + if (tenantProfileConfiguration.getMaxWsSubscriptionsPerRegularUser() > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { + Set regularUserSessions = regularUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); + synchronized (regularUserSessions) { + regularUserSessions.removeIf(subId -> subId.startsWith(sessionId)); + } + } + if (tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { + Set publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); + synchronized (publicUserSessions) { + publicUserSessions.removeIf(subId -> subId.startsWith(sessionId)); + } + } + } + } + } + + private boolean processSubscription(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) { + var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); + if (tenantProfileConfiguration == null) return true; + + String subId = "[" + sessionRef.getSessionId() + "]:[" + cmd.getCmdId() + "]"; + try { + if (tenantProfileConfiguration.getMaxWsSubscriptionsPerTenant() > 0) { + Set tenantSubscriptions = tenantSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); + synchronized (tenantSubscriptions) { + if (cmd.isUnsubscribe()) { + tenantSubscriptions.remove(subId); + } else if (tenantSubscriptions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerTenant()) { + tenantSubscriptions.add(subId); + } else { + log.info("[{}][{}][{}] Failed to start subscription. Max tenant subscriptions limit reached" + , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId); + msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max tenant subscriptions limit reached!")); + return false; + } + } + } + + if (sessionRef.getSecurityCtx().isCustomerUser()) { + if (tenantProfileConfiguration.getMaxWsSubscriptionsPerCustomer() > 0) { + Set customerSessions = customerSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet()); + synchronized (customerSessions) { + if (cmd.isUnsubscribe()) { + customerSessions.remove(subId); + } else if (customerSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerCustomer()) { + customerSessions.add(subId); + } else { + log.info("[{}][{}][{}] Failed to start subscription. Max customer subscriptions limit reached" + , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId); + msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max customer subscriptions limit reached")); + return false; + } + } + } + if (tenantProfileConfiguration.getMaxWsSubscriptionsPerRegularUser() > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { + Set regularUserSessions = regularUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); + synchronized (regularUserSessions) { + if (regularUserSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerRegularUser()) { + regularUserSessions.add(subId); + } else { + log.info("[{}][{}][{}] Failed to start subscription. Max regular user subscriptions limit reached" + , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId); + msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max regular user subscriptions limit reached")); + return false; + } + } + } + if (tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { + Set publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); + synchronized (publicUserSessions) { + if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser()) { + publicUserSessions.add(subId); + } else { + log.info("[{}][{}][{}] Failed to start subscription. Max public user subscriptions limit reached" + , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId); + msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max public user subscriptions limit reached")); + return false; + } + } + } + } + } catch (IOException e) { + log.warn("[{}] Failed to send session close: {}", sessionRef.getSessionId(), e); + return false; + } + return true; + } + + private void handleWsAttributesSubscriptionCmd(WebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) { + if (!processSubscription(sessionRef, cmd)) { + return; + } + + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd, sessionId)) { + if (cmd.isUnsubscribe()) { + unsubscribe(sessionRef, cmd, sessionId); + } else if (validateSubscriptionCmd(sessionRef, cmd)) { + EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); + log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId); + Optional> keysOptional = getKeys(cmd); + if (keysOptional.isPresent()) { + List keys = new ArrayList<>(keysOptional.get()); + handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys); + } else { + handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId); + } + } + } + } + + private void handleWsAttributesSubscriptionByKeys(WebSocketSessionRef sessionRef, + AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId, + List keys) { + FutureCallback> callback = new FutureCallback<>() { + @Override + public void onSuccess(List data) { + List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); + + Map subState = new HashMap<>(keys.size()); + keys.forEach(key -> subState.put(key, 0L)); + attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); + + TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope()); + + Lock subLock = new ReentrantLock(); + TbAttributeSubscription sub = TbAttributeSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionId) + .subscriptionId(cmd.getCmdId()) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .allKeys(false) + .keyStates(subState) + .scope(scope) + .updateProcessor((subscription, update) -> { + subLock.lock(); + try { + sendWsMsg(subscription.getSessionId(), update); + } finally { + subLock.unlock(); + } + }) + .build(); + + subLock.lock(); + try{ + oldSubService.addSubscription(sub); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); + } finally { + subLock.unlock(); + } + + } + + @Override + public void onFailure(Throwable e) { + log.error(FAILED_TO_FETCH_ATTRIBUTES, e); + TelemetrySubscriptionUpdate update; + if (e instanceof UnauthorizedException) { + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, + SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); + } else { + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + FAILED_TO_FETCH_ATTRIBUTES); + } + sendWsMsg(sessionRef, update); + } + }; + + if (StringUtils.isEmpty(cmd.getScope())) { + accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_ATTRIBUTES, entityId, getAttributesFetchCallback(sessionRef.getSecurityCtx().getTenantId(), entityId, keys, callback)); + } else { + accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_ATTRIBUTES, entityId, getAttributesFetchCallback(sessionRef.getSecurityCtx().getTenantId(), entityId, cmd.getScope(), keys, callback)); + } + } + + private void handleWsHistoryCmd(WebSocketSessionRef sessionRef, GetHistoryCmd cmd) { + String sessionId = sessionRef.getSessionId(); + WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); + if (sessionMD == null) { + log.warn("[{}] Session meta data not found. ", sessionId); + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + SESSION_META_DATA_NOT_FOUND); + sendWsMsg(sessionRef, update); + return; + } + if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Device id is empty!"); + sendWsMsg(sessionRef, update); + return; + } + if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Keys are empty!"); + sendWsMsg(sessionRef, update); + return; + } + EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); + List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); + List queries = keys.stream().map(key -> new BaseReadTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))) + .collect(Collectors.toList()); + + FutureCallback> callback = new FutureCallback>() { + @Override + public void onSuccess(List data) { + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); + } + + @Override + public void onFailure(Throwable e) { + TelemetrySubscriptionUpdate update; + if (UnauthorizedException.class.isInstance(e)) { + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, + SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); + } else { + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + FAILED_TO_FETCH_DATA); + } + sendWsMsg(sessionRef, update); + } + }; + accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, + on(r -> Futures.addCallback(tsService.findAll(sessionRef.getSecurityCtx().getTenantId(), entityId, queries), callback, executor), callback::onFailure)); + } + + private void handleWsAttributesSubscription(WebSocketSessionRef sessionRef, + AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) { + FutureCallback> callback = new FutureCallback<>() { + @Override + public void onSuccess(List data) { + List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); + + Map subState = new HashMap<>(attributesData.size()); + attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); + + TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope()); + + Lock subLock = new ReentrantLock(); + TbAttributeSubscription sub = TbAttributeSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionId) + .subscriptionId(cmd.getCmdId()) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .allKeys(true) + .keyStates(subState) + .updateProcessor((subscription, update) -> { + subLock.lock(); + try { + sendWsMsg(subscription.getSessionId(), update); + } finally { + subLock.unlock(); + } + }) + .scope(scope) + .build(); + + subLock.lock(); + try { + oldSubService.addSubscription(sub); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); + } finally { + subLock.unlock(); + } + } + + @Override + public void onFailure(Throwable e) { + log.error(FAILED_TO_FETCH_ATTRIBUTES, e); + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + FAILED_TO_FETCH_ATTRIBUTES); + sendWsMsg(sessionRef, update); + } + }; + + + if (StringUtils.isEmpty(cmd.getScope())) { + accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_ATTRIBUTES, entityId, getAttributesFetchCallback(sessionRef.getSecurityCtx().getTenantId(), entityId, callback)); + } else { + accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_ATTRIBUTES, entityId, getAttributesFetchCallback(sessionRef.getSecurityCtx().getTenantId(), entityId, cmd.getScope(), callback)); + } + } + + private void handleWsTimeseriesSubscriptionCmd(WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) { + if (!processSubscription(sessionRef, cmd)) { + return; + } + + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd, sessionId)) { + if (cmd.isUnsubscribe()) { + unsubscribe(sessionRef, cmd, sessionId); + } else if (validateSubscriptionCmd(sessionRef, cmd)) { + EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); + Optional> keysOptional = getKeys(cmd); + + if (keysOptional.isPresent()) { + handleWsTimeseriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId); + } else { + handleWsTimeseriesSubscription(sessionRef, cmd, sessionId, entityId); + } + } + } + } + + private void handleWsTimeseriesSubscriptionByKeys(WebSocketSessionRef sessionRef, + TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) { + long startTs; + if (cmd.getTimeWindow() > 0) { + List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); + log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), entityId); + startTs = cmd.getStartTs(); + long endTs = cmd.getStartTs() + cmd.getTimeWindow(); + List queries = keys.stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, cmd.getInterval(), + getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); + + final FutureCallback> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys); + accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, + on(r -> Futures.addCallback(tsService.findAll(sessionRef.getSecurityCtx().getTenantId(), entityId, queries), callback, executor), callback::onFailure)); + } else { + List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); + startTs = System.currentTimeMillis(); + log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), entityId); + final FutureCallback> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys); + accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, + on(r -> Futures.addCallback(tsService.findLatest(sessionRef.getSecurityCtx().getTenantId(), entityId, keys), callback, executor), callback::onFailure)); + } + } + + private void handleWsTimeseriesSubscription(WebSocketSessionRef sessionRef, + TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) { + FutureCallback> callback = new FutureCallback>() { + @Override + public void onSuccess(List data) { + Map subState = new HashMap<>(data.size()); + data.forEach(v -> subState.put(v.getKey(), v.getTs())); + + Lock subLock = new ReentrantLock(); + TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionId) + .subscriptionId(cmd.getCmdId()) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .updateProcessor((subscription, update) -> { + subLock.lock(); + try { + sendWsMsg(subscription.getSessionId(), update); + } finally { + subLock.unlock(); + } + }) + .allKeys(true) + .keyStates(subState) + .build(); + + subLock.lock(); + try { + oldSubService.addSubscription(sub); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); + } finally { + subLock.unlock(); + } + } + + @Override + public void onFailure(Throwable e) { + TelemetrySubscriptionUpdate update; + if (UnauthorizedException.class.isInstance(e)) { + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, + SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); + } else { + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + FAILED_TO_FETCH_DATA); + } + sendWsMsg(sessionRef, update); + } + }; + accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, + on(r -> Futures.addCallback(tsService.findAllLatest(sessionRef.getSecurityCtx().getTenantId(), entityId), callback, executor), callback::onFailure)); + } + + private FutureCallback> getSubscriptionCallback(final WebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List keys) { + return new FutureCallback<>() { + @Override + public void onSuccess(List data) { + Map subState = new HashMap<>(keys.size()); + keys.forEach(key -> subState.put(key, startTs)); + data.forEach(v -> subState.put(v.getKey(), v.getTs())); + + Lock subLock = new ReentrantLock(); + TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionId) + .subscriptionId(cmd.getCmdId()) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .updateProcessor((subscription, update) -> { + subLock.lock(); + try { + sendWsMsg(subscription.getSessionId(), update); + } finally { + subLock.unlock(); + } + }) + .allKeys(false) + .keyStates(subState) + .build(); + + subLock.lock(); + try{ + oldSubService.addSubscription(sub); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); + } finally { + subLock.unlock(); + } + } + + @Override + public void onFailure(Throwable e) { + if (e instanceof TenantRateLimitException || e.getCause() instanceof TenantRateLimitException) { + log.trace("[{}] Tenant rate limit detected for subscription: [{}]:{}", sessionRef.getSecurityCtx().getTenantId(), entityId, cmd); + } else { + log.info(FAILED_TO_FETCH_DATA, e); + } + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + FAILED_TO_FETCH_DATA); + sendWsMsg(sessionRef, update); + } + }; + } + + private void unsubscribe(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { + if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { + oldSubService.cancelAllSessionSubscriptions(sessionId); + } else { + oldSubService.cancelSubscription(sessionId, cmd.getCmdId()); + } + } + + private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) { + if (cmd.getCmdId() < 0) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Cmd id is negative value!"); + sendWsMsg(sessionRef, update); + return false; + } else if (cmd.getQuery() == null && !cmd.hasAnyCmd()) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Query is empty!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + + private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) { + if (cmd.getCmdId() < 0) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Cmd id is negative value!"); + sendWsMsg(sessionRef, update); + return false; + } else if (cmd.getQuery() == null) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Query is empty!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + + private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) { + if (cmd.getCmdId() < 0) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Cmd id is negative value!"); + sendWsMsg(sessionRef, update); + return false; + } else if (cmd.getQuery() == null) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Query is empty!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + + private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) { + if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Device id is empty!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + + private boolean validateSessionMetadata(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { + return validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId); + } + + private boolean validateSessionMetadata(WebSocketSessionRef sessionRef, int cmdId, String sessionId) { + WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); + if (sessionMD == null) { + log.warn("[{}] Session meta data not found. ", sessionId); + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmdId, SubscriptionErrorCode.INTERNAL_ERROR, + SESSION_META_DATA_NOT_FOUND); + sendWsMsg(sessionRef, update); + return false; + } else { + return true; + } + } + + private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { + if (cmd.getCmdId() < 0) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Cmd id is negative value!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + + private void sendWsMsg(WebSocketSessionRef sessionRef, EntityDataUpdate update) { + sendWsMsg(sessionRef, update.getCmdId(), update); + } + + private void sendWsMsg(WebSocketSessionRef sessionRef, TelemetrySubscriptionUpdate update) { + sendWsMsg(sessionRef, update.getSubscriptionId(), update); + } + + private void sendWsMsg(WebSocketSessionRef sessionRef, int cmdId, Object update) { + try { + String msg = JacksonUtil.OBJECT_MAPPER.writeValueAsString(update); + executor.submit(() -> { + try { + msgEndpoint.send(sessionRef, cmdId, msg); + } catch (IOException e) { + log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e); + } + }); + } catch (JsonProcessingException e) { + log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); + } + } + + private void sendPing() { + long currentTime = System.currentTimeMillis(); + wsSessionsMap.values().forEach(md -> + executor.submit(() -> { + try { + msgEndpoint.sendPing(md.getSessionRef(), currentTime); + } catch (IOException e) { + log.warn("[{}] Failed to send ping: {}", md.getSessionRef().getSessionId(), e); + } + })); + } + + private static Optional> getKeys(TelemetryPluginCmd cmd) { + if (!StringUtils.isEmpty(cmd.getKeys())) { + Set keys = new HashSet<>(); + Collections.addAll(keys, cmd.getKeys().split(",")); + return Optional.of(keys); + } else { + return Optional.empty(); + } + } + + private ListenableFuture> mergeAllAttributesFutures(List>> futures) { + return Futures.transform(Futures.successfulAsList(futures), + (Function>, ? extends List>) input -> { + List tmp = new ArrayList<>(); + if (input != null) { + input.forEach(tmp::addAll); + } + return tmp; + }, executor); + } + + private FutureCallback getAttributesFetchCallback(final TenantId tenantId, final EntityId entityId, final List keys, final FutureCallback> callback) { + return new FutureCallback() { + @Override + public void onSuccess(@Nullable ValidationResult result) { + List>> futures = new ArrayList<>(); + for (String scope : DataConstants.allScopes()) { + futures.add(attributesService.find(tenantId, entityId, scope, keys)); + } + + ListenableFuture> future = mergeAllAttributesFutures(futures); + Futures.addCallback(future, callback, MoreExecutors.directExecutor()); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }; + } + + private FutureCallback getAttributesFetchCallback(final TenantId tenantId, final EntityId entityId, final String scope, final List keys, final FutureCallback> callback) { + return new FutureCallback() { + @Override + public void onSuccess(@Nullable ValidationResult result) { + Futures.addCallback(attributesService.find(tenantId, entityId, scope, keys), callback, MoreExecutors.directExecutor()); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }; + } + + private FutureCallback getAttributesFetchCallback(final TenantId tenantId, final EntityId entityId, final FutureCallback> callback) { + return new FutureCallback() { + @Override + public void onSuccess(@Nullable ValidationResult result) { + List>> futures = new ArrayList<>(); + for (String scope : DataConstants.allScopes()) { + futures.add(attributesService.findAll(tenantId, entityId, scope)); + } + + ListenableFuture> future = mergeAllAttributesFutures(futures); + Futures.addCallback(future, callback, MoreExecutors.directExecutor()); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }; + } + + private FutureCallback getAttributesFetchCallback(final TenantId tenantId, final EntityId entityId, final String scope, final FutureCallback> callback) { + return new FutureCallback() { + @Override + public void onSuccess(@Nullable ValidationResult result) { + Futures.addCallback(attributesService.findAll(tenantId, entityId, scope), callback, MoreExecutors.directExecutor()); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }; + } + + private FutureCallback on(Consumer success, Consumer failure) { + return new FutureCallback() { + @Override + public void onSuccess(@Nullable ValidationResult result) { + ValidationResultCode resultCode = result.getResultCode(); + if (resultCode == ValidationResultCode.OK) { + success.accept(null); + } else { + onFailure(ValidationCallback.getException(result)); + } + } + + @Override + public void onFailure(Throwable t) { + failure.accept(t); + } + }; + } + + + public static Aggregation getAggregation(String agg) { + return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg); + } + + private int getLimit(int limit) { + return limit == 0 ? DEFAULT_LIMIT : limit; + } + + private DefaultTenantProfileConfiguration getTenantProfileConfiguration(WebSocketSessionRef sessionRef) { + return Optional.ofNullable(tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId())) + .map(TenantProfile::getDefaultProfileConfiguration).orElse(null); + } + + + public static WsCmdHandler newCmdHandler(java.util.function.Function cmdExtractor, + BiConsumer handler) { + return new WsCmdHandler<>(cmdExtractor, handler); + } + + public static WsCmdListHandler newCmdsHandler(java.util.function.Function> cmdsExtractor, + BiConsumer handler) { + return new WsCmdListHandler<>(cmdsExtractor, handler); + } + + @RequiredArgsConstructor + public static class WsCmdHandler { + private final java.util.function.Function cmdExtractor; + private final BiConsumer handler; + + public C extractCmd(W cmdsWrapper) { + return cmdExtractor.apply(cmdsWrapper); + } + + @SuppressWarnings("unchecked") + public void handle(WebSocketSessionRef sessionRef, Object cmd) { + handler.accept(sessionRef, (C) cmd); + } + } + + @RequiredArgsConstructor + public static class WsCmdListHandler { + private final java.util.function.Function> cmdsExtractor; + private final BiConsumer handler; + + public List extractCmds(W cmdsWrapper) { + return cmdsExtractor.apply(cmdsWrapper); + } + + @SuppressWarnings("unchecked") + public void handle(WebSocketSessionRef sessionRef, List cmds) { + cmds.forEach(cmd -> { + handler.accept(sessionRef, (C) cmd); + }); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/SessionEvent.java b/application/src/main/java/org/thingsboard/server/service/ws/SessionEvent.java new file mode 100644 index 0000000000..591a688809 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/SessionEvent.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws; + +import lombok.Getter; +import lombok.ToString; + +import java.util.Optional; + +@ToString +public class SessionEvent { + + public enum SessionEventType { + ESTABLISHED, CLOSED, ERROR + }; + + @Getter + private final SessionEventType eventType; + @Getter + private final Optional error; + + private SessionEvent(SessionEventType eventType, Throwable error) { + super(); + this.eventType = eventType; + this.error = Optional.ofNullable(error); + } + + public static SessionEvent onEstablished() { + return new SessionEvent(SessionEventType.ESTABLISHED, null); + } + + public static SessionEvent onClosed() { + return new SessionEvent(SessionEventType.CLOSED, null); + } + + public static SessionEvent onError(Throwable t) { + return new SessionEvent(SessionEventType.ERROR, t); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java new file mode 100644 index 0000000000..bc45639210 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws; + +import org.springframework.web.socket.CloseStatus; + +import java.io.IOException; + +/** + * Created by ashvayka on 27.03.18. + */ +public interface WebSocketMsgEndpoint { + + void send(WebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException; + + void sendPing(WebSocketSessionRef sessionRef, long currentTime) throws IOException; + + void close(WebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException; +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java new file mode 100644 index 0000000000..1ebd6101f0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws; + +import org.springframework.web.socket.CloseStatus; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; + +/** + * Created by ashvayka on 27.03.18. + */ +public interface WebSocketService { + + void handleWebSocketSessionEvent(WebSocketSessionRef sessionRef, SessionEvent sessionEvent); + + void handleWebSocketMsg(WebSocketSessionRef sessionRef, String msg); + + void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update); + + void sendWsMsg(String sessionId, CmdUpdate update); + + void close(String sessionId, CloseStatus status); +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java new file mode 100644 index 0000000000..e799c8faa0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.net.InetSocketAddress; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by ashvayka on 27.03.18. + */ +@RequiredArgsConstructor +@Builder +@Getter +public class WebSocketSessionRef { + + private static final long serialVersionUID = 1L; + + private final String sessionId; + private final SecurityUser securityCtx; + private final InetSocketAddress localAddress; + private final InetSocketAddress remoteAddress; + private final WebSocketSessionType sessionType; + private final AtomicInteger sessionSubIdSeq = new AtomicInteger(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + WebSocketSessionRef that = (WebSocketSessionRef) o; + return Objects.equals(sessionId, that.sessionId); + } + + @Override + public int hashCode() { + return Objects.hash(sessionId); + } + + @Override + public String toString() { + return "WebSocketSessionRef{" + + "sessionId='" + sessionId + '\'' + + ", localAddress=" + localAddress + + ", remoteAddress=" + remoteAddress + + ", sessionType=" + sessionType + + '}'; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionType.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionType.java new file mode 100644 index 0000000000..5c1a155c4b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionType.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +@RequiredArgsConstructor +@Getter +public enum WebSocketSessionType { + TELEMETRY("telemetry"), + NOTIFICATIONS("notifications"); + + private final String name; + + public static Optional forName(String name) { + return Arrays.stream(values()) + .filter(sessionType -> sessionType.getName().equals(name)) + .findFirst(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WsSessionMetaData.java b/application/src/main/java/org/thingsboard/server/service/ws/WsSessionMetaData.java new file mode 100644 index 0000000000..48c2e65509 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/WsSessionMetaData.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws; + + +/** + * Created by ashvayka on 27.03.18. + */ +public class WsSessionMetaData { + private WebSocketSessionRef sessionRef; + private long lastActivityTime; + + public WsSessionMetaData(WebSocketSessionRef sessionRef) { + super(); + this.sessionRef = sessionRef; + this.lastActivityTime = System.currentTimeMillis(); + } + + public WebSocketSessionRef getSessionRef() { + return sessionRef; + } + + public void setSessionRef(WebSocketSessionRef sessionRef) { + this.sessionRef = sessionRef; + } + + public long getLastActivityTime() { + return lastActivityTime; + } + + public void setLastActivityTime(long lastActivityTime) { + this.lastActivityTime = lastActivityTime; + } + + @Override + public String toString() { + return "WsSessionMetaData [sessionRef=" + sessionRef + ", lastActivityTime=" + lastActivityTime + "]"; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java new file mode 100644 index 0000000000..012808b253 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -0,0 +1,248 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.server.common.data.id.IdBased; +import org.thingsboard.server.common.data.id.NotificationId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationStatus; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.dao.notification.NotificationService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; +import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd; +import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd; +import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd; +import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd; +import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; +import org.thingsboard.server.service.ws.notification.sub.NotificationsCountSubscription; +import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscription; +import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +@Slf4j +public class DefaultNotificationCommandsHandler implements NotificationCommandsHandler { + + private final NotificationService notificationService; + private final TbLocalSubscriptionService localSubscriptionService; + private final NotificationCenter notificationCenter; + private final TbServiceInfoProvider serviceInfoProvider; + @Autowired @Lazy + private WebSocketService wsService; + + @Override + public void handleUnreadNotificationsSubCmd(WebSocketSessionRef sessionRef, NotificationsSubCmd cmd) { + log.debug("[{}] Handling unread notifications subscription cmd (cmdId: {})", sessionRef.getSessionId(), cmd.getCmdId()); + SecurityUser securityCtx = sessionRef.getSecurityCtx(); + NotificationsSubscription subscription = NotificationsSubscription.builder() + .serviceId(serviceInfoProvider.getServiceId()) + .sessionId(sessionRef.getSessionId()) + .subscriptionId(cmd.getCmdId()) + .tenantId(securityCtx.getTenantId()) + .entityId(securityCtx.getId()) + .updateProcessor(this::handleNotificationsSubscriptionUpdate) + .limit(cmd.getLimit()) + .build(); + localSubscriptionService.addSubscription(subscription); + + fetchUnreadNotifications(subscription); + sendUpdate(sessionRef.getSessionId(), subscription.createFullUpdate()); + } + + @Override + public void handleUnreadNotificationsCountSubCmd(WebSocketSessionRef sessionRef, NotificationsCountSubCmd cmd) { + log.debug("[{}] Handling unread notifications count subscription cmd (cmdId: {})", sessionRef.getSessionId(), cmd.getCmdId()); + SecurityUser securityCtx = sessionRef.getSecurityCtx(); + NotificationsCountSubscription subscription = NotificationsCountSubscription.builder() + .serviceId(serviceInfoProvider.getServiceId()) + .sessionId(sessionRef.getSessionId()) + .subscriptionId(cmd.getCmdId()) + .tenantId(securityCtx.getTenantId()) + .entityId(securityCtx.getId()) + .updateProcessor(this::handleNotificationsCountSubscriptionUpdate) + .build(); + localSubscriptionService.addSubscription(subscription); + + fetchUnreadNotificationsCount(subscription); + sendUpdate(sessionRef.getSessionId(), subscription.createUpdate()); + } + + private void fetchUnreadNotifications(NotificationsSubscription subscription) { + log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId()); + PageData notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(), + (UserId) subscription.getEntityId(), subscription.getLimit()); + subscription.getLatestUnreadNotifications().clear(); + notifications.getData().forEach(notification -> { + subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification); + }); + subscription.getTotalUnreadCounter().set((int) notifications.getTotalElements()); + } + + private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) { + log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId()); + int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId()); + subscription.getUnreadCounter().set(unreadCount); + } + + + /* Notifications subscription update handling */ + private void handleNotificationsSubscriptionUpdate(NotificationsSubscription subscription, NotificationsSubscriptionUpdate subscriptionUpdate) { + try { + if (subscriptionUpdate.getNotificationUpdate() != null) { + handleNotificationUpdate(subscription, subscriptionUpdate.getNotificationUpdate()); + } else if (subscriptionUpdate.getNotificationRequestUpdate() != null) { + handleNotificationRequestUpdate(subscription, subscriptionUpdate.getNotificationRequestUpdate()); + } + } catch (Exception e) { + log.error("[{}, subId: {}] Failed to handle update for notifications subscription: {}", subscription.getSessionId(), subscription.getSubscriptionId(), subscriptionUpdate, e); + } + } + + private void handleNotificationUpdate(NotificationsSubscription subscription, NotificationUpdate update) { + log.trace("[{}, subId: {}] Handling notification update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); + Notification notification = update.getNotification(); + UUID notificationId = notification != null ? notification.getUuidId() : update.getNotificationId(); + if (update.isCreated()) { + subscription.getLatestUnreadNotifications().put(notificationId, notification); + subscription.getTotalUnreadCounter().incrementAndGet(); + if (subscription.getLatestUnreadNotifications().size() > subscription.getLimit()) { + Set beyondLimit = subscription.getSortedNotifications().stream().skip(subscription.getLimit()) + .map(IdBased::getUuidId).collect(Collectors.toSet()); + beyondLimit.forEach(id -> subscription.getLatestUnreadNotifications().remove(id)); + } + sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification)); + } else if (update.isUpdated()) { + if (update.getNewStatus() == NotificationStatus.READ) { + if (update.isAllNotifications() || subscription.getLatestUnreadNotifications().containsKey(notificationId)) { + fetchUnreadNotifications(subscription); + sendUpdate(subscription.getSessionId(), subscription.createFullUpdate()); + } else { + subscription.getTotalUnreadCounter().decrementAndGet(); + sendUpdate(subscription.getSessionId(), subscription.createCountUpdate()); + } + } else if (notification.getStatus() != NotificationStatus.READ) { + if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) { + subscription.getLatestUnreadNotifications().put(notificationId, notification); + sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification)); + } + } + } else if (update.isDeleted()) { + if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) { + fetchUnreadNotifications(subscription); + sendUpdate(subscription.getSessionId(), subscription.createFullUpdate()); + } else if (notification.getStatus() != NotificationStatus.READ) { + subscription.getTotalUnreadCounter().decrementAndGet(); + sendUpdate(subscription.getSessionId(), subscription.createCountUpdate()); + } + } + } + + private void handleNotificationRequestUpdate(NotificationsSubscription subscription, NotificationRequestUpdate update) { + log.trace("[{}, subId: {}] Handling notification request update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); + fetchUnreadNotifications(subscription); + sendUpdate(subscription.getSessionId(), subscription.createFullUpdate()); + } + + + /* Notifications count subscription update handling */ + private void handleNotificationsCountSubscriptionUpdate(NotificationsCountSubscription subscription, NotificationsSubscriptionUpdate subscriptionUpdate) { + try { + if (subscriptionUpdate.getNotificationUpdate() != null) { + handleNotificationUpdate(subscription, subscriptionUpdate.getNotificationUpdate()); + } else if (subscriptionUpdate.getNotificationRequestUpdate() != null) { + handleNotificationRequestUpdate(subscription, subscriptionUpdate.getNotificationRequestUpdate()); + } + } catch (Exception e) { + log.error("[{}, subId: {}] Failed to handle update for notifications count subscription: {}", subscription.getSessionId(), subscription.getSubscriptionId(), subscriptionUpdate, e); + } + } + + private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) { + log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); + if (update.isCreated()) { + subscription.getUnreadCounter().incrementAndGet(); + sendUpdate(subscription.getSessionId(), subscription.createUpdate()); + } else if (update.isUpdated()) { + if (update.getNewStatus() == NotificationStatus.READ) { + if (update.isAllNotifications()) { + fetchUnreadNotificationsCount(subscription); + } else { + subscription.getUnreadCounter().decrementAndGet(); + } + sendUpdate(subscription.getSessionId(), subscription.createUpdate()); + } + } else if (update.isDeleted()) { + if (update.getNotification().getStatus() != NotificationStatus.READ) { + subscription.getUnreadCounter().decrementAndGet(); + sendUpdate(subscription.getSessionId(), subscription.createUpdate()); + } + } + } + + private void handleNotificationRequestUpdate(NotificationsCountSubscription subscription, NotificationRequestUpdate update) { + log.trace("[{}, subId: {}] Handling notification request update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); + fetchUnreadNotificationsCount(subscription); + sendUpdate(subscription.getSessionId(), subscription.createUpdate()); + } + + + @Override + public void handleMarkAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationsAsReadCmd cmd) { + SecurityUser securityCtx = sessionRef.getSecurityCtx(); + cmd.getNotifications().stream() + .map(NotificationId::new) + .forEach(notificationId -> { + notificationCenter.markNotificationAsRead(securityCtx.getTenantId(), securityCtx.getId(), notificationId); + }); + } + + @Override + public void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd) { + SecurityUser securityCtx = sessionRef.getSecurityCtx(); + notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), securityCtx.getId()); + } + + @Override + public void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { + localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), cmd.getCmdId()); + } + + private void sendUpdate(String sessionId, CmdUpdate update) { + log.trace("[{}, cmdId: {}] Sending WS update: {}", sessionId, update.getCmdId(), update); + wsService.sendWsMsg(sessionId, update); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java new file mode 100644 index 0000000000..e62ebf1606 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification; + +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd; +import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd; +import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd; +import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; + +public interface NotificationCommandsHandler { + + void handleUnreadNotificationsSubCmd(WebSocketSessionRef sessionRef, NotificationsSubCmd cmd); + + void handleUnreadNotificationsCountSubCmd(WebSocketSessionRef sessionRef, NotificationsCountSubCmd cmd); + + void handleMarkAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationsAsReadCmd cmd); + + void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd); + + void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.java new file mode 100644 index 0000000000..f096106135 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.cmd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MarkAllNotificationsAsReadCmd implements WsCmd { + private int cmdId; +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationsAsReadCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationsAsReadCmd.java new file mode 100644 index 0000000000..55de75387f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationsAsReadCmd.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.cmd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MarkNotificationsAsReadCmd implements WsCmd { + private int cmdId; + private List notifications; +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java new file mode 100644 index 0000000000..88613eb966 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.cmd; + +import lombok.Data; + +@Data +public class NotificationCmdsWrapper { + + private NotificationsCountSubCmd unreadCountSubCmd; + + private NotificationsSubCmd unreadSubCmd; + + private MarkNotificationsAsReadCmd markAsReadCmd; + + private MarkAllNotificationsAsReadCmd markAllAsReadCmd; + + private NotificationsUnsubCmd unsubCmd; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.java new file mode 100644 index 0000000000..0d6757a6e7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.cmd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NotificationsCountSubCmd implements WsCmd { + private int cmdId; +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java new file mode 100644 index 0000000000..e022d2c49f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.cmd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NotificationsSubCmd implements WsCmd { + private int cmdId; + private int limit; +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java new file mode 100644 index 0000000000..c9f0897eeb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.cmd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NotificationsUnsubCmd implements UnsubscribeCmd, WsCmd { + private int cmdId; +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java new file mode 100644 index 0000000000..93f51a965b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.cmd; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType; + +@Getter +@ToString +public class UnreadNotificationsCountUpdate extends CmdUpdate { + + private final int totalUnreadCount; + + @Builder + @JsonCreator + public UnreadNotificationsCountUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg, + @JsonProperty("totalUnreadCount") int totalUnreadCount) { + super(cmdId, errorCode, errorMsg); + this.totalUnreadCount = totalUnreadCount; + } + + @Override + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.NOTIFICATIONS_COUNT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.java new file mode 100644 index 0000000000..e64624d16e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.cmd; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType; + +import java.util.Collection; + +@Getter +@ToString(exclude = "notifications") +public class UnreadNotificationsUpdate extends CmdUpdate { + + private final Collection notifications; + private final Notification update; + private final int totalUnreadCount; + + @Builder + @JsonCreator + public UnreadNotificationsUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg, + @JsonProperty("notifications") Collection notifications, + @JsonProperty("update") Notification update, + @JsonProperty("totalUnreadCount") int totalUnreadCount) { + super(cmdId, errorCode, errorMsg); + this.notifications = notifications; + this.update = update; + this.totalUnreadCount = totalUnreadCount; + } + + @Override + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.NOTIFICATIONS; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/WsCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/WsCmd.java new file mode 100644 index 0000000000..97bfeab70c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/WsCmd.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.cmd; + +public interface WsCmd { + int getCmdId(); +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationRequestUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationRequestUpdate.java new file mode 100644 index 0000000000..ebbcc716ab --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationRequestUpdate.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.sub; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.NotificationRequestId; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class NotificationRequestUpdate { + private NotificationRequestId notificationRequestId; + private boolean deleted; +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java new file mode 100644 index 0000000000..b00231a5c9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.sub; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationStatus; + +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class NotificationUpdate { + + private UUID notificationId; + + private boolean created; + private Notification notification; + + private boolean updated; + private NotificationStatus newStatus; + private boolean allNotifications; + + private boolean deleted; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java new file mode 100644 index 0000000000..cb5d140d21 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.sub; + +import lombok.Builder; +import lombok.Getter; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.subscription.TbSubscription; +import org.thingsboard.server.service.subscription.TbSubscriptionType; +import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; + +@Getter +public class NotificationsCountSubscription extends TbSubscription { + + private final AtomicInteger unreadCounter = new AtomicInteger(); + + @Builder + public NotificationsCountSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, + BiConsumer updateProcessor) { + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS_COUNT, updateProcessor); + } + + public UnreadNotificationsCountUpdate createUpdate() { + return UnreadNotificationsCountUpdate.builder() + .cmdId(getSubscriptionId()) + .totalUnreadCount(unreadCounter.get()) + .build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java new file mode 100644 index 0000000000..591781a5f6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.sub; + +import lombok.Builder; +import lombok.Getter; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.service.subscription.TbSubscription; +import org.thingsboard.server.service.subscription.TbSubscriptionType; +import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +@Getter +public class NotificationsSubscription extends TbSubscription { + + private final Map latestUnreadNotifications = new HashMap<>(); + private final int limit; + private final AtomicInteger totalUnreadCounter = new AtomicInteger(); + + @Builder + public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, + BiConsumer updateProcessor, + int limit) { + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS, updateProcessor); + this.limit = limit; + } + + public UnreadNotificationsUpdate createFullUpdate() { + return UnreadNotificationsUpdate.builder() + .cmdId(getSubscriptionId()) + .notifications(getSortedNotifications()) + .totalUnreadCount(totalUnreadCounter.get()) + .build(); + } + + public List getSortedNotifications() { + return latestUnreadNotifications.values().stream() + .sorted(Comparator.comparing(BaseData::getCreatedTime, Comparator.reverseOrder())) + .collect(Collectors.toList()); + } + + public UnreadNotificationsUpdate createPartialUpdate(Notification notification) { + return UnreadNotificationsUpdate.builder() + .cmdId(getSubscriptionId()) + .update(notification) + .totalUnreadCount(totalUnreadCounter.get()) + .build(); + } + + public UnreadNotificationsUpdate createCountUpdate() { + return UnreadNotificationsUpdate.builder() + .cmdId(getSubscriptionId()) + .totalUnreadCount(totalUnreadCounter.get()) + .build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscriptionUpdate.java new file mode 100644 index 0000000000..b69039cbfe --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscriptionUpdate.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.sub; + +import lombok.Data; + +@Data +public class NotificationsSubscriptionUpdate { + + private final NotificationUpdate notificationUpdate; + private final NotificationRequestUpdate notificationRequestUpdate; + + public NotificationsSubscriptionUpdate(NotificationUpdate notificationUpdate) { + this.notificationUpdate = notificationUpdate; + this.notificationRequestUpdate = null; + } + + public NotificationsSubscriptionUpdate(NotificationRequestUpdate notificationRequestUpdate) { + this.notificationUpdate = null; + this.notificationRequestUpdate = notificationRequestUpdate; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryFeature.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryFeature.java new file mode 100644 index 0000000000..c5324208ee --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryFeature.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry; + +/** + * Created by ashvayka on 08.05.17. + */ +public enum TelemetryFeature { + + ATTRIBUTES, TIMESERIES; + + public static TelemetryFeature forName(String name) { + return TelemetryFeature.valueOf(name.toUpperCase()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryWebSocketTextMsg.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryWebSocketTextMsg.java new file mode 100644 index 0000000000..6de98ac8a5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryWebSocketTextMsg.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry; + +import lombok.Data; +import org.thingsboard.server.service.ws.WebSocketSessionRef; + +/** + * Created by ashvayka on 27.03.18. + */ +@Data +public class TelemetryWebSocketTextMsg { + + private final WebSocketSessionRef sessionRef; + private final String payload; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java new file mode 100644 index 0000000000..5b2c12afcd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd; + +import lombok.Data; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUnsubscribeCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUnsubscribeCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUnsubscribeCmd; + +import java.util.List; + +/** + * @author Andrew Shvayka + */ +@Data +public class TelemetryPluginCmdsWrapper { + + private List attrSubCmds; + + private List tsSubCmds; + + private List historyCmds; + + private List entityDataCmds; + + private List entityDataUnsubscribeCmds; + + private List alarmDataCmds; + + private List alarmDataUnsubscribeCmds; + + private List entityCountCmds; + + private List entityCountUnsubscribeCmds; + + private List alarmCountCmds; + + private List alarmCountUnsubscribeCmds; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/AttributesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/AttributesSubscriptionCmd.java new file mode 100644 index 0000000000..1ccec261b0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/AttributesSubscriptionCmd.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v1; + +import lombok.NoArgsConstructor; +import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; + +/** + * @author Andrew Shvayka + */ +@NoArgsConstructor +public class AttributesSubscriptionCmd extends SubscriptionCmd { + + @Override + public TelemetryFeature getType() { + return TelemetryFeature.ATTRIBUTES; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/GetHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/GetHistoryCmd.java new file mode 100644 index 0000000000..6791066641 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/GetHistoryCmd.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v1; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Andrew Shvayka + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class GetHistoryCmd implements TelemetryPluginCmd { + + private int cmdId; + private String entityType; + private String entityId; + private String keys; + private long startTs; + private long endTs; + private long interval; + private int limit; + private String agg; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/SubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/SubscriptionCmd.java new file mode 100644 index 0000000000..6a5f9d820d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/SubscriptionCmd.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v1; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public abstract class SubscriptionCmd implements TelemetryPluginCmd { + + private int cmdId; + private String entityType; + private String entityId; + private String keys; + private String scope; + private boolean unsubscribe; + + public abstract TelemetryFeature getType(); + + @Override + public String toString() { + return "SubscriptionCmd [entityType=" + entityType + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]"; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TelemetryPluginCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TelemetryPluginCmd.java new file mode 100644 index 0000000000..dc9d3a48c2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TelemetryPluginCmd.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v1; + +/** + * @author Andrew Shvayka + */ +public interface TelemetryPluginCmd { + + int getCmdId(); + + void setCmdId(int cmdId); + + String getKeys(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java new file mode 100644 index 0000000000..f0a7b9d3af --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v1; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; + +/** + * @author Andrew Shvayka + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class TimeseriesSubscriptionCmd extends SubscriptionCmd { + + private long startTs; + private long timeWindow; + private long interval; + private int limit; + private String agg; + + @Override + public TelemetryFeature getType() { + return TelemetryFeature.TIMESERIES; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggHistoryCmd.java new file mode 100644 index 0000000000..b928363fc4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggHistoryCmd.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; + +import java.util.List; + +@Data +public class AggHistoryCmd { + + private List keys; + private long startTs; + private long endTs; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggKey.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggKey.java new file mode 100644 index 0000000000..dc8966c0d5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggKey.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; +import org.thingsboard.server.common.data.kv.Aggregation; + +@Data +public class AggKey { + + private int id; + private String key; + private Aggregation agg; + + private Long previousStartTs; + private Long previousEndTs; + private Boolean previousValueOnly; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggTimeSeriesCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggTimeSeriesCmd.java new file mode 100644 index 0000000000..f9dc34fb75 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggTimeSeriesCmd.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; + +import java.util.List; + +@Data +public class AggTimeSeriesCmd { + + private List keys; + private long startTs; + private long timeWindow; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountCmd.java new file mode 100644 index 0000000000..80fd998eb1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountCmd.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.query.AlarmCountQuery; + +public class AlarmCountCmd extends DataCmd { + + @Getter + private final AlarmCountQuery query; + + @JsonCreator + public AlarmCountCmd(@JsonProperty("cmdId") int cmdId, + @JsonProperty("query") AlarmCountQuery query) { + super(cmdId); + this.query = query; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUnsubscribeCmd.java new file mode 100644 index 0000000000..79c860f3f6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUnsubscribeCmd.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; + +@Data +public class AlarmCountUnsubscribeCmd implements UnsubscribeCmd { + + private final int cmdId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUpdate.java new file mode 100644 index 0000000000..b6f70811b8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUpdate.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; + +@ToString +public class AlarmCountUpdate extends CmdUpdate { + + @Getter + private int count; + + public AlarmCountUpdate(int cmdId, int count) { + super(cmdId, SubscriptionErrorCode.NO_ERROR.getCode(), null); + this.count = count; + } + + public AlarmCountUpdate(int cmdId, int errorCode, String errorMsg) { + super(cmdId, errorCode, errorMsg); + } + + @Override + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.ALARM_COUNT_DATA; + } + + @JsonCreator + public AlarmCountUpdate(@JsonProperty("cmdId") int cmdId, + @JsonProperty("count") int count, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg) { + super(cmdId, errorCode, errorMsg); + this.count = count; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataCmd.java new file mode 100644 index 0000000000..0f83d00d6d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataCmd.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.query.AlarmDataQuery; + +public class AlarmDataCmd extends DataCmd { + + @Getter + private final AlarmDataQuery query; + + @JsonCreator + public AlarmDataCmd(@JsonProperty("cmdId") int cmdId, @JsonProperty("query") AlarmDataQuery query) { + super(cmdId); + this.query = query; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java new file mode 100644 index 0000000000..97db558cf9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; + +@Data +public class AlarmDataUnsubscribeCmd implements UnsubscribeCmd { + + private final int cmdId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUpdate.java new file mode 100644 index 0000000000..7d7d29300a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUpdate.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; + +import java.util.List; + +@ToString +public class AlarmDataUpdate extends DataUpdate { + + @Getter + private long allowedEntities; + @Getter + private long totalEntities; + + public AlarmDataUpdate(int cmdId, PageData data, List update, long allowedEntities, long totalEntities) { + super(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); + this.allowedEntities = allowedEntities; + this.totalEntities = totalEntities; + } + + public AlarmDataUpdate(int cmdId, int errorCode, String errorMsg) { + super(cmdId, null, null, errorCode, errorMsg); + } + + @Override + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.ALARM_DATA; + } + + @JsonCreator + public AlarmDataUpdate(@JsonProperty("cmdId") int cmdId, + @JsonProperty("data") PageData data, + @JsonProperty("update") List update, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg, + @JsonProperty("allowedEntities") long allowedEntities, + @JsonProperty("totalEntities") long totalEntities) { + super(cmdId, data, update, errorCode, errorMsg); + this.allowedEntities = allowedEntities; + this.totalEntities = totalEntities; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdate.java new file mode 100644 index 0000000000..ba779176aa --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdate.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class CmdUpdate { + + private final int cmdId; + private final int errorCode; + private final String errorMsg; + + public abstract CmdUpdateType getCmdUpdateType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java new file mode 100644 index 0000000000..f5b3809ce2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +public enum CmdUpdateType { + ENTITY_DATA, + ALARM_DATA, + ALARM_COUNT_DATA, + COUNT_DATA, + NOTIFICATIONS, + NOTIFICATIONS_COUNT +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataCmd.java new file mode 100644 index 0000000000..c09cbc03d1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataCmd.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; +import lombok.Getter; + +@Data +public class DataCmd { + + @Getter + private final int cmdId; + + public DataCmd(int cmdId) { + this.cmdId = cmdId; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataUpdate.java new file mode 100644 index 0000000000..c4bfce441a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataUpdate.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Getter; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; + +import java.util.List; + +public abstract class DataUpdate extends CmdUpdate { + + @Getter + private final PageData data; + @Getter + private final List update; + + public DataUpdate(int cmdId, PageData data, List update, int errorCode, String errorMsg) { + super(cmdId, errorCode, errorMsg); + this.data = data; + this.update = update; + } + + public DataUpdate(int cmdId, PageData data, List update) { + this(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); + } + + public DataUpdate(int cmdId, int errorCode, String errorMsg) { + this(cmdId, null, null, errorCode, errorMsg); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountCmd.java new file mode 100644 index 0000000000..c39f81f1b7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountCmd.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.query.EntityCountQuery; + +public class EntityCountCmd extends DataCmd { + + @Getter + private final EntityCountQuery query; + + @JsonCreator + public EntityCountCmd(@JsonProperty("cmdId") int cmdId, + @JsonProperty("query") EntityCountQuery query) { + super(cmdId); + this.query = query; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java new file mode 100644 index 0000000000..b82fecd8fd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; + +@Data +public class EntityCountUnsubscribeCmd implements UnsubscribeCmd { + + private final int cmdId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUpdate.java new file mode 100644 index 0000000000..030d39b8dc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUpdate.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; + +@ToString +public class EntityCountUpdate extends CmdUpdate { + + @Getter + private int count; + + public EntityCountUpdate(int cmdId, int count) { + super(cmdId, SubscriptionErrorCode.NO_ERROR.getCode(), null); + this.count = count; + } + + public EntityCountUpdate(int cmdId, int errorCode, String errorMsg) { + super(cmdId, errorCode, errorMsg); + } + + @Override + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.COUNT_DATA; + } + + @JsonCreator + public EntityCountUpdate(@JsonProperty("cmdId") int cmdId, + @JsonProperty("count") int count, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg) { + super(cmdId, errorCode, errorMsg); + this.count = count; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataCmd.java new file mode 100644 index 0000000000..5fe9179651 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataCmd.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +public class EntityDataCmd extends DataCmd { + + @Getter + private final EntityDataQuery query; + @Getter + private final EntityHistoryCmd historyCmd; + @Getter + private final LatestValueCmd latestCmd; + @Getter + private final TimeSeriesCmd tsCmd; + @Getter + private final AggHistoryCmd aggHistoryCmd; + @Getter + private final AggTimeSeriesCmd aggTsCmd; + + public EntityDataCmd(int cmdId, EntityDataQuery query, EntityHistoryCmd historyCmd, LatestValueCmd latestCmd, TimeSeriesCmd tsCmd) { + this(cmdId, query, historyCmd, latestCmd, tsCmd, null, null); + } + + @JsonCreator + public EntityDataCmd(@JsonProperty("cmdId") int cmdId, + @JsonProperty("query") EntityDataQuery query, + @JsonProperty("historyCmd") EntityHistoryCmd historyCmd, + @JsonProperty("latestCmd") LatestValueCmd latestCmd, + @JsonProperty("tsCmd") TimeSeriesCmd tsCmd, + @JsonProperty("aggHistoryCmd") AggHistoryCmd aggHistoryCmd, + @JsonProperty("aggTsCmd") AggTimeSeriesCmd aggTsCmd) { + super(cmdId); + this.query = query; + this.historyCmd = historyCmd; + this.latestCmd = latestCmd; + this.tsCmd = tsCmd; + this.aggHistoryCmd = aggHistoryCmd; + this.aggTsCmd = aggTsCmd; + } + + @JsonIgnore + public boolean hasAnyCmd() { + return historyCmd != null || latestCmd != null || tsCmd != null || aggHistoryCmd != null || aggTsCmd != null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java new file mode 100644 index 0000000000..80577edda2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; + +@Data +public class EntityDataUnsubscribeCmd implements UnsubscribeCmd { + + private final int cmdId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUpdate.java new file mode 100644 index 0000000000..3b3cf69186 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUpdate.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; + +import java.util.List; + +@ToString +public class EntityDataUpdate extends DataUpdate { + + @Getter + private long allowedEntities; + + public EntityDataUpdate(int cmdId, PageData data, List update, long allowedEntities) { + super(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); + this.allowedEntities = allowedEntities; + } + + public EntityDataUpdate(int cmdId, int errorCode, String errorMsg) { + super(cmdId, null, null, errorCode, errorMsg); + } + + @Override + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.ENTITY_DATA; + } + + @JsonCreator + public EntityDataUpdate(@JsonProperty("cmdId") int cmdId, + @JsonProperty("data") PageData data, + @JsonProperty("update") List update, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg) { + super(cmdId, data, update, errorCode, errorMsg); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.java new file mode 100644 index 0000000000..5f22897b8f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; +import org.thingsboard.server.common.data.kv.Aggregation; + +import java.util.List; + +@Data +public class EntityHistoryCmd implements GetTsCmd { + + private List keys; + private long startTs; + private long endTs; + private long interval; + private int limit; + private Aggregation agg; + private boolean fetchLatestPreviousPoint; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.java new file mode 100644 index 0000000000..5d6a730772 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import org.thingsboard.server.common.data.kv.Aggregation; + +import java.util.List; + +public interface GetTsCmd { + + long getStartTs(); + + long getEndTs(); + + List getKeys(); + + long getInterval(); + + int getLimit(); + + Aggregation getAgg(); + + boolean isFetchLatestPreviousPoint(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/LatestValueCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/LatestValueCmd.java new file mode 100644 index 0000000000..291829b47b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/LatestValueCmd.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityKey; + +import java.util.List; + +@Data +public class LatestValueCmd { + + private List keys; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.java new file mode 100644 index 0000000000..6ece880d7e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.kv.Aggregation; + +import java.util.List; + +@Data +public class TimeSeriesCmd implements GetTsCmd { + + private List keys; + private long startTs; + private long timeWindow; + private long interval; + private int limit; + private Aggregation agg; + private boolean fetchLatestPreviousPoint; + + @JsonIgnore + @Override + public long getEndTs() { + return startTs + timeWindow; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/UnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/UnsubscribeCmd.java new file mode 100644 index 0000000000..81288c76da --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/UnsubscribeCmd.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.cmd.v2; + +public interface UnsubscribeCmd { + + int getCmdId(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java new file mode 100644 index 0000000000..90041ffa13 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.sub; + +import lombok.Getter; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; + +public class AlarmSubscriptionUpdate { + + @Getter + private int subscriptionId; + @Getter + private int errorCode; + @Getter + private String errorMsg; + @Getter + private AlarmInfo alarm; + @Getter + private boolean alarmDeleted; + + public AlarmSubscriptionUpdate(int subscriptionId, AlarmInfo alarm) { + this(subscriptionId, alarm, false); + } + + public AlarmSubscriptionUpdate(int subscriptionId, AlarmInfo alarm, boolean alarmDeleted) { + super(); + this.subscriptionId = subscriptionId; + this.alarm = alarm; + this.alarmDeleted = alarmDeleted; + } + + public AlarmSubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) { + this(subscriptionId, errorCode, null); + } + + public AlarmSubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) { + super(); + this.subscriptionId = subscriptionId; + this.errorCode = errorCode.getCode(); + this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg(); + } + + @Override + public String toString() { + return "AlarmUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + + ", alarm=" + alarm + "]"; + } +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/SubscriptionState.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/SubscriptionState.java new file mode 100644 index 0000000000..88d8f7ca7c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/SubscriptionState.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.sub; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; + +import java.util.Map; + +/** + * @author Andrew Shvayka + */ +@AllArgsConstructor +public class SubscriptionState { + + @Getter private final String wsSessionId; + @Getter private final int subscriptionId; + @Getter private final TenantId tenantId; + @Getter private final EntityId entityId; + @Getter private final TelemetryFeature type; + @Getter private final boolean allKeys; + @Getter private final Map keyStates; + @Getter private final String scope; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SubscriptionState that = (SubscriptionState) o; + + if (subscriptionId != that.subscriptionId) return false; + if (wsSessionId != null ? !wsSessionId.equals(that.wsSessionId) : that.wsSessionId != null) return false; + if (entityId != null ? !entityId.equals(that.entityId) : that.entityId != null) return false; + return type == that.type; + } + + @Override + public int hashCode() { + int result = wsSessionId != null ? wsSessionId.hashCode() : 0; + result = 31 * result + subscriptionId; + result = 31 * result + (entityId != null ? entityId.hashCode() : 0); + result = 31 * result + (type != null ? type.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "SubscriptionState{" + + "type=" + type + + ", entityId=" + entityId + + ", subscriptionId=" + subscriptionId + + ", wsSessionId='" + wsSessionId + '\'' + + '}'; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java new file mode 100644 index 0000000000..108c5cf1d4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.telemetry.sub; + +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class TelemetrySubscriptionUpdate { + + private int subscriptionId; + private int errorCode; + private String errorMsg; + private Map> data; + + public TelemetrySubscriptionUpdate(int subscriptionId, List data) { + super(); + this.subscriptionId = subscriptionId; + this.data = new TreeMap<>(); + if (data != null) { + for (TsKvEntry tsEntry : data) { + List values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>()); + Object[] value = new Object[2]; + value[0] = tsEntry.getTs(); + value[1] = tsEntry.getValueAsString(); + values.add(value); + } + } + } + + public TelemetrySubscriptionUpdate(int subscriptionId, Map> data) { + super(); + this.subscriptionId = subscriptionId; + this.data = data; + } + + public TelemetrySubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) { + this(subscriptionId, errorCode, null); + } + + public TelemetrySubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) { + super(); + this.subscriptionId = subscriptionId; + this.errorCode = errorCode.getCode(); + this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg(); + } + + public int getSubscriptionId() { + return subscriptionId; + } + + public Map> getData() { + return data; + } + + public Map getLatestValues() { + if (data == null) { + return Collections.emptyMap(); + } else { + return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> { + List data = e.getValue(); + Object[] latest = (Object[]) data.get(data.size() - 1); + return (long) latest[0]; + })); + } + } + + public int getErrorCode() { + return errorCode; + } + + public String getErrorMsg() { + return errorMsg; + } + + @Override + public String toString() { + return "TsSubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data=" + + data + "]"; + } +} diff --git a/application/src/main/java/org/thingsboard/server/springfox/SpringfoxHandlerProviderBeanPostProcessor.java b/application/src/main/java/org/thingsboard/server/springfox/SpringfoxHandlerProviderBeanPostProcessor.java index af03c72004..6a222870b0 100644 --- a/application/src/main/java/org/thingsboard/server/springfox/SpringfoxHandlerProviderBeanPostProcessor.java +++ b/application/src/main/java/org/thingsboard/server/springfox/SpringfoxHandlerProviderBeanPostProcessor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; -import org.thingsboard.server.queue.util.TbCoreComponent; import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider; import java.lang.reflect.Field; diff --git a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java index e4d0c13376..a40b41955d 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java b/application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java deleted file mode 100644 index d7e44e6e5a..0000000000 --- a/application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.utils; - -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.function.Consumer; - -/** - * This class deduplicate executions of the specified function. - * Useful in cluster mode, when you get event about partition change multiple times. - * Assuming that the function execution is expensive, we should execute it immediately when first time event occurs and - * later, once the processing of first event is done, process last pending task. - * - * @param

parameters of the function - */ -@Slf4j -public class EventDeduplicationExecutor

{ - private final String name; - private final ExecutorService executor; - private final Consumer

function; - private P pendingTask; - private boolean busy; - - public EventDeduplicationExecutor(String name, ExecutorService executor, Consumer

function) { - this.name = name; - this.executor = executor; - this.function = function; - } - - public void submit(P params) { - log.info("[{}] Going to submit: {}", name, params); - synchronized (EventDeduplicationExecutor.this) { - if (!busy) { - busy = true; - pendingTask = null; - try { - log.info("[{}] Submitting task: {}", name, params); - executor.submit(() -> { - try { - log.info("[{}] Executing task: {}", name, params); - function.accept(params); - } catch (Throwable e) { - log.warn("[{}] Failed to process task with parameters: {}", name, params, e); - throw e; - } finally { - unlockAndProcessIfAny(); - } - }); - } catch (Throwable e) { - log.warn("[{}] Failed to submit task with parameters: {}", name, params, e); - unlockAndProcessIfAny(); - throw e; - } - } else { - log.info("[{}] Task is already in progress. {} pending task: {}", name, pendingTask == null ? "adding" : "updating", params); - pendingTask = params; - } - } - } - - private void unlockAndProcessIfAny() { - synchronized (EventDeduplicationExecutor.this) { - busy = false; - if (pendingTask != null) { - submit(pendingTask); - } - } - } -} diff --git a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java new file mode 100644 index 0000000000..4a07444bbb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.utils; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.DDFFileParser; +import org.eclipse.leshan.core.model.DefaultDDFFileValidator; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.ObjectModel; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; +import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve; +import org.thingsboard.server.common.data.util.TbDDFFileParser; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_SEARCH_TEXT; + +@Slf4j +public class LwM2mObjectModelUtils { + + private static final TbDDFFileParser ddfFileParser = new TbDDFFileParser(); + + public static void toLwm2mResource (TbResource resource) throws ThingsboardException { + try { + List objectModels = + ddfFileParser.parse(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + if (!objectModels.isEmpty()) { + ObjectModel objectModel = objectModels.get(0); + + String resourceKey = objectModel.id + LWM2M_SEPARATOR_KEY + objectModel.version; + String name = objectModel.name; + resource.setResourceKey(resourceKey); + if (resource.getId() == null) { + resource.setTitle(name + " id=" + objectModel.id + " v" + objectModel.version); + } + resource.setSearchText(resourceKey + LWM2M_SEPARATOR_SEARCH_TEXT + name); + } else { + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); + } + } catch (InvalidDDFFileException e) { + log.error("Failed to parse file {}", resource.getFileName(), e); + throw new DataValidationException("Failed to parse file " + resource.getFileName()); + } catch (IOException e) { + throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); + } + if (resource.getResourceType().equals(ResourceType.LWM2M_MODEL) && toLwM2mObject(resource, true) == null) { + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); + } + } + + public static LwM2mObject toLwM2mObject(TbResource resource, boolean isSave) { + try { + List objectModels = + ddfFileParser.parse(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + if (objectModels.size() == 0) { + return null; + } else { + ObjectModel obj = objectModels.get(0); + LwM2mObject lwM2mObject = new LwM2mObject(); + lwM2mObject.setId(obj.id); + lwM2mObject.setKeyId(resource.getResourceKey()); + lwM2mObject.setName(obj.name); + lwM2mObject.setMultiple(obj.multiple); + lwM2mObject.setMandatory(obj.mandatory); + LwM2mInstance instance = new LwM2mInstance(); + instance.setId(0); + List resources = new ArrayList<>(); + obj.resources.forEach((k, v) -> { + if (isSave) { + LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); + resources.add(lwM2MResourceObserve); + } else if (v.operations.isReadable()) { + LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); + resources.add(lwM2MResourceObserve); + } + }); + if (isSave || resources.size() > 0) { + instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); + lwM2mObject.setInstances(new LwM2mInstance[]{instance}); + return lwM2mObject; + } else { + return null; + } + } + } catch (IOException | InvalidDDFFileException e) { + log.error("Could not parse the XML of objectModel with name [{}]", resource.getSearchText(), e); + return null; + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java index fbb217a846..2fc0fa134c 100644 --- a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java b/application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java index b521b6f17b..3bf1efc8f4 100644 --- a/application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java +++ b/application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 941bd7d278..7159a0256d 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -1,7 +1,7 @@ + + + + + + + diff --git a/application/src/test/resources/provision/x509ChainProvisionTest.pem b/application/src/test/resources/provision/x509ChainProvisionTest.pem new file mode 100644 index 0000000000..b2ec300f78 --- /dev/null +++ b/application/src/test/resources/provision/x509ChainProvisionTest.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIw +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1ow +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qK +QiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaC +kQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8G +A1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZ +uccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICMTCCAdegAwIBAgIUUEKxS9hTz4l+oLUMF0LV6TC/gCIwCgYIKoZIzj0EAwIw +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTczNloXDTI0MDMyODE0NTczNlow +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECMlWO72k +rDoUL9FQjUmSCetkhaEGJUfQkdSfkLSNa0GyAEIMbfmzI4zITeapunu4rGet3EMy +LydQzuQanBicp6NTMFEwHQYDVR0OBBYEFHpZ78tPnztNii4Da/yCw6mhEIL3MB8G +A1UdIwQYMBaAFHpZ78tPnztNii4Da/yCw6mhEIL3MA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSAAwRQIgJ7qyMFqNcwSYkH6o+UlQXzLWfwZbNjVk+aR7foAZ +NGsCIQDsd7v3WQIGHiArfZeDs1DLEDuV/2h6L+ZNoGNhEKL+1A== +-----END CERTIFICATE----- diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 8f2c1e1b13..b113788a11 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -1,6 +1,6 @@ + + 4.0.0 + + org.thingsboard + 3.5.2-SNAPSHOT + thingsboard + + + monitoring + ThingsBoard Monitoring Service + jar + + + UTF-8 + ${basedir}/.. + java + false + process-resources + package + tb-monitoring + false + ${project.build.directory}/windows + ThingsBoard Monitoring Service + org.thingsboard.monitoring.ThingsboardMonitoringApplication + + 2.6.1 + 2.0.0-M4 + + + + + org.thingsboard.common + data + + + org.thingsboard.common + util + + + org.thingsboard + rest-client + compile + + + org.springframework.boot + spring-boot-starter + + + + org.eclipse.californium + californium-core + ${californium.version} + + + org.eclipse.californium + scandium + ${californium.version} + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + + + org.apache.httpcomponents + httpclient + + + org.eclipse.leshan + leshan-client-cf + ${leshan.version} + + + org.eclipse.leshan + leshan-core + ${leshan.version} + + + org.java-websocket + Java-WebSocket + compile + + + com.google.guava + guava + + + org.apache.commons + commons-lang3 + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + + + ${pkg.name}-${project.version} + + + ${project.basedir}/src/main/resources + + + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + org.thingsboard + gradle-maven-plugin + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.apache.maven.plugins + maven-install-plugin + + + + + + jenkins + Jenkins Repository + https://repo.jenkins-ci.org/releases + + false + + + + diff --git a/monitoring/src/main/conf/logback.xml b/monitoring/src/main/conf/logback.xml new file mode 100644 index 0000000000..3c1e1382b7 --- /dev/null +++ b/monitoring/src/main/conf/logback.xml @@ -0,0 +1,51 @@ + + + + + + + ${pkg.logFolder}/${pkg.name}.log + + ${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/monitoring/src/main/conf/tb-monitoring.conf b/monitoring/src/main/conf/tb-monitoring.conf new file mode 100644 index 0000000000..d1d729bd20 --- /dev/null +++ b/monitoring/src/main/conf/tb-monitoring.conf @@ -0,0 +1,22 @@ +# +# Copyright © 2016-2023 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export LOG_FILENAME=tb-monitoring.out +export LOADER_PATH=/usr/share/tb-monitoring/conf diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java new file mode 100644 index 0000000000..399d0f4a56 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.monitoring.service.BaseMonitoringService; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@SpringBootApplication +@EnableScheduling +@Slf4j +public class ThingsboardMonitoringApplication { + + @Autowired + private List> monitoringServices; + + @Value("${monitoring.monitoring_rate_ms}") + private int monitoringRateMs; + + public static void main(String[] args) { + new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) + .properties(Map.of("spring.config.name", "tb-monitoring")) + .run(args); + } + + @EventListener(ApplicationReadyEvent.class) + public void startMonitoring() { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("monitoring-executor")); + scheduler.scheduleWithFixedDelay(() -> { + monitoringServices.forEach(monitoringService -> { + monitoringService.runChecks(); + }); + }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java new file mode 100644 index 0000000000..473a17aa00 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java @@ -0,0 +1,171 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.client; + +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.californium.core.observe.ObservationStore; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.leshan.client.californium.LeshanClient; +import org.eclipse.leshan.client.californium.LeshanClientBuilder; +import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; +import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.client.object.Server; +import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.resource.DummyInstanceEnabler; +import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.client.servers.ServerIdentity; +import org.eclipse.leshan.core.californium.EndpointFactory; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.ObjectLoader; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.StaticModel; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; +import org.eclipse.leshan.core.response.ReadResponse; +import org.thingsboard.monitoring.util.ResourceUtils; + +import javax.security.auth.Destroyable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.eclipse.leshan.client.object.Security.noSec; +import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; +import static org.eclipse.leshan.core.LwM2mId.DEVICE; +import static org.eclipse.leshan.core.LwM2mId.SECURITY; +import static org.eclipse.leshan.core.LwM2mId.SERVER; + +@Slf4j +public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { + + @Getter + @Setter + private LeshanClient leshanClient; + + private static final List supportedResources = Collections.singletonList(0); + + private String data = ""; + + private String serverUri; + private String endpoint; + + public Lwm2mClient(String serverUri, String endpoint) { + this.serverUri = serverUri; + this.endpoint = endpoint; + } + + public Lwm2mClient() { + } + + public void initClient() throws InvalidDDFFileException, IOException { + String[] resources = new String[]{"0.xml", "1.xml", "2.xml", "test-model.xml"}; + List models = new ArrayList<>(); + for (String resourceName : resources) { + models.addAll(ObjectLoader.loadDdfFile(ResourceUtils.getResourceAsStream("lwm2m/models/" + resourceName), resourceName)); + } + + Security security = noSec(serverUri, 123); + NetworkConfig coapConfig = new NetworkConfig().setString(NetworkConfig.Keys.COAP_PORT, StringUtils.substringAfterLast(serverUri, ":")); + + LeshanClient leshanClient; + + LwM2mModel model = new StaticModel(models); + ObjectsInitializer initializer = new ObjectsInitializer(model); + initializer.setInstancesForObject(SECURITY, security); + initializer.setInstancesForObject(SERVER, new Server(123, 300)); + initializer.setInstancesForObject(DEVICE, this); + initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); + DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); + dtlsConfig.setRecommendedCipherSuitesOnly(true); + dtlsConfig.setClientOnly(); + + DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory(); + engineFactory.setReconnectOnUpdate(false); + engineFactory.setResumeOnConnect(true); + + EndpointFactory endpointFactory = new EndpointFactory() { + + @Override + public CoapEndpoint createUnsecuredEndpoint(InetSocketAddress address, NetworkConfig coapConfig, + ObservationStore store) { + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + builder.setInetSocketAddress(address); + builder.setNetworkConfig(coapConfig); + return builder.build(); + } + + @Override + public CoapEndpoint createSecuredEndpoint(DtlsConnectorConfig dtlsConfig, NetworkConfig coapConfig, + ObservationStore store) { + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + DtlsConnectorConfig.Builder dtlsConfigBuilder = new DtlsConnectorConfig.Builder(dtlsConfig); + builder.setConnector(new DTLSConnector(dtlsConfigBuilder.build())); + builder.setNetworkConfig(coapConfig); + return builder.build(); + } + }; + + LeshanClientBuilder builder = new LeshanClientBuilder(endpoint); + builder.setObjects(initializer.createAll()); + builder.setCoapConfig(coapConfig); + builder.setDtlsConfig(dtlsConfig); + builder.setRegistrationEngineFactory(engineFactory); + builder.setEndpointFactory(endpointFactory); + builder.setDecoder(new DefaultLwM2mDecoder(false)); + builder.setEncoder(new DefaultLwM2mEncoder(false)); + leshanClient = builder.build(); + + setLeshanClient(leshanClient); + + leshanClient.start(); + } + + @Override + public List getAvailableResourceIds(ObjectModel model) { + return supportedResources; + } + + @Override + public ReadResponse read(ServerIdentity identity, int resourceId) { + if (supportedResources.contains(resourceId)) { + return ReadResponse.success(resourceId, data); + } + return super.read(identity, resourceId); + } + + @SneakyThrows + public void send(String data, int resource) { + this.data = data; + fireResourcesChange(resource); + } + + @Override + public void destroy() { + if (leshanClient != null) { + leshanClient.destroy(true); + } + } +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java new file mode 100644 index 0000000000..9dcc58cf2f --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.client; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.thingsboard.rest.client.RestClient; + +import java.time.Duration; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TbClient extends RestClient { + + @Value("${monitoring.rest.username}") + private String username; + @Value("${monitoring.rest.password}") + private String password; + + public TbClient(@Value("${monitoring.rest.base_url}") String baseUrl, + @Value("${monitoring.rest.request_timeout_ms}") int requestTimeoutMs) { + super(new RestTemplateBuilder() + .setConnectTimeout(Duration.ofMillis(requestTimeoutMs)) + .setReadTimeout(Duration.ofMillis(requestTimeoutMs)) + .build(), baseUrl); + } + + public String logIn() { + login(username, password); + return getToken(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java new file mode 100644 index 0000000000..20e7a900ff --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java @@ -0,0 +1,194 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.client; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.data.cmd.CmdsWrapper; +import org.thingsboard.monitoring.data.cmd.EntityDataCmd; +import org.thingsboard.monitoring.data.cmd.EntityDataUpdate; +import org.thingsboard.monitoring.data.cmd.LatestValueCmd; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityListFilter; + +import javax.net.ssl.SSLParameters; +import java.net.URI; +import java.nio.channels.NotYetConnectedException; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +@Slf4j +public class WsClient extends WebSocketClient implements AutoCloseable { + + public volatile JsonNode lastMsg; + private CountDownLatch reply; + private CountDownLatch update; + + private final Lock updateLock = new ReentrantLock(); + + private long requestTimeoutMs; + + public WsClient(URI serverUri, long requestTimeoutMs) { + super(serverUri); + this.requestTimeoutMs = requestTimeoutMs; + } + + @Override + public void onOpen(ServerHandshake serverHandshake) { + + } + + @Override + public void onMessage(String s) { + if (s == null) { + return; + } + updateLock.lock(); + try { + lastMsg = JacksonUtil.toJsonNode(s); + log.trace("Received new msg: {}", lastMsg.toPrettyString()); + if (update != null) { + update.countDown(); + } + if (reply != null) { + reply.countDown(); + } + } finally { + updateLock.unlock(); + } + } + + @Override + public void onClose(int i, String s, boolean b) { + log.debug("WebSocket client is closed"); + } + + @Override + public void onError(Exception e) { + log.error("WebSocket client error:", e); + } + + public void registerWaitForUpdate() { + updateLock.lock(); + try { + lastMsg = null; + update = new CountDownLatch(1); + } finally { + updateLock.unlock(); + } + log.trace("Registered wait for update"); + } + + @Override + public void send(String text) throws NotYetConnectedException { + updateLock.lock(); + try { + reply = new CountDownLatch(1); + } finally { + updateLock.unlock(); + } + super.send(text); + } + + public WsClient subscribeForTelemetry(List devices, String key) { + EntityDataCmd cmd = new EntityDataCmd(); + cmd.setCmdId(RandomUtils.nextInt(0, 1000)); + + EntityListFilter devicesFilter = new EntityListFilter(); + devicesFilter.setEntityType(EntityType.DEVICE); + devicesFilter.setEntityList(devices.stream().map(UUID::toString).collect(Collectors.toList())); + EntityDataPageLink pageLink = new EntityDataPageLink(100,0, null, null); + EntityDataQuery devicesQuery = new EntityDataQuery(devicesFilter, pageLink, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + cmd.setQuery(devicesQuery); + + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(List.of(new EntityKey(EntityKeyType.TIME_SERIES, key))); + cmd.setLatestCmd(latestCmd); + + CmdsWrapper wrapper = new CmdsWrapper(); + wrapper.setEntityDataCmds(List.of(cmd)); + send(JacksonUtil.toString(wrapper)); + return this; + } + + public JsonNode waitForUpdate(long ms) { + log.trace("update latch count: {}", update.getCount()); + try { + if (update.await(ms, TimeUnit.MILLISECONDS)) { + log.trace("Waited for update"); + return getLastMsg(); + } + } catch (InterruptedException e) { + log.debug("Failed to await reply", e); + } + log.trace("No update arrived within {} ms", ms); + return null; + } + + public JsonNode waitForReply() { + try { + if (reply.await(requestTimeoutMs, TimeUnit.MILLISECONDS)) { + log.trace("Waited for reply"); + return getLastMsg(); + } + } catch (InterruptedException e) { + log.debug("Failed to await reply", e); + } + log.trace("No reply arrived within {} ms", requestTimeoutMs); + throw new IllegalStateException("No WS reply arrived within " + requestTimeoutMs + " ms"); + } + + private JsonNode getLastMsg() { + if (lastMsg != null) { + JsonNode errorMsg = lastMsg.get("errorMsg"); + if (errorMsg != null && !errorMsg.isNull() && StringUtils.isNotEmpty(errorMsg.asText())) { + throw new RuntimeException("WS error from server: " + errorMsg.asText()); + } else { + return lastMsg; + } + } else { + return null; + } + } + + public Object getTelemetryUpdate(UUID deviceId, String key) { + JsonNode lastMsg = getLastMsg(); + if (lastMsg == null || lastMsg.isNull()) return null; + EntityDataUpdate update = JacksonUtil.treeToValue(lastMsg, EntityDataUpdate.class); + return update.getLatest(deviceId, key); + } + + @Override + protected void onSetSSLParameters(SSLParameters sslParameters) { + sslParameters.setEndpointIdentificationAlgorithm(null); + } + +} \ No newline at end of file diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClientFactory.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClientFactory.java new file mode 100644 index 0000000000..1bab803dc9 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClientFactory.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.client; + +import lombok.RequiredArgsConstructor; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.ssl.SSLContexts; +import org.apache.http.ssl.TrustStrategy; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.data.Latencies; +import org.thingsboard.monitoring.service.MonitoringReporter; +import org.thingsboard.monitoring.util.TbStopWatch; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class WsClientFactory { + + private final MonitoringReporter monitoringReporter; + private final TbStopWatch stopWatch; + @Value("${monitoring.ws.base_url}") + private String baseUrl; + @Value("${monitoring.ws.request_timeout_ms}") + private int requestTimeoutMs; + + public WsClient createClient(String accessToken) throws Exception { + URI uri = new URI(baseUrl + "/api/ws/plugins/telemetry?token=" + accessToken); + stopWatch.start(); + WsClient wsClient = new WsClient(uri, requestTimeoutMs); + if (baseUrl.startsWith("wss")) { + SSLContextBuilder builder = SSLContexts.custom(); + builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true); + wsClient.setSocketFactory(builder.build().getSocketFactory()); + } + boolean connected = wsClient.connectBlocking(requestTimeoutMs, TimeUnit.MILLISECONDS); + if (!connected) { + throw new IllegalStateException("Failed to establish WS session"); + } + monitoringReporter.reportLatency(Latencies.WS_CONNECT, stopWatch.getTime()); + return wsClient; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java new file mode 100644 index 0000000000..4304ecdf0e --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config; + +import java.util.List; + +public interface MonitoringConfig { + + List getTargets(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java new file mode 100644 index 0000000000..0e62670f81 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config; + +import java.util.UUID; + +public interface MonitoringTarget { + + UUID getDeviceId(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java new file mode 100644 index 0000000000..a95aca18f4 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config.transport; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(name = "monitoring.transports.coap.enabled", havingValue = "true") +@ConfigurationProperties(prefix = "monitoring.transports.coap") +public class CoapTransportMonitoringConfig extends TransportMonitoringConfig { + + @Override + public TransportType getTransportType() { + return TransportType.COAP; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java new file mode 100644 index 0000000000..94c0ea3a84 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config.transport; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.security.DeviceCredentials; + +import java.util.UUID; + +@Data +public class DeviceConfig { + + private UUID id; + private String name; + private DeviceCredentials credentials; + + public void setId(String id) { + this.id = StringUtils.isNotEmpty(id) ? UUID.fromString(id) : null; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java new file mode 100644 index 0000000000..3b2be2343d --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config.transport; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(name = "monitoring.transports.http.enabled", havingValue = "true") +@ConfigurationProperties(prefix = "monitoring.transports.http") +public class HttpTransportMonitoringConfig extends TransportMonitoringConfig { + + @Override + public TransportType getTransportType() { + return TransportType.HTTP; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java new file mode 100644 index 0000000000..7d375abd6e --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config.transport; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(name = "monitoring.transports.lwm2m.enabled", havingValue = "true") +@ConfigurationProperties(prefix = "monitoring.transports.lwm2m") +public class Lwm2mTransportMonitoringConfig extends TransportMonitoringConfig { + + @Override + public TransportType getTransportType() { + return TransportType.LWM2M; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java new file mode 100644 index 0000000000..ebc91a327e --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config.transport; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(name = "monitoring.transports.mqtt.enabled", havingValue = "true") +@ConfigurationProperties(prefix = "monitoring.transports.mqtt") +@Data +@EqualsAndHashCode(callSuper = true) +public class MqttTransportMonitoringConfig extends TransportMonitoringConfig { + + private Integer qos; + + @Override + public TransportType getTransportType() { + return TransportType.MQTT; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java new file mode 100644 index 0000000000..6fb2e25ec9 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config.transport; + +import lombok.Data; + +@Data +public class TransportInfo { + + private final TransportType transportType; + private final String baseUrl; + + @Override + public String toString() { + return String.format("%s transport (%s)", transportType, baseUrl); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java new file mode 100644 index 0000000000..77d702f779 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config.transport; + +import lombok.Data; +import org.thingsboard.monitoring.config.MonitoringConfig; + +import java.util.List; + +@Data +public abstract class TransportMonitoringConfig implements MonitoringConfig { + + private int requestTimeoutMs; + + private List targets; + + public abstract TransportType getTransportType(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java new file mode 100644 index 0000000000..816f64fbce --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config.transport; + +import lombok.Data; +import org.thingsboard.monitoring.config.MonitoringTarget; + +import java.util.UUID; + +@Data +public class TransportMonitoringTarget implements MonitoringTarget { + + private String baseUrl; + private DeviceConfig device; // set manually during initialization + + @Override + public UUID getDeviceId() { + return device.getId(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java new file mode 100644 index 0000000000..eeb085348b --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.config.transport; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.CoapTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.HttpTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.Lwm2mTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.MqttTransportHealthChecker; + +@AllArgsConstructor +@Getter +public enum TransportType { + + MQTT(MqttTransportHealthChecker.class), + COAP(CoapTransportHealthChecker.class), + HTTP(HttpTransportHealthChecker.class), + LWM2M(Lwm2mTransportHealthChecker.class); + + private final Class> serviceClass; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java new file mode 100644 index 0000000000..3370d42462 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data; + +public class Latencies { + + public static final String WS_UPDATE = "wsUpdate"; + public static final String WS_CONNECT = "wsConnect"; + public static final String LOG_IN = "logIn"; + + public static String request(String key) { + return String.format("%sRequest", key); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java new file mode 100644 index 0000000000..c64291e30f --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data; + +import com.google.common.util.concurrent.AtomicDouble; +import lombok.RequiredArgsConstructor; + +import java.util.concurrent.atomic.AtomicInteger; + +@RequiredArgsConstructor +public class Latency { + + private final String key; + private final AtomicDouble latencySum = new AtomicDouble(); + private final AtomicInteger counter = new AtomicInteger(); + + public synchronized void report(double latencyInMs) { + latencySum.addAndGet(latencyInMs); + counter.incrementAndGet(); + } + + public synchronized double getAvg() { + return latencySum.get() / counter.get(); + } + + public boolean isNotEmpty() { + return counter.get() > 0; + } + + public synchronized void reset() { + latencySum.set(0.0); + counter.set(0); + } + + public String getKey() { + return key; + } + + public synchronized Latency snapshot() { + Latency snapshot = new Latency(key); + snapshot.latencySum.set(latencySum.get()); + snapshot.counter.set(counter.get()); + return snapshot; + } + + @Override + public String toString() { + return "Latency{" + + "key='" + key + '\'' + + ", avgLatency=" + getAvg() + + '}'; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java new file mode 100644 index 0000000000..825162952f --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data; + +public class MonitoredServiceKey { + + public static final String GENERAL = "Monitoring"; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java new file mode 100644 index 0000000000..dc91a56a3c --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data; + +public class ServiceFailureException extends RuntimeException { + + public ServiceFailureException(Throwable cause) { + super(cause.getMessage(), cause); + } + + public ServiceFailureException(String message) { + super(message); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/CmdsWrapper.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/CmdsWrapper.java new file mode 100644 index 0000000000..34227150ea --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/CmdsWrapper.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.cmd; + +import lombok.Data; + +import java.util.List; + +@Data +public class CmdsWrapper { + + private List entityDataCmds; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataCmd.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataCmd.java new file mode 100644 index 0000000000..ccd6fa8f7c --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataCmd.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.cmd; + +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +@Data +public class EntityDataCmd { + + private int cmdId; + private EntityDataQuery query; + private LatestValueCmd latestCmd; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java new file mode 100644 index 0000000000..5f3a466217 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.cmd; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.TsValue; + +import java.util.List; +import java.util.UUID; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class EntityDataUpdate { + + @JsonIgnoreProperties(ignoreUnknown = true) + private List update; + + public String getLatest(UUID entityId, String key) { + if (update == null) return null; + + return update.stream() + .filter(entityData -> entityData.getEntityId().getId().equals(entityId)).findFirst() + .map(EntityData::getLatest).map(latest -> latest.get(EntityKeyType.TIME_SERIES)) + .map(latest -> latest.get(key)).map(TsValue::getValue) + .orElse(null); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/LatestValueCmd.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/LatestValueCmd.java new file mode 100644 index 0000000000..5c2cffcbb6 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/LatestValueCmd.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.cmd; + +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityKey; + +import java.util.List; + +@Data +public class LatestValueCmd { + + private List keys; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java new file mode 100644 index 0000000000..939cb7440f --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +import org.thingsboard.monitoring.data.Latency; + +import java.util.Collection; + +public class HighLatencyNotification implements Notification { + + private final Collection latencies; + private final int thresholdMs; + + public HighLatencyNotification(Collection latencies, int thresholdMs) { + this.latencies = latencies; + this.thresholdMs = thresholdMs; + } + + @Override + public String getText() { + StringBuilder text = new StringBuilder(); + text.append("Some of the latencies are higher than ").append(thresholdMs).append(" ms:\n"); + latencies.forEach(latency -> { + text.append(String.format("[%s] %,.2f ms\n", latency.getKey(), latency.getAvg())); + }); + return text.toString(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java new file mode 100644 index 0000000000..21e5994d84 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +public interface Notification { + + String getText(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java new file mode 100644 index 0000000000..4b78d96a63 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +import lombok.Getter; +import org.apache.commons.lang3.exception.ExceptionUtils; + +@Getter +public class ServiceFailureNotification implements Notification { + + private final Object serviceKey; + private final Throwable error; + private final int failuresCount; + + public ServiceFailureNotification(Object serviceKey, Throwable error, int failuresCount) { + this.serviceKey = serviceKey; + this.error = error; + this.failuresCount = failuresCount; + } + + @Override + public String getText() { + String errorMsg = error.getMessage(); + if (errorMsg == null || errorMsg.equals("null")) { + Throwable cause = ExceptionUtils.getRootCause(error); + if (cause != null) { + errorMsg = cause.getMessage(); + } + } + if (errorMsg == null) { + errorMsg = error.getClass().getSimpleName(); + } + return String.format("[%s] Failure: %s (number of subsequent failures: %s)", serviceKey, errorMsg, failuresCount); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java new file mode 100644 index 0000000000..44eaf093cc --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +public class ServiceRecoveryNotification implements Notification { + + private final Object serviceKey; + + public ServiceRecoveryNotification(Object serviceKey) { + this.serviceKey = serviceKey; + } + + @Override + public String getText() { + return String.format("[%s] is OK", serviceKey); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java new file mode 100644 index 0000000000..f882105048 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.notification; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.data.notification.Notification; +import org.thingsboard.monitoring.notification.channels.NotificationChannel; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +@Component +@RequiredArgsConstructor +@Slf4j +public class NotificationService { + + private final List notificationChannels; + private final ExecutorService notificationExecutor = Executors.newSingleThreadExecutor(); + + public void sendNotification(Notification notification) { + forEachNotificationChannel(notificationChannel -> notificationChannel.sendNotification(notification)); + } + + private void forEachNotificationChannel(Consumer function) { + notificationChannels.forEach(notificationChannel -> { + notificationExecutor.submit(() -> { + try { + function.accept(notificationChannel); + } catch (Exception e) { + log.error("Failed to send notification to {}", notificationChannel.getClass().getSimpleName(), e); + } + }); + }); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java new file mode 100644 index 0000000000..6c1c23cc28 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.notification.channels; + +import org.thingsboard.monitoring.data.notification.Notification; + +public interface NotificationChannel { + + void sendNotification(Notification notification); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java new file mode 100644 index 0000000000..092ea0b8c6 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.notification.channels.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.monitoring.data.notification.Notification; +import org.thingsboard.monitoring.notification.channels.NotificationChannel; + +import javax.annotation.PostConstruct; +import java.time.Duration; +import java.util.Map; + +@Component +@ConditionalOnProperty(value = "monitoring.notification_channels.slack.enabled", havingValue = "true") +@Slf4j +public class SlackNotificationChannel implements NotificationChannel { + + @Value("${monitoring.notification_channels.slack.webhook_url}") + private String webhookUrl; + + private RestTemplate restTemplate; + + @PostConstruct + private void init() { + restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(2)) + .build(); + } + + @Override + public void sendNotification(Notification notification) { + sendNotification(notification.getText()); + } + + private void sendNotification(String message) { + restTemplate.postForObject(webhookUrl, Map.of("text", message), String.class); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java new file mode 100644 index 0000000000..affca1ce09 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.thingsboard.monitoring.client.TbClient; +import org.thingsboard.monitoring.client.WsClient; +import org.thingsboard.monitoring.config.MonitoringConfig; +import org.thingsboard.monitoring.config.MonitoringTarget; +import org.thingsboard.monitoring.data.Latencies; +import org.thingsboard.monitoring.data.MonitoredServiceKey; +import org.thingsboard.monitoring.data.ServiceFailureException; +import org.thingsboard.monitoring.util.TbStopWatch; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.UUID; + +@RequiredArgsConstructor +@Slf4j +public abstract class BaseHealthChecker { + + protected final C config; + protected final T target; + + private Object info; + + @Autowired + private MonitoringReporter reporter; + @Autowired + private TbStopWatch stopWatch; + @Value("${monitoring.check_timeout_ms}") + private int resultCheckTimeoutMs; + + public static final String TEST_TELEMETRY_KEY = "testData"; + + @PostConstruct + private void init() { + info = getInfo(); + } + + protected abstract void initialize(TbClient tbClient); + + public final void check(WsClient wsClient) { + log.debug("[{}] Checking", info); + try { + wsClient.registerWaitForUpdate(); + + String testValue = UUID.randomUUID().toString(); + String testPayload = createTestPayload(testValue); + try { + initClient(); + stopWatch.start(); + sendTestPayload(testPayload); + reporter.reportLatency(Latencies.request(getKey()), stopWatch.getTime()); + log.trace("[{}] Sent test payload ({})", info, testPayload); + } catch (Throwable e) { + throw new ServiceFailureException(e); + } + + log.trace("[{}] Waiting for WS update", info); + checkWsUpdate(wsClient, testValue); + + reporter.serviceIsOk(info); + reporter.serviceIsOk(MonitoredServiceKey.GENERAL); + } catch (ServiceFailureException serviceFailureException) { + reporter.serviceFailure(info, serviceFailureException); + } catch (Exception e) { + reporter.serviceFailure(MonitoredServiceKey.GENERAL, e); + } + } + + private void checkWsUpdate(WsClient wsClient, String testValue) { + stopWatch.start(); + wsClient.waitForUpdate(resultCheckTimeoutMs); + log.trace("[{}] Waited for WS update. Last WS msg: {}", info, wsClient.lastMsg); + Object update = wsClient.getTelemetryUpdate(target.getDeviceId(), TEST_TELEMETRY_KEY); + if (update == null) { + throw new ServiceFailureException("No WS update arrived within " + resultCheckTimeoutMs + " ms"); + } else if (!update.toString().equals(testValue)) { + throw new ServiceFailureException("Was expecting value " + testValue + " but got " + update); + } + reporter.reportLatency(Latencies.WS_UPDATE, stopWatch.getTime()); + } + + + protected abstract void initClient() throws Exception; + + protected abstract String createTestPayload(String testValue); + + protected abstract void sendTestPayload(String payload) throws Exception; + + @PreDestroy + protected abstract void destroyClient() throws Exception; + + protected abstract Object getInfo(); + protected abstract String getKey(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java new file mode 100644 index 0000000000..d286d9faa1 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.thingsboard.monitoring.client.TbClient; +import org.thingsboard.monitoring.client.WsClient; +import org.thingsboard.monitoring.client.WsClientFactory; +import org.thingsboard.monitoring.config.MonitoringConfig; +import org.thingsboard.monitoring.config.MonitoringTarget; +import org.thingsboard.monitoring.data.Latencies; +import org.thingsboard.monitoring.data.MonitoredServiceKey; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; +import org.thingsboard.monitoring.util.TbStopWatch; + +import javax.annotation.PostConstruct; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +@Slf4j +public abstract class BaseMonitoringService, T extends MonitoringTarget> { + + @Autowired + private List configs; + private final List> healthCheckers = new LinkedList<>(); + private final List devices = new LinkedList<>(); + + @Autowired + private TbClient tbClient; + @Autowired + private WsClientFactory wsClientFactory; + @Autowired + private TbStopWatch stopWatch; + @Autowired + private MonitoringReporter reporter; + @Autowired + protected ApplicationContext applicationContext; + + @PostConstruct + private void init() { + tbClient.logIn(); + configs.forEach(config -> { + config.getTargets().forEach(target -> { + BaseHealthChecker healthChecker = (BaseHealthChecker) createHealthChecker(config, target); + log.info("Initializing {}", healthChecker.getClass().getSimpleName()); + healthChecker.initialize(tbClient); + devices.add(target.getDeviceId()); + healthCheckers.add(healthChecker); + }); + }); + } + + public final void runChecks() { + if (healthCheckers.isEmpty()) { + return; + } + try { + log.info("Starting {}", getName()); + stopWatch.start(); + String accessToken = tbClient.logIn(); + reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); + + try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { + wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); + + for (BaseHealthChecker healthChecker : healthCheckers) { + healthChecker.check(wsClient); + } + } + reporter.reportLatencies(tbClient); + log.debug("Finished {}", getName()); + } catch (Throwable error) { + try { + reporter.serviceFailure(MonitoredServiceKey.GENERAL, error); + } catch (Throwable reportError) { + log.error("Error occurred during service failure reporting", reportError); + } + } + } + + protected abstract BaseHealthChecker createHealthChecker(C config, T target); + + protected abstract String getName(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java new file mode 100644 index 0000000000..f41454a76a --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service; + +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.client.TbClient; +import org.thingsboard.monitoring.data.Latency; +import org.thingsboard.monitoring.data.MonitoredServiceKey; +import org.thingsboard.monitoring.data.notification.HighLatencyNotification; +import org.thingsboard.monitoring.data.notification.ServiceFailureNotification; +import org.thingsboard.monitoring.data.notification.ServiceRecoveryNotification; +import org.thingsboard.monitoring.notification.NotificationService; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +@Slf4j +public class MonitoringReporter { + + private final NotificationService notificationService; + + private final Map latencies = new ConcurrentHashMap<>(); + private final Map failuresCounters = new ConcurrentHashMap<>(); + + @Value("${monitoring.failures_threshold}") + private int failuresThreshold; + @Value("${monitoring.repeated_failure_notification}") + private int repeatedFailureNotification; + + @Value("${monitoring.latency.enabled}") + private boolean latencyReportingEnabled; + @Value("${monitoring.latency.threshold_ms}") + private int latencyThresholdMs; + @Value("${monitoring.latency.reporting_asset_id}") + private String reportingAssetId; + + public void reportLatencies(TbClient tbClient) { + List latencies = this.latencies.values().stream() + .filter(Latency::isNotEmpty) + .map(latency -> { + Latency snapshot = latency.snapshot(); + latency.reset(); + return snapshot; + }) + .collect(Collectors.toList()); + if (latencies.isEmpty()) { + return; + } + log.info("Latencies:\n{}", latencies.stream().map(latency -> latency.getKey() + ": " + latency.getAvg() + " ms") + .collect(Collectors.joining("\n")) + "\n"); + + if (!latencyReportingEnabled) return; + + if (latencies.stream().anyMatch(latency -> latency.getAvg() >= (double) latencyThresholdMs)) { + HighLatencyNotification highLatencyNotification = new HighLatencyNotification(latencies, latencyThresholdMs); + notificationService.sendNotification(highLatencyNotification); + } + + try { + if (StringUtils.isBlank(reportingAssetId)) { + String assetName = "[Monitoring] Latencies"; + Asset monitoringAsset = tbClient.findAsset(assetName).orElseGet(() -> { + Asset asset = new Asset(); + asset.setType("Monitoring"); + asset.setName(assetName); + asset = tbClient.saveAsset(asset); + log.info("Created monitoring asset {}", asset.getId()); + return asset; + }); + reportingAssetId = monitoringAsset.getId().toString(); + } + + ObjectNode msg = JacksonUtil.newObjectNode(); + latencies.forEach(latency -> { + msg.set(latency.getKey(), new DoubleNode(latency.getAvg())); + }); + tbClient.saveEntityTelemetry(new AssetId(UUID.fromString(reportingAssetId)), "time", msg); + } catch (Exception e) { + log.error("Failed to report latencies: {}", e.getMessage()); + } + } + + public void reportLatency(String key, long latencyInNanos) { + String latencyKey = key + "Latency"; + double latencyInMs = (double) latencyInNanos / 1000_000; + log.trace("Reporting latency [{}]: {} ms", key, latencyInMs); + latencies.computeIfAbsent(latencyKey, k -> new Latency(latencyKey)).report(latencyInMs); + } + + public void serviceFailure(Object serviceKey, Throwable error) { + if (log.isDebugEnabled()) { + log.error("Error occurred", error); + } + int failuresCount = failuresCounters.computeIfAbsent(serviceKey, k -> new AtomicInteger()).incrementAndGet(); + ServiceFailureNotification notification = new ServiceFailureNotification(serviceKey, error, failuresCount); + log.error(notification.getText()); + if (failuresCount == failuresThreshold || (repeatedFailureNotification != 0 && failuresCount % repeatedFailureNotification == 0)) { + notificationService.sendNotification(notification); + } + } + + public void serviceIsOk(Object serviceKey) { + ServiceRecoveryNotification notification = new ServiceRecoveryNotification(serviceKey); + if (!serviceKey.equals(MonitoredServiceKey.GENERAL)) { + log.info(notification.getText()); + } + AtomicInteger failuresCounter = failuresCounters.get(serviceKey); + if (failuresCounter != null) { + if (failuresCounter.get() >= failuresThreshold) { + notificationService.sendNotification(notification); + } + failuresCounter.set(0); + } + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java new file mode 100644 index 0000000000..c822720f23 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java @@ -0,0 +1,139 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service.transport; + +import com.fasterxml.jackson.databind.node.TextNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.client.TbClient; +import org.thingsboard.monitoring.config.transport.DeviceConfig; +import org.thingsboard.monitoring.config.transport.TransportInfo; +import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.service.BaseHealthChecker; +import org.thingsboard.monitoring.util.ResourceUtils; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; + +@Slf4j +public abstract class TransportHealthChecker extends BaseHealthChecker { + + private static final String DEFAULT_DEVICE_NAME = "[Monitoring] %s transport (%s)"; + private static final String DEFAULT_PROFILE_NAME = "[Monitoring] %s"; + + public TransportHealthChecker(C config, TransportMonitoringTarget target) { + super(config, target); + } + + @Override + protected void initialize(TbClient tbClient) { + String deviceName = String.format(DEFAULT_DEVICE_NAME, config.getTransportType(), target.getBaseUrl()); + Device device = tbClient.getTenantDevice(deviceName) + .orElseGet(() -> { + log.info("Creating new device '{}'", deviceName); + return createDevice(config.getTransportType(), deviceName, tbClient); + }); + DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(device.getId()) + .orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + device.getId())); + + DeviceConfig deviceConfig = new DeviceConfig(); + deviceConfig.setId(device.getId().toString()); + deviceConfig.setName(deviceName); + deviceConfig.setCredentials(credentials); + target.setDevice(deviceConfig); + } + + @Override + protected String createTestPayload(String testValue) { + return JacksonUtil.newObjectNode().set(TEST_TELEMETRY_KEY, new TextNode(testValue)).toString(); + } + + @Override + protected Object getInfo() { + return new TransportInfo(getTransportType(), target.getBaseUrl()); + } + + @Override + protected String getKey() { + return getTransportType().name().toLowerCase() + "Transport"; + } + + protected abstract TransportType getTransportType(); + + + private Device createDevice(TransportType transportType, String name, TbClient tbClient) { + Device device = new Device(); + device.setName(name); + + DeviceCredentials credentials = new DeviceCredentials(); + credentials.setCredentialsId(RandomStringUtils.randomAlphabetic(20)); + + DeviceData deviceData = new DeviceData(); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + if (transportType != TransportType.LWM2M) { + device.setType("default"); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + } else { + tbClient.getResources(new PageLink(1, 0, "lwm2m monitoring")).getData() + .stream().findFirst() + .orElseGet(() -> { + TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); + log.info("Creating LwM2M resource"); + return tbClient.saveResource(newResource); + }); + String profileName = String.format(DEFAULT_PROFILE_NAME, transportType); + DeviceProfile profile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData() + .stream().findFirst() + .orElseGet(() -> { + DeviceProfile newProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); + newProfile.setName(profileName); + log.info("Creating LwM2M device profile"); + return tbClient.saveDeviceProfile(newProfile); + }); + device.setType(profileName); + device.setDeviceProfileId(profile.getId()); + deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); + + credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); + LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials(); + NoSecClientCredential client = new NoSecClientCredential(); + client.setEndpoint(credentials.getCredentialsId()); + lwm2mCreds.setClient(client); + LwM2MBootstrapClientCredentials bootstrap = new LwM2MBootstrapClientCredentials(); + bootstrap.setBootstrapServer(new NoSecBootstrapClientCredential()); + bootstrap.setLwm2mServer(new NoSecBootstrapClientCredential()); + lwm2mCreds.setBootstrap(bootstrap); + credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); + } + return tbClient.saveDeviceWithCredentials(device, credentials).get(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java new file mode 100644 index 0000000000..b3ce86e799 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service.transport; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.service.BaseHealthChecker; +import org.thingsboard.monitoring.service.BaseMonitoringService; + +@Service +@RequiredArgsConstructor +@Slf4j +public final class TransportsMonitoringService extends BaseMonitoringService { + + @Override + protected BaseHealthChecker createHealthChecker(TransportMonitoringConfig config, TransportMonitoringTarget target) { + return applicationContext.getBean(config.getTransportType().getServiceClass(), config, target); + } + + @Override + protected String getName() { + return "transports check"; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java new file mode 100644 index 0000000000..f56415fa6a --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service.transport.impl; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.config.transport.CoapTransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; + +import java.io.IOException; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@Slf4j +public class CoapTransportHealthChecker extends TransportHealthChecker { + + private CoapClient coapClient; + + protected CoapTransportHealthChecker(CoapTransportMonitoringConfig config, TransportMonitoringTarget target) { + super(config, target); + } + + @Override + protected void initClient() throws Exception { + if (coapClient == null) { + String accessToken = target.getDevice().getCredentials().getCredentialsId(); + String uri = target.getBaseUrl() + "/api/v1/" + accessToken + "/telemetry"; + coapClient = new CoapClient(uri); + coapClient.setTimeout((long) config.getRequestTimeoutMs()); + log.debug("Initialized CoAP client for URI {}", uri); + } + } + + @Override + protected void sendTestPayload(String payload) throws Exception { + CoapResponse response = coapClient.post(payload, MediaTypeRegistry.APPLICATION_JSON); + CoAP.ResponseCode code = response.getCode(); + if (code.codeClass != CoAP.CodeClass.SUCCESS_RESPONSE.value) { + throw new IOException("COAP client didn't receive success response from transport"); + } + } + + @Override + protected void destroyClient() throws Exception { + if (coapClient != null) { + coapClient.shutdown(); + coapClient = null; + log.info("Disconnected CoAP client"); + } + } + + @Override + protected TransportType getTransportType() { + return TransportType.COAP; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/HttpTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/HttpTransportHealthChecker.java new file mode 100644 index 0000000000..4cddc59401 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/HttpTransportHealthChecker.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service.transport.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.monitoring.config.transport.HttpTransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; + +import java.time.Duration; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@Slf4j +public class HttpTransportHealthChecker extends TransportHealthChecker { + + private RestTemplate restTemplate; + + protected HttpTransportHealthChecker(HttpTransportMonitoringConfig config, TransportMonitoringTarget target) { + super(config, target); + } + + @Override + protected void initClient() throws Exception { + if (restTemplate == null) { + restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.ofMillis(config.getRequestTimeoutMs())) + .setReadTimeout(Duration.ofMillis(config.getRequestTimeoutMs())) + .build(); + log.debug("Initialized HTTP client"); + } + } + + @Override + protected void sendTestPayload(String payload) throws Exception { + String accessToken = target.getDevice().getCredentials().getCredentialsId(); + restTemplate.postForObject(target.getBaseUrl() + "/api/v1/" + accessToken + "/telemetry", payload, String.class); + } + + @Override + protected void destroyClient() throws Exception {} + + @Override + protected TransportType getTransportType() { + return TransportType.HTTP; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/Lwm2mTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/Lwm2mTransportHealthChecker.java new file mode 100644 index 0000000000..44a061a6fb --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/Lwm2mTransportHealthChecker.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service.transport.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; +import org.thingsboard.monitoring.client.Lwm2mClient; +import org.thingsboard.monitoring.config.transport.Lwm2mTransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; + +@Service +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@Slf4j +public class Lwm2mTransportHealthChecker extends TransportHealthChecker { + + private Lwm2mClient lwm2mClient; + + protected Lwm2mTransportHealthChecker(Lwm2mTransportMonitoringConfig config, TransportMonitoringTarget target) { + super(config, target); + } + + @Override + protected void initClient() throws Exception { + if (lwm2mClient == null || lwm2mClient.getLeshanClient() == null || lwm2mClient.isDestroyed()) { + String endpoint = target.getDevice().getCredentials().getCredentialsId(); + lwm2mClient = new Lwm2mClient(target.getBaseUrl(), endpoint); + lwm2mClient.initClient(); + log.debug("Initialized LwM2M client for endpoint '{}'", endpoint); + } + } + + @Override + protected void sendTestPayload(String payload) throws Exception { + lwm2mClient.send(payload, 0); + } + + @Override + protected String createTestPayload(String testValue) { + return testValue; + } + + @Override + protected void destroyClient() throws Exception { + if (lwm2mClient != null) { + lwm2mClient.destroy(); + lwm2mClient = null; + } + } + + @Override + protected TransportType getTransportType() { + return TransportType.LWM2M; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/MqttTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/MqttTransportHealthChecker.java new file mode 100644 index 0000000000..ec7c81642c --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/MqttTransportHealthChecker.java @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service.transport.impl; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.config.transport.MqttTransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@Slf4j +public class MqttTransportHealthChecker extends TransportHealthChecker { + + private MqttClient mqttClient; + + private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry"; + + protected MqttTransportHealthChecker(MqttTransportMonitoringConfig config, TransportMonitoringTarget target) { + super(config, target); + } + + @Override + protected void initClient() throws Exception { + if (mqttClient == null || !mqttClient.isConnected()) { + String clientId = MqttAsyncClient.generateClientId(); + String accessToken = target.getDevice().getCredentials().getCredentialsId(); + mqttClient = new MqttClient(target.getBaseUrl(), clientId, new MemoryPersistence()); + mqttClient.setTimeToWait(config.getRequestTimeoutMs()); + + MqttConnectOptions options = new MqttConnectOptions(); + options.setUserName(accessToken); + options.setConnectionTimeout(config.getRequestTimeoutMs() / 1000); + IMqttToken result = mqttClient.connectWithResult(options); + if (result.getException() != null) { + throw result.getException(); + } + log.debug("Initialized MQTT client for URI {}", mqttClient.getServerURI()); + } + } + + @Override + protected void sendTestPayload(String payload) throws Exception { + MqttMessage message = new MqttMessage(); + message.setPayload(payload.getBytes()); + message.setQos(config.getQos()); + mqttClient.publish(DEVICE_TELEMETRY_TOPIC, message); + } + + @Override + protected void destroyClient() throws Exception { + if (mqttClient != null) { + mqttClient.disconnect(); + mqttClient = null; + log.info("Disconnected MQTT client"); + } + } + + @Override + protected TransportType getTransportType() { + return TransportType.MQTT; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java b/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java new file mode 100644 index 0000000000..4cc1efe8cb --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.util; + +import lombok.SneakyThrows; +import org.thingsboard.common.util.JacksonUtil; + +import java.io.InputStream; + +public class ResourceUtils { + + @SneakyThrows + public static T getResource(String path, Class type) { + InputStream resource = ResourceUtils.class.getClassLoader().getResourceAsStream(path); + if (resource == null) { + throw new IllegalArgumentException("Resource not found for path " + path); + } + return JacksonUtil.OBJECT_MAPPER.readValue(resource, type); + } + + public static InputStream getResourceAsStream(String path) { + InputStream resource = ResourceUtils.class.getClassLoader().getResourceAsStream(path); + if (resource == null) { + throw new IllegalArgumentException("Resource not found for path " + path); + } + return resource; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/util/TbStopWatch.java b/monitoring/src/main/java/org/thingsboard/monitoring/util/TbStopWatch.java new file mode 100644 index 0000000000..49e387999a --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/util/TbStopWatch.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.util; + +import org.apache.commons.lang3.time.StopWatch; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TbStopWatch { + + private final StopWatch internal = new StopWatch(); + + public void start() { + internal.reset(); + internal.start(); + } + + public long getTime() { + internal.stop(); + long nanoTime = internal.getNanoTime(); + internal.reset(); + return nanoTime; + } + +} diff --git a/monitoring/src/main/resources/logback.xml b/monitoring/src/main/resources/logback.xml new file mode 100644 index 0000000000..a7ca84126f --- /dev/null +++ b/monitoring/src/main/resources/logback.xml @@ -0,0 +1,38 @@ + + + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + diff --git a/monitoring/src/main/resources/lwm2m/device_profile.json b/monitoring/src/main/resources/lwm2m/device_profile.json new file mode 100644 index 0000000000..095f4859e9 --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/device_profile.json @@ -0,0 +1,58 @@ +{ + "type": "DEFAULT", + "image": null, + "defaultQueueName": null, + "transportType": "LWM2M", + "provisionType": "DISABLED", + "description": "", + "profileData": { + "configuration": { + "type": "DEFAULT" + }, + "transportConfiguration": { + "observeAttr": { + "observe": [ + "/3_1.0/0/0" + ], + "attribute": [], + "telemetry": [ + "/3_1.0/0/0" + ], + "keyName": { + "/3_1.0/0/0": "testData" + }, + "attributeLwm2m": {} + }, + "bootstrap": [ + { + "shortServerId": 123, + "bootstrapServerIs": false, + "host": "0.0.0.0", + "port": 5685, + "clientHoldOffTime": 1, + "serverPublicKey": "", + "serverCertificate": "", + "bootstrapServerAccountTimeout": 0, + "lifetime": 300, + "defaultMinPeriod": 1, + "notifIfDisabled": true, + "binding": "U", + "securityMode": "NO_SEC" + } + ], + "clientLwM2mSettings": { + "clientOnlyObserveAfterConnect": 1, + "fwUpdateStrategy": 1, + "swUpdateStrategy": 1, + "powerMode": "DRX", + "compositeOperationsSupport": false + }, + "bootstrapServerUpdateEnable": false, + "type": "LWM2M" + }, + "alarms": null, + "provisionConfiguration": { + "type": "DISABLED" + } + } +} \ No newline at end of file diff --git a/monitoring/src/main/resources/lwm2m/models/0.xml b/monitoring/src/main/resources/lwm2m/models/0.xml new file mode 100644 index 0000000000..d122984c16 --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/models/0.xml @@ -0,0 +1,364 @@ + + + + + LWM2M Security + + 0 + urn:oma:lwm2m:oma:0:1.2 + 1.1 + 1.2 + Multiple + Mandatory + + + LWM2M Server URI + + Single + Mandatory + String + 0..255 + + + + + Bootstrap-Server + + Single + Mandatory + Boolean + + + + + + Security Mode + + Single + Mandatory + Integer + 0..4 + + + + + Public Key or Identity + + Single + Mandatory + Opaque + + + + + + Server Public Key + + Single + Mandatory + Opaque + + + + + + Secret Key + + Single + Mandatory + Opaque + + + + + + SMS Security Mode + + Single + Optional + Integer + 0..255 + + + + + SMS Binding Key Parameters + + Single + Optional + Opaque + 6 + + + + + SMS Binding Secret Key(s) + + Single + Optional + Opaque + 16,32,48 + + + + + LwM2M Server SMS Number + + Single + Optional + String + + + + + + Short Server ID + + Single + Optional + Integer + 1..65534 + + + + + Client Hold Off Time + + Single + Optional + Integer + + s + + + + Bootstrap-Server Account Timeout + + Single + Optional + Integer + + s + + + + Matching Type + + Single + Optional + Integer + 0..3 + + + + + SNI + + Single + Optional + String + + + + + + Certificate Usage + + Single + Optional + Integer + 0..3 + + + + + DTLS/TLS Ciphersuite + + Multiple + Optional + Integer + + + + + OSCORE Security Mode + + Single + Optional + Objlnk + + + + + + Groups To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithms Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + Signature Algorithms To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithm Certs Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + + TLS 1.3 Features To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions Supported by Server + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + Secondary LwM2M Server URI + + Multiple + Optional + String + 0..255 + + + + MQTT Server + + Single + Optional + Objlnk + + + + + LwM2M COSE Security + + Multiple + Optional + Objlnk + + + + + RDS Destination Port + + Single + Optional + Integer + 0..15 + + + + RDS Source Port + + Single + Optional + Integer + 0..15 + + + + RDS Application ID + + Single + Optional + String + + + + + + + + diff --git a/monitoring/src/main/resources/lwm2m/models/1.xml b/monitoring/src/main/resources/lwm2m/models/1.xml new file mode 100644 index 0000000000..a81caa27b0 --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/models/1.xml @@ -0,0 +1,319 @@ + + + + + LwM2M Server + + 1 + urn:oma:lwm2m:oma:1:1.2 + 1.2 + 1.2 + Multiple + Mandatory + + + Short Server ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Lifetime + RW + Single + Mandatory + Integer + + s + + + + Default Minimum Period + RW + Single + Optional + Integer + + s + + + + Default Maximum Period + RW + Single + Optional + Integer + + s + + + + Disable + E + Single + Optional + + + + + + + Disable Timeout + RW + Single + Optional + Integer + + s + + + + Notification Storing When Disabled or Offline + RW + Single + Mandatory + Boolean + + + + + + Binding + RW + Single + Mandatory + String + + + + + + Registration Update Trigger + E + Single + Mandatory + + + + + + + Bootstrap-Request Trigger + E + Single + Optional + + + + + + + APN Link + RW + Single + Optional + Objlnk + + + + + + TLS-DTLS Alert Code + R + Single + Optional + Integer + 0..255 + + + + + Last Bootstrapped + R + Single + Optional + Time + + + + + + Registration Priority Order + R + Single + Optional + Integer + + + + + + Initial Registration Delay Timer + RW + Single + Optional + Integer + + s + + + + Registration Failure Block + R + Single + Optional + Boolean + + + + + + Bootstrap on Registration Failure + R + Single + Optional + Boolean + + + + + + Communication Retry Count + RW + Single + Optional + Integer + + + + + + Communication Retry Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Delay Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Retry Count + RW + Single + Optional + Integer + + + + + + Trigger + RW + Single + Optional + Boolean + + + + + + Preferred Transport + RW + Single + Optional + String + The possible values are those listed in the LwM2M Core Specification + + + + Mute Send + RW + Single + Optional + Boolean + + + + + + Alternate APN Links + RW + Multiple + Optional + Objlnk + + + + + + Supported Server Versions + RW + Multiple + Optional + String + + + + + + Default Notification Mode + RW + Single + Optional + Integer + 0..1 + + + + + Profile ID Hash Algorithm + RW + Single + Optional + Integer + 0..255 + + + + + + + diff --git a/monitoring/src/main/resources/lwm2m/models/2.xml b/monitoring/src/main/resources/lwm2m/models/2.xml new file mode 100644 index 0000000000..79b2ed1321 --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/models/2.xml @@ -0,0 +1,83 @@ + + + + + LwM2M Access Control + + 2 + urn:oma:lwm2m:oma:2:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Object ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Object Instance ID + R + Single + Mandatory + Integer + 0..65535 + + + + + ACL + RW + Multiple + Optional + Integer + 0..31 + + + + + Access Control Owner + RW + Single + Mandatory + Integer + 0..65535 + + + + + + + diff --git a/monitoring/src/main/resources/lwm2m/models/test-model.xml b/monitoring/src/main/resources/lwm2m/models/test-model.xml new file mode 100644 index 0000000000..c8e49ce32c --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/models/test-model.xml @@ -0,0 +1,45 @@ + + + + + LwM2M Monitoring + + + 3 + urn:oma:lwm2m:oma:3:1.0 + 1.1 + 1.0 + Single + Mandatory + + + Test data + R + Single + Optional + String + + + + + + + + diff --git a/monitoring/src/main/resources/lwm2m/resource.json b/monitoring/src/main/resources/lwm2m/resource.json new file mode 100644 index 0000000000..b624fa400e --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/resource.json @@ -0,0 +1,6 @@ +{ + "title": "", + "resourceType": "LWM2M_MODEL", + "fileName": "test-model.xml", + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjIgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozOjEuMDwvT2JqZWN0VVJOPgogICAgICAgIDxMV00yTVZlcnNpb24+MS4xPC9MV00yTVZlcnNpb24+CiAgICAgICAgPE9iamVjdFZlcnNpb24+MS4wPC9PYmplY3RWZXJzaW9uPgogICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgPFJlc291cmNlcz4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjAiPgogICAgICAgICAgICAgICAgPE5hbWU+VGVzdCBkYXRhPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbVGVzdCBkYXRhXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgIDwvUmVzb3VyY2VzPgogICAgICAgIDxEZXNjcmlwdGlvbjI+PC9EZXNjcmlwdGlvbjI+CiAgICA8L09iamVjdD4KPC9MV00yTT4K" +} \ No newline at end of file diff --git a/monitoring/src/main/resources/tb-monitoring.yml b/monitoring/src/main/resources/tb-monitoring.yml new file mode 100644 index 0000000000..88979bff1a --- /dev/null +++ b/monitoring/src/main/resources/tb-monitoring.yml @@ -0,0 +1,105 @@ +# +# Copyright © 2016-2023 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +monitoring: + # Monitored server domain + domain: "${DOMAIN:localhost}" + rest: + # Base REST API url, https://DOMAIN by default + base_url: '${REST_BASE_URL:https://${monitoring.domain}}' + # Authentication username + username: '${REST_AUTH_USERNAME:tenant@thingsboard.org}' + # Authentication password + password: '${REST_AUTH_PASSWORD:tenant}' + # REST request timeout in milliseconds + request_timeout_ms: '${REST_REQUEST_TIMEOUT_MS:5000}' + ws: + # WebSocket url, wss://DOMAIN by default + base_url: '${WS_BASE_URL:wss://${monitoring.domain}}' + # WebSocket request timeout + request_timeout_ms: '${WS_REQUEST_TIMEOUT_MS:3000}' + + # Checks frequency in milliseconds + monitoring_rate_ms: '${MONITORING_RATE_MS:10000}' + # Maximum time between request to transport and WebSocket update + check_timeout_ms: '${CHECK_TIMEOUT_MS:5000}' + + # Failures threshold for notifying + failures_threshold: '${FAILURES_THRESHOLD:2}' + # Notify after each REPEATED_FAILURE_NOTIFICATION subsequent failures, 0 to notify only once on first failure + repeated_failure_notification: '${REPEATED_FAILURE_NOTIFICATION:4}' + + transports: + mqtt: + # Enable MQTT transport checks + enabled: '${MQTT_TRANSPORT_MONITORING_ENABLED:true}' + # MQTT request timeout in milliseconds + request_timeout_ms: '${MQTT_REQUEST_TIMEOUT_MS:4000}' + # MQTT QoS + qos: '${MQTT_QOS_LEVEL:1}' + targets: + # MQTT transport base url, tcp://DOMAIN:1883 by default + - base_url: '${MQTT_TRANSPORT_BASE_URL:tcp://${monitoring.domain}:1883}' + # To add more targets, use following environment variables: + # monitoring.transports.mqtt.targets[1].base_url, monitoring.transports.mqtt.targets[2].base_url, etc. + + coap: + # Enable CoAP transport checks + enabled: '${COAP_TRANSPORT_MONITORING_ENABLED:true}' + # CoAP request timeout in milliseconds + request_timeout_ms: '${COAP_REQUEST_TIMEOUT_MS:4000}' + targets: + # CoAP transport base url, coap://DOMAIN by default + - base_url: '${COAP_TRANSPORT_BASE_URL:coap://${monitoring.domain}}' + # To add more targets, use following environment variables: + # monitoring.transports.coap.targets[1].base_url, monitoring.transports.coap.targets[2].base_url, etc. + + http: + # Enable HTTP transport checks + enabled: '${HTTP_TRANSPORT_MONITORING_ENABLED:true}' + # HTTP request timeout in milliseconds + request_timeout_ms: '${HTTP_REQUEST_TIMEOUT_MS:4000}' + targets: + # HTTP transport base url, http://DOMAIN by default + - base_url: '${HTTP_TRANSPORT_BASE_URL:http://${monitoring.domain}}' + # To add more targets, use following environment variables: + # monitoring.transports.http.targets[1].base_url, monitoring.transports.http.targets[2].base_url, etc. + + lwm2m: + # Enable LwM2M transport checks + enabled: '${LWM2M_TRANSPORT_MONITORING_ENABLED:true}' + # LwM2M request timeout in milliseconds + request_timeout_ms: '${LWM2M_REQUEST_TIMEOUT_MS:4000}' + targets: + # LwM2M transport base url, coap://DOMAIN:5685 by default + - base_url: '${LWM2M_TRANSPORT_BASE_URL:coap://${monitoring.domain}:5685}' + # To add more targets, use following environment variables: + # monitoring.transports.lwm2m.targets[1].base_url, monitoring.transports.lwm2m.targets[2].base_url, etc. + + notification_channels: + slack: + # Enable notifying via Slack + enabled: '${SLACK_NOTIFICATION_CHANNEL_ENABLED:false}' + # Slack webhook url + webhook_url: '${SLACK_WEBHOOK_URL:}' + + latency: + # Enable latencies reporting + enabled: "${LATENCY_REPORTING_ENABLED:true}" + # Latency threshold for notifying + threshold_ms: '${LATENCY_THRESHOLD:2000}' + # ID of the asset to save latencies to. If not set and latencies reporting is enabled - asset will be found or created automatically + reporting_asset_id: '${LATENCY_REPORTING_ASSET_ID:}' diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index a60e7405a8..340f0d0eb8 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -18,7 +18,7 @@ As result, in REPOSITORY column, next images should be present: thingsboard/tb-web-ui thingsboard/tb-js-executor -- Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory with Redis standalone: +- Run the black box tests (without ui tests) in the [msa/black-box-tests](../black-box-tests) directory with Redis standalone: mvn clean install -DblackBoxTests.skip=false @@ -26,13 +26,39 @@ As result, in REPOSITORY column, next images should be present: mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.redisCluster=true +- Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory with Redis sentinel: + + mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.redisSentinel=true + - Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory in Hybrid mode (postgres + cassandra): mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.hybridMode=true -To run the black box tests with using local env run tests in the [msa/black-box-tests](../black-box-tests) directory with runLocal property: +- To run the black box tests with using local env run tests in the [msa/black-box-tests](../black-box-tests) directory with runLocal property: mvn clean install -DblackBoxTests.skip=false -DrunLocal=true +- To run only ui tests in the [msa/black-box-tests](../black-box-tests) directory: + + mvn clean install -DblackBoxTests.skip=false -Dsuite=uiTests + +- To run only ui smoke rule chains tests in the [msa/black-box-tests](../black-box-tests) directory: + + mvn clean install -DblackBoxTests.skip=false -Dsuite=smokesRuleChain + +- To run only ui smoke customers tests in the [msa/black-box-tests](../black-box-tests) directory: + + mvn clean install -DblackBoxTests.skip=false -Dsuite=smokesCustomer + +- To run only ui smoke profiles tests in the [msa/black-box-tests](../black-box-tests) directory: + + mvn clean install -DblackBoxTests.skip=false -Dsuite=smokesPrifiles + +- To run all tests (black-box and ui) in the [msa/black-box-tests](../black-box-tests) directory: + mvn clean install -DblackBoxTests.skip=false -Dsuite=all +### To run a separate test manually on a built UI: +1. Add the black-box-tests module in the [pom.xml](../pom.xml) or add as a Maven project +2. Add Vm Option "*-DrunLocal=true -Dtb.baseUiUrl=http://localhost:4200/*" in "Run" -> "Edit Configuration" -> "Edit Configuration Templates" -> "TestNG" +3. To run a specific test, go to the test class in the [UI tests package](../black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests) and run the test. Alternatively, go to the [resources](../black-box-tests/src/test/resources) in the black-box-tests module and run the test suite that you need. \ No newline at end of file diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 74c97fc87d..6413973d7c 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/all.xml b/msa/black-box-tests/src/test/resources/all.xml new file mode 100644 index 0000000000..7bf6c85546 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/all.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/allure.properties b/msa/black-box-tests/src/test/resources/allure.properties new file mode 100644 index 0000000000..f0525ba437 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/allure.properties @@ -0,0 +1,2 @@ +allure.results.directory=target/allure-results +allure.output.directory=target/allure-results \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/assetProfileForImport.json b/msa/black-box-tests/src/test/resources/assetProfileForImport.json new file mode 100644 index 0000000000..b42d77f998 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/assetProfileForImport.json @@ -0,0 +1,10 @@ +{ + "name": "Asset Profile For Import", + "description": null, + "image": null, + "defaultRuleChainId": null, + "defaultDashboardId": null, + "defaultQueueName": null, + "externalId": null, + "default": false +} \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/config.properties b/msa/black-box-tests/src/test/resources/config.properties deleted file mode 100644 index 419c73185d..0000000000 --- a/msa/black-box-tests/src/test/resources/config.properties +++ /dev/null @@ -1,2 +0,0 @@ -tb.baseUrl=http://localhost:8080 -tb.wsUrl=ws://localhost:8080 diff --git a/msa/black-box-tests/src/test/resources/connectivity.xml b/msa/black-box-tests/src/test/resources/connectivity.xml new file mode 100644 index 0000000000..0db3c01577 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/connectivity.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/deviceProfileForImport.json b/msa/black-box-tests/src/test/resources/deviceProfileForImport.json new file mode 100644 index 0000000000..b2e1107945 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/deviceProfileForImport.json @@ -0,0 +1,29 @@ +{ + "name": "Device Profile For Import", + "description": "", + "image": null, + "type": "DEFAULT", + "transportType": "DEFAULT", + "provisionType": "DISABLED", + "defaultRuleChainId": null, + "defaultDashboardId": null, + "defaultQueueName": null, + "profileData": { + "configuration": { + "type": "DEFAULT" + }, + "transportConfiguration": { + "type": "DEFAULT" + }, + "provisionConfiguration": { + "type": "DISABLED", + "provisionDeviceSecret": null + }, + "alarms": null + }, + "provisionDeviceKey": null, + "firmwareId": null, + "softwareId": null, + "externalId": null, + "default": false +} \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/docker-compose.hybrid-test-extras.yml b/msa/black-box-tests/src/test/resources/docker-compose.hybrid-test-extras.yml new file mode 100644 index 0000000000..5df6d2582d --- /dev/null +++ b/msa/black-box-tests/src/test/resources/docker-compose.hybrid-test-extras.yml @@ -0,0 +1,23 @@ +# +# Copyright © 2016-2023 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +version: '3.0' + +services: + cassandra: + environment: + HEAP_NEWSIZE: 128M + MAX_HEAP_SIZE: 1024M diff --git a/msa/black-box-tests/src/test/resources/docker-compose.postgres-test-extras.yml b/msa/black-box-tests/src/test/resources/docker-compose.postgres-test-extras.yml new file mode 100644 index 0000000000..90d7a351cf --- /dev/null +++ b/msa/black-box-tests/src/test/resources/docker-compose.postgres-test-extras.yml @@ -0,0 +1,19 @@ +# +# Copyright © 2016-2023 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +version: '3.0' + +# Placeholder diff --git a/msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml b/msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml index 21aba7061a..f1c2f0b41c 100644 --- a/msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml +++ b/msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2022 The Thingsboard Authors +# Copyright © 2016-2023 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ # limitations under the License. # -version: '2.2' +version: '3.0' services: rabbitmq: diff --git a/msa/black-box-tests/src/test/resources/docker-selenium.yml b/msa/black-box-tests/src/test/resources/docker-selenium.yml new file mode 100644 index 0000000000..2a88264bc8 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/docker-selenium.yml @@ -0,0 +1,36 @@ +# +# Copyright © 2016-2023 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +version: '3' +services: + selenium-chrome: + restart: always + image: selenium/standalone-chrome + ports: + - '4444:4444' + - '7900:7900' + shm_size: 2gb + environment: + SE_NODE_MAX_SESSIONS: 8 + SE_NODE_OVERRIDE_MAX_SESSIONS: 'true' + SE_NODE_SESSION_TIMEOUT: 5000 + SE_SCREEN_WIDTH: 1920 + SE_SCREEN_HEIGHT: 1080 + SE_SCREEN_DEPTH: 24 + SE_SCREEN_DPI: 74 +# Alternative way how to connect to the host address +# extra_hosts: +# - "host.docker.internal:host-gateway" diff --git a/msa/black-box-tests/src/test/resources/forImport.txt b/msa/black-box-tests/src/test/resources/forImport.txt new file mode 100644 index 0000000000..2c5593320e --- /dev/null +++ b/msa/black-box-tests/src/test/resources/forImport.txt @@ -0,0 +1,16 @@ +==== + Copyright © 2016-2023 The Thingsboard Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==== + diff --git a/msa/black-box-tests/src/test/resources/logback.xml b/msa/black-box-tests/src/test/resources/logback.xml index cdf87aab92..0df6c199f4 100644 --- a/msa/black-box-tests/src/test/resources/logback.xml +++ b/msa/black-box-tests/src/test/resources/logback.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/smokesCustomer.xml b/msa/black-box-tests/src/test/resources/smokesCustomer.xml new file mode 100644 index 0000000000..4399076cfd --- /dev/null +++ b/msa/black-box-tests/src/test/resources/smokesCustomer.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/smokesProfiles.xml b/msa/black-box-tests/src/test/resources/smokesProfiles.xml new file mode 100644 index 0000000000..8173d116a4 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/smokesProfiles.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/smokesRuleChain.xml b/msa/black-box-tests/src/test/resources/smokesRuleChain.xml new file mode 100644 index 0000000000..687c6329ba --- /dev/null +++ b/msa/black-box-tests/src/test/resources/smokesRuleChain.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/testNG.xml b/msa/black-box-tests/src/test/resources/testNG.xml deleted file mode 100644 index 45e93f76f1..0000000000 --- a/msa/black-box-tests/src/test/resources/testNG.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/uiTests.xml b/msa/black-box-tests/src/test/resources/uiTests.xml new file mode 100644 index 0000000000..e9bea8b1ef --- /dev/null +++ b/msa/black-box-tests/src/test/resources/uiTests.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/js-executor/api/httpServer.ts b/msa/js-executor/api/httpServer.ts index e1c294fdff..62372f6edd 100644 --- a/msa/js-executor/api/httpServer.ts +++ b/msa/js-executor/api/httpServer.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/msa/js-executor/api/jsExecutor.models.ts b/msa/js-executor/api/jsExecutor.models.ts index 17407f4d50..5d54fd2bc6 100644 --- a/msa/js-executor/api/jsExecutor.models.ts +++ b/msa/js-executor/api/jsExecutor.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/msa/js-executor/api/jsExecutor.ts b/msa/js-executor/api/jsExecutor.ts index f22f14281b..8f38d8aabf 100644 --- a/msa/js-executor/api/jsExecutor.ts +++ b/msa/js-executor/api/jsExecutor.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/msa/js-executor/api/jsInvokeMessageProcessor.ts b/msa/js-executor/api/jsInvokeMessageProcessor.ts index 52a337c74a..836deb7d0e 100644 --- a/msa/js-executor/api/jsInvokeMessageProcessor.ts +++ b/msa/js-executor/api/jsInvokeMessageProcessor.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/msa/js-executor/api/utils.ts b/msa/js-executor/api/utils.ts index 58fec28b0b..468dd50e7e 100644 --- a/msa/js-executor/api/utils.ts +++ b/msa/js-executor/api/utils.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/msa/js-executor/config/custom-environment-variables.yml b/msa/js-executor/config/custom-environment-variables.yml index 2ebea4ccc1..5ca66d71a1 100644 --- a/msa/js-executor/config/custom-environment-variables.yml +++ b/msa/js-executor/config/custom-environment-variables.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2022 The Thingsboard Authors +# Copyright © 2016-2023 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ kafka: linger_ms: "TB_KAFKA_LINGER_MS" # for producer partitions_consumed_concurrently: "TB_KAFKA_PARTITIONS_CONSUMED_CONCURRENTLY" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency requestTimeout: "TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS" + connectionTimeout: "TB_KAFKA_CONNECTION_TIMEOUT_MS" compression: "TB_QUEUE_KAFKA_COMPRESSION" # gzip or uncompressed topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES" use_confluent_cloud: "TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD" diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index 96f3401da5..43712f0f61 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2022 The Thingsboard Authors +# Copyright © 2016-2023 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ kafka: linger_ms: "5" # for producer partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency requestTimeout: "30000" # The default value in kafkajs is: 30000 + connectionTimeout: "1000" # The default value in kafkajs is: 1000 compression: "gzip" # gzip or uncompressed topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1" use_confluent_cloud: false diff --git a/msa/js-executor/config/logger.ts b/msa/js-executor/config/logger.ts index cecad0b487..8a2e8a6305 100644 --- a/msa/js-executor/config/logger.ts +++ b/msa/js-executor/config/logger.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/msa/js-executor/config/tb-js-executor.conf b/msa/js-executor/config/tb-js-executor.conf index fbdcc163f5..b3d4e05629 100644 --- a/msa/js-executor/config/tb-js-executor.conf +++ b/msa/js-executor/config/tb-js-executor.conf @@ -1,5 +1,5 @@ # -# Copyright © 2016-2022 The Thingsboard Authors +# Copyright © 2016-2023 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/msa/js-executor/docker/Dockerfile b/msa/js-executor/docker/Dockerfile index 93e5ab21ff..f934628a96 100644 --- a/msa/js-executor/docker/Dockerfile +++ b/msa/js-executor/docker/Dockerfile @@ -1,5 +1,5 @@ # -# Copyright © 2016-2022 The Thingsboard Authors +# Copyright © 2016-2023 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/msa/js-executor/docker/start-js-executor.sh b/msa/js-executor/docker/start-js-executor.sh index d30b62c145..2272f9f20a 100755 --- a/msa/js-executor/docker/start-js-executor.sh +++ b/msa/js-executor/docker/start-js-executor.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2022 The Thingsboard Authors +# Copyright © 2016-2023 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/msa/js-executor/install.js b/msa/js-executor/install.js index a20142a4a0..71cf8d33a3 100644 --- a/msa/js-executor/install.js +++ b/msa/js-executor/install.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index e094dcc4e5..7736db173f 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "3.4.3", + "version": "3.5.2", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 860eb3891a..fe92a77763 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -1,6 +1,6 @@ + + 4.0.0 + + org.thingsboard + 3.5.2-SNAPSHOT + msa + + + org.thingsboard.msa + monitoring + pom + + ThingsBoard Monitoring Microservice + + + UTF-8 + ${basedir}/../.. + tb-monitoring + tb-monitoring + /var/log/${pkg.name} + /usr/share/${pkg.name} + pre-integration-test + + + + + org.thingsboard + monitoring + ${project.version} + deb + deb + provided + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-tb-monitoring-deb + package + + copy + + + + + org.thingsboard + monitoring + deb + deb + ${pkg.name}.deb + ${project.build.directory} + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-docker-config + process-resources + + copy-resources + + + ${project.build.directory} + + + docker + true + + + + + + + + com.spotify + dockerfile-maven-plugin + + + build-docker-image + pre-integration-test + + build + + + ${dockerfile.skip} + ${docker.repo}/${docker.name} + true + false + ${project.build.directory} + + + + tag-docker-image + pre-integration-test + + tag + + + ${dockerfile.skip} + ${docker.repo}/${docker.name} + ${project.version} + + + + + + + + + push-docker-image + + + push-docker-image + + + + + + com.spotify + dockerfile-maven-plugin + + + push-latest-docker-image + pre-integration-test + + push + + + latest + ${docker.repo}/${docker.name} + + + + push-version-docker-image + pre-integration-test + + push + + + ${project.version} + ${docker.repo}/${docker.name} + + + + + + + + + + + jenkins + Jenkins Repository + https://repo.jenkins-ci.org/releases + + false + + + + + \ No newline at end of file diff --git a/msa/pom.xml b/msa/pom.xml index 9e628b0f70..451466f98a 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -1,6 +1,6 @@ tb + web-ui vc-executor vc-executor-docker - js-executor - web-ui tb-node transport + js-executor + monitoring diff --git a/msa/tb-node/docker/Dockerfile b/msa/tb-node/docker/Dockerfile index cbfbebf86c..68e428992a 100644 --- a/msa/tb-node/docker/Dockerfile +++ b/msa/tb-node/docker/Dockerfile @@ -1,5 +1,5 @@ # -# Copyright © 2016-2022 The Thingsboard Authors +# Copyright © 2016-2023 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/msa/tb-node/docker/start-tb-node.sh b/msa/tb-node/docker/start-tb-node.sh index 3c5f43bfff..c979f72dad 100755 --- a/msa/tb-node/docker/start-tb-node.sh +++ b/msa/tb-node/docker/start-tb-node.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2022 The Thingsboard Authors +# Copyright © 2016-2023 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 7119da74d8..3d39ba285b 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -1,6 +1,6 @@ - 3.2.0 + 3.6.7 1.5.0 - 1.4.3 + 1.5.4 1.9.4 3.2.2 1.9.0 @@ -121,11 +123,10 @@ 6.0.20.Final 3.0.0 2.0.1.Final - 1.6.8 + 1.7.2 2.8.5 4.1.0 - 4.3.1.0 2.7.2 1.5.2 5.8.2 @@ -144,6 +145,12 @@ 6.1.0.202203080745-r 0.4.8 1.0.0 + 4.6.0 + 5.2.0 + 2.21.0 + 2.12.0 + 1.12.1 + 6.4.2 @@ -157,6 +164,7 @@ application msa rest-client + monitoring @@ -187,6 +195,7 @@ org.apache.maven.plugins maven-resources-plugin + 3.2.0 copy-conf @@ -616,7 +625,7 @@ org.apache.maven.plugins maven-resources-plugin - 2.7 + 3.2.0 org.apache.maven.plugins @@ -654,7 +663,7 @@ ${surefire.version} - --illegal-access=permit + --illegal-access=permit -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=20 @@ -813,13 +822,16 @@ docker/haproxy/** docker/tb-node/** ui/** - src/.browserslistrc + **/.browserslistrc **/yarn.lock + **/.yarnrc + **/.angular/** **/*.raw **/*.patch **/apache/cassandra/io/** .run/** **/NetworkReceive.java + **/lwm2m-registry/** JAVADOC_STYLE @@ -1074,6 +1086,11 @@ jaxb-runtime ${jaxb-runtime.version} + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + org.springframework.boot spring-boot-starter-security @@ -1423,6 +1440,11 @@ classmate ${fasterxml-classmate.version} + + com.auth0 + java-jwt + ${auth0-jwt.version} + com.github.fge json-schema-validator @@ -1596,26 +1618,6 @@ - - org.cassandraunit - cassandra-unit - ${cassandra-unit.version} - test - - - junit - junit - - - org.hamcrest - hamcrest-core - - - org.hamcrest - hamcrest-library - - - org.apache.cassandra cassandra-all @@ -1656,6 +1658,30 @@ ${rest-assured.version} test + + org.seleniumhq.selenium + selenium-java + ${selenium.version} + test + + + io.github.bonigarcia + webdrivermanager + ${webdrivermanager.version} + test + + + io.qameta.allure + allure-testng + ${allure-testng.version} + test + + + io.qameta.allure + allure-maven + ${allure-maven.version} + test + org.hamcrest hamcrest-all @@ -1685,6 +1711,11 @@ org.eclipse.paho.client.mqttv3 ${paho.client.version} + + org.eclipse.paho + org.eclipse.paho.mqttv5.client + ${paho.mqttv5.client.version} + org.apache.curator curator-x-discovery @@ -1710,6 +1741,12 @@ bcpkix-jdk15on ${bouncycastle.version} + + org.testcontainers + cassandra + ${testcontainers.version} + test + org.testcontainers postgresql @@ -1799,6 +1836,16 @@ com.microsoft.azure azure-servicebus ${azure-servicebus.version} + + + com.nimbusds + content-type + + + org.ow2.asm + asm + + org.passay @@ -1945,6 +1992,11 @@ aerogear-otp-java ${aerogear-otp.version} + + com.slack.api + slack-api-client + ${slack-api.version} + org.eclipse.jgit org.eclipse.jgit @@ -1960,6 +2012,11 @@ exp4j ${exp4j.version} + + com.github.oshi + oshi-core + ${oshi.version} + diff --git a/pull_request_template.md b/pull_request_template.md index 21362429dc..f65aa0a1f3 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -5,14 +5,14 @@ Put your PR description here instead of this sentence. ## General checklist - [ ] You have reviewed the guidelines [document](https://docs.google.com/document/d/1wqcOafLx5hth8SAg4dqV_LV3un3m5WYR8RdTJ4MbbUM/edit?usp=sharing). -- [ ] PR name contains fix version. For example, "[3.3.4] Hotfix of some UI component" or "[3.4] New Super Feature". +- [ ] [Labels](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels#about-labels) that classify your pull request have been added. - [ ] The [milestone](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/about-milestones) is specified and corresponds to fix version. - [ ] Description references specific [issue](https://github.com/thingsboard/thingsboard/issues). - [ ] Description contains human-readable scope of changes. - [ ] Description contains brief notes about what needs to be added to the documentation. - [ ] No merge conflicts, commented blocks of code, code formatting issues. - [ ] Changes are backward compatible or upgrade script is provided. -- [ ] Similar PR is opened for PE version to simplify merge. Required for internal contributors only. +- [ ] Similar PR is opened for PE version to simplify merge. Crosslinks between PRs added. Required for internal contributors only. ## Front-End feature checklist diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 24f8453786..728ffc5040 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -1,6 +1,6 @@ + + + search + + + account_circle + {{ assigneeNotSetText | translate }} + + + + +

+ + +
+ + +
+ + {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} + +
+
+ + + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss new file mode 100644 index 0000000000..5472d60495 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + width: 100%; + overflow: auto; + background: white; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15); + border-radius: 4px; +} + +::ng-deep { + mat-form-field.search-users { + padding: 8px; + height: 340px; + font-size: 14px; + background-color: white; + } + + .mat-form-field-appearance-outline .mdc-notched-outline__trailing{ + color: rgba(0, 0, 0, 0.12) !important; + } + + .tb-assignee-autocomplete { + &.tb-assignee-autocomplete.mat-mdc-autocomplete-panel { + position: relative; + margin-top: 8px; + box-shadow: none !important; + } + .mat-mdc-option { + font-size: 14px; + border: none; + height: 52px !important; + .unassigned-icon { + color: rgba(0, 0, 0, 0.38); + font-size: 28px; + width: 28px; + height: 28px; + margin-right: 8px; + } + .user-avatar { + display: inline-flex; + justify-content: center; + align-items: center; + margin-right: 8px; + border-radius: 50%; + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + color: white; + font-size: 13px; + font-weight: 700 + } + .user-display-name { + max-width: 80%; + overflow: hidden; + span { + overflow: hidden; + text-overflow: ellipsis; + } + span + span { + color: rgba(0, 0, 0, 0.38); + } + } + .mdc-list-item__primary-text { + width: 100%; + display: flex; + align-items: center; + line-height: normal; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts new file mode 100644 index 0000000000..30fa807027 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts @@ -0,0 +1,237 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + Inject, + InjectionToken, OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Observable, of, Subject } from 'rxjs'; +import { + catchError, + debounceTime, + distinctUntilChanged, + map, + share, + switchMap, + takeUntil, +} from 'rxjs/operators'; +import { User, UserEmailInfo } from '@shared/models/user.model'; +import { TranslateService } from '@ngx-translate/core'; +import { UserService } from '@core/http/user.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { emptyPageData } from '@shared/models/page/page-data'; +import { AlarmService } from '@core/http/alarm.service'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { UtilsService } from '@core/services/utils.service'; + +export const ALARM_ASSIGNEE_PANEL_DATA = new InjectionToken('AlarmAssigneePanelData'); + +export interface AlarmAssigneePanelData { + alarmId: string; + assigneeId: string; +} + +@Component({ + selector: 'tb-alarm-assignee-panel', + templateUrl: './alarm-assignee-panel.component.html', + styleUrls: ['./alarm-assignee-panel.component.scss'] +}) +export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDestroy { + + private dirty = false; + + alarmId: string; + + assigneeId?: string; + + assigneeNotSetText = 'alarm.unassigned'; + + reassigned: boolean = false; + + selectUserFormGroup: FormGroup; + + @ViewChild('userInput', {static: true}) userInput: ElementRef; + + filteredUsers: Observable>; + + searchText = ''; + + private destroy$ = new Subject(); + + constructor(@Inject(ALARM_ASSIGNEE_PANEL_DATA) public data: AlarmAssigneePanelData, + public overlayRef: OverlayRef, + public translate: TranslateService, + private userService: UserService, + private alarmService: AlarmService, + private fb: FormBuilder, + private utilsService: UtilsService) { + this.alarmId = data.alarmId; + this.assigneeId = data.assigneeId; + this.selectUserFormGroup = this.fb.group({ + user: [null] + }); + } + + ngOnInit() { + this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges + .pipe( + debounceTime(150), + map(value => { + return value ? (typeof value === 'string' ? value : '') : '' + }), + distinctUntilChanged(), + switchMap(name => this.fetchUsers(name)), + share(), + takeUntil(this.destroy$) + ); + } + + ngAfterViewInit() { + setTimeout(() => { + this.userInput.nativeElement.focus(); + }, 0) + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + displayUserFn(user?: User): string | undefined { + return user ? user.email : undefined; + } + + selected(event: MatAutocompleteSelectedEvent): void { + this.clear(); + const user: User = event.option.value; + if (user) { + this.assign(user); + } else { + this.unassign(); + } + } + + assign(user: User): void { + this.alarmService.assignAlarm(this.alarmId, user.id.id, {ignoreLoading: true}).subscribe( + () => { + this.reassigned = true; + this.overlayRef.dispose() + }); + } + + unassign(): void { + this.alarmService.unassignAlarm(this.alarmId, {ignoreLoading: true}).subscribe( + () => { + this.reassigned = true; + this.overlayRef.dispose() + }); + } + + fetchUsers(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(50, 0, searchText, { + property: 'email', + direction: Direction.ASC + }); + return this.userService.getUsersForAssign(this.alarmId, pageLink, {ignoreLoading: true}) + .pipe( + catchError(() => of(emptyPageData())), + map(pageData => { + return pageData.data; + }) + ); + } + + onFocus(): void { + if (!this.dirty) { + this.selectUserFormGroup.get('user').updateValueAndValidity({onlySelf: true}); + this.dirty = true; + } + } + + clear() { + this.selectUserFormGroup.get('user').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.userInput.nativeElement.blur(); + this.userInput.nativeElement.focus(); + }, 0); + } + + getUserDisplayName(entity: User) { + let displayName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + displayName += entity.firstName; + } + if (entity.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.lastName; + } + } else { + displayName = entity.email; + } + return displayName; + } + + getUserInitials(entity: User): string { + let initials = ''; + if (entity.firstName && entity.firstName.length || + entity.lastName && entity.lastName.length) { + if (entity.firstName) { + initials += entity.firstName.charAt(0); + } + if (entity.lastName) { + initials += entity.lastName.charAt(0); + } + } else { + initials += entity.email.charAt(0); + } + return initials.toUpperCase(); + } + + getFullName(entity: User): string { + let fullName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + fullName += entity.firstName; + } + if (entity.lastName) { + if (fullName.length > 0) { + fullName += ' '; + } + fullName += entity.lastName; + } + } + return fullName; + } + + getAvatarBgColor(entity: User) { + return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts new file mode 100644 index 0000000000..1526616833 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts @@ -0,0 +1,203 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + Inject, + InjectionToken, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Observable, of, Subject } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, takeUntil, } from 'rxjs/operators'; +import { User, UserEmailInfo } from '@shared/models/user.model'; +import { TranslateService } from '@ngx-translate/core'; +import { UserService } from '@core/http/user.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { emptyPageData } from '@shared/models/page/page-data'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { UtilsService } from '@core/services/utils.service'; + +export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken('AlarmAssigneeSelectPanelData'); + +export interface AlarmAssigneeSelectPanelData { + assigneeId?: string; +} + +@Component({ + selector: 'tb-alarm-assignee-select-panel', + templateUrl: './alarm-assignee-panel.component.html', + styleUrls: ['./alarm-assignee-panel.component.scss'] +}) +export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit, OnDestroy { + + private dirty = false; + + assigneeId?: string; + + assigneeNotSetText = 'alarm.assignee-not-set'; + + selectUserFormGroup: FormGroup; + + @ViewChild('userInput', {static: true}) userInput: ElementRef; + + filteredUsers: Observable>; + + searchText = ''; + + userSelected = false; + + result?: UserEmailInfo; + + private destroy$ = new Subject(); + + constructor(@Inject(ALARM_ASSIGNEE_SELECT_PANEL_DATA) public data: AlarmAssigneeSelectPanelData, + public overlayRef: OverlayRef, + public translate: TranslateService, + private userService: UserService, + private fb: FormBuilder, + private utilsService: UtilsService) { + this.assigneeId = data.assigneeId; + this.selectUserFormGroup = this.fb.group({ + user: [null] + }); + } + + ngOnInit() { + this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges + .pipe( + debounceTime(150), + map(value => value ? (typeof value === 'string' ? value : '') : ''), + distinctUntilChanged(), + switchMap(name => this.fetchUsers(name)), + share(), + takeUntil(this.destroy$) + ); + } + + ngAfterViewInit() { + setTimeout(() => { + this.userInput.nativeElement.focus(); + }, 0); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + displayUserFn(user?: User): string | undefined { + return user ? user.email : undefined; + } + + selected(event: MatAutocompleteSelectedEvent): void { + this.clear(); + this.userSelected = true; + this.result = event.option.value; + this.overlayRef.dispose(); + } + + fetchUsers(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(50, 0, searchText, { + property: 'email', + direction: Direction.ASC + }); + return this.userService.findUsersByQuery(pageLink, {ignoreLoading: true}) + .pipe( + catchError(() => of(emptyPageData())), + map(pageData => pageData.data) + ); + } + + onFocus(): void { + if (!this.dirty) { + this.selectUserFormGroup.get('user').updateValueAndValidity({onlySelf: true}); + this.dirty = true; + } + } + + clear() { + this.selectUserFormGroup.get('user').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.userInput.nativeElement.blur(); + this.userInput.nativeElement.focus(); + }, 0); + } + + getUserDisplayName(entity: UserEmailInfo) { + let displayName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + displayName += entity.firstName; + } + if (entity.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.lastName; + } + } else { + displayName = entity.email; + } + return displayName; + } + + getUserInitials(entity: UserEmailInfo): string { + let initials = ''; + if (entity.firstName && entity.firstName.length || + entity.lastName && entity.lastName.length) { + if (entity.firstName) { + initials += entity.firstName.charAt(0); + } + if (entity.lastName) { + initials += entity.lastName.charAt(0); + } + } else { + initials += entity.email.charAt(0); + } + return initials.toUpperCase(); + } + + getFullName(entity: UserEmailInfo): string { + let fullName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + fullName += entity.firstName; + } + if (entity.lastName) { + if (fullName.length > 0) { + fullName += ' '; + } + fullName += entity.lastName; + } + } + return fullName; + } + + getAvatarBgColor(entity: UserEmailInfo) { + return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html new file mode 100644 index 0000000000..2ad7229365 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html @@ -0,0 +1,31 @@ + + + + alarm.assignee + + + {{ getUserInitials() }} + + account_circle + arrow_drop_down + + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts new file mode 100644 index 0000000000..4d1f6021be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts @@ -0,0 +1,193 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Injector, Input, OnInit, StaticProvider, ViewContainerRef } from '@angular/core'; +import { UtilsService } from '@core/services/utils.service'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { TranslateService } from '@ngx-translate/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { UserId } from '@shared/models/id/user-id'; +import { UserService } from '@core/http/user.service'; +import { User, UserEmailInfo } from '@shared/models/user.model'; +import { catchError, map, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { + ALARM_ASSIGNEE_SELECT_PANEL_DATA, + AlarmAssigneeSelectPanelComponent, + AlarmAssigneeSelectPanelData +} from '@home/components/alarm/alarm-assignee-select-panel.component'; + +@Component({ + selector: 'tb-alarm-assignee-select', + templateUrl: './alarm-assignee-select.component.html', + styleUrls: ['./alarm-assignee.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmAssigneeSelectComponent), + multi: true + } + ] +}) +export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccessor { + + @Input() disabled: boolean; + + assigneeFormGroup: UntypedFormGroup; + assignee?: User | UserEmailInfo; + + private propagateChange = (_: any) => {}; + + constructor(private utilsService: UtilsService, + private overlay: Overlay, + private fb: UntypedFormBuilder, + private userService: UserService, + private viewContainerRef: ViewContainerRef, + private translateService: TranslateService) { + } + + ngOnInit(): void { + this.assigneeFormGroup = this.fb.group({ + assignee: [null, []] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.assigneeFormGroup.disable({emitEvent: false}); + } else { + this.assigneeFormGroup.enable({emitEvent: false}); + } + } + + writeValue(userId?: UserId): void { + const userObservable = userId ? this.userService.getUser(userId.id, {ignoreErrors: true}).pipe( + catchError(() => of(null)) + ) : of(null); + userObservable.pipe( + tap((user) => { + this.assignee = user; + }), + map((user) => this.getAssignee(user)) + ).subscribe((assignee) => { + this.assigneeFormGroup.get('assignee').patchValue(assignee, {emitEvent: false}); + }); + } + + private getAssignee(user?: User| UserEmailInfo): string { + if (user) { + return this.getUserDisplayName(user); + } else { + return this.translateService.instant('alarm.assignee-not-set'); + } + } + + private getUserDisplayName(user?: User | UserEmailInfo): string { + let displayName = ''; + if ((user?.firstName && user?.firstName.length > 0) || + (user?.lastName && user?.lastName.length > 0)) { + if (user?.firstName) { + displayName += user?.firstName; + } + if (user?.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += user?.lastName; + } + } else { + displayName = user?.email; + } + return displayName; + } + + getUserInitials(): string { + let initials = ''; + if (this.assignee?.firstName && this.assignee?.firstName.length || + this.assignee?.lastName && this.assignee?.lastName.length) { + if (this.assignee?.firstName) { + initials += this.assignee?.firstName.charAt(0); + } + if (this.assignee?.lastName) { + initials += this.assignee?.lastName.charAt(0); + } + } else { + initials += this.assignee?.email.charAt(0); + } + return initials.toUpperCase(); + } + + getAvatarBgColor(): string { + return this.utilsService.stringToHslColor(this.getUserDisplayName(this.assignee), 40, 60); + } + + openAlarmAssigneeSelectPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + if (!this.disabled) { + const target = $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'center', + originY: 'bottom', + overlayX: 'center', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + config.width = (target as HTMLElement).offsetWidth; + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: ALARM_ASSIGNEE_SELECT_PANEL_DATA, + useValue: { + assigneeId: this.assignee?.id?.id + } as AlarmAssigneeSelectPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const component = overlayRef.attach(new ComponentPortal(AlarmAssigneeSelectPanelComponent, + this.viewContainerRef, injector)); + component.onDestroy(() => { + if (component.instance.userSelected) { + this.assignee = component.instance.result; + this.assigneeFormGroup.get('assignee').patchValue(this.getAssignee(this.assignee), {emitEvent: false}); + this.propagateChange(this.assignee?.id); + } + }); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html new file mode 100644 index 0000000000..dd4c8ee1d9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html @@ -0,0 +1,30 @@ + + + + alarm.assignee + + + {{ getUserInitials(alarm.assignee) }} + + account_circle + arrow_drop_down + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss new file mode 100644 index 0000000000..82f10650cf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.user-avatar { + display: inline-flex; + justify-content: center; + align-items: center; + border-radius: 50%; + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + color: white; + font-size: 13px; + font-weight: 700; + margin-left: 12px; + margin-right: 20px; +} + +.unassigned-icon { + width: 28px; + height: 28px; + font-size: 28px; + color: rgba(0, 0, 0, 0.38); + overflow: visible; + margin-left: 12px; + margin-right: 20px; + padding: 0; +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts new file mode 100644 index 0000000000..1fb1d3f689 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts @@ -0,0 +1,157 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Injector, Input, Output, StaticProvider, ViewContainerRef } from '@angular/core'; +import { UtilsService } from '@core/services/utils.service'; +import { AlarmAssignee, AlarmInfo } from '@shared/models/alarm.models'; +import { + ALARM_ASSIGNEE_PANEL_DATA, + AlarmAssigneePanelComponent, + AlarmAssigneePanelData +} from '@home/components/alarm/alarm-assignee-panel.component'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-alarm-assignee', + templateUrl: './alarm-assignee.component.html', + styleUrls: ['./alarm-assignee.component.scss'] +}) +export class AlarmAssigneeComponent { + @Input() + alarm: AlarmInfo; + + @Input() + allowAssign: boolean; + + @Output() + alarmReassigned = new EventEmitter(); + + constructor(private utilsService: UtilsService, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private translateService: TranslateService) { + } + + getAssignee() { + if (this.alarm) { + if (this.alarm.assignee) { + return this.getUserDisplayName(this.alarm.assignee); + } else { + return this.translateService.instant('alarm.unassigned'); + } + } + } + + getUserDisplayName(entity: AlarmAssignee) { + let displayName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + displayName += entity.firstName; + } + if (entity.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.lastName; + } + } else { + displayName = entity.email; + } + return displayName; + } + + getUserInitials(entity: AlarmAssignee): string { + let initials = ''; + if (entity.firstName && entity.firstName.length || + entity.lastName && entity.lastName.length) { + if (entity.firstName) { + initials += entity.firstName.charAt(0); + } + if (entity.lastName) { + initials += entity.lastName.charAt(0); + } + } else { + initials += entity.email.charAt(0); + } + return initials.toUpperCase(); + } + + getFullName(entity: AlarmAssignee): string { + let fullName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + fullName += entity.firstName; + } + if (entity.lastName) { + if (fullName.length > 0) { + fullName += ' '; + } + fullName += entity.lastName; + } + } + return fullName; + } + + getAvatarBgColor(entity: AlarmAssignee) { + return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + + openAlarmAssigneePanel($event: Event, alarm: AlarmInfo) { + if ($event) { + $event.stopPropagation(); + } + if (this.allowAssign) { + const target = $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'center', + originY: 'bottom', + overlayX: 'center', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + config.width = (target as HTMLElement).offsetWidth; + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: ALARM_ASSIGNEE_PANEL_DATA, + useValue: { + alarmId: alarm.id.id, + assigneeId: alarm.assigneeId?.id + } as AlarmAssigneePanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent, + this.viewContainerRef, injector)).onDestroy(() => this.alarmReassigned.emit(true)); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html new file mode 100644 index 0000000000..8618592ae0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html @@ -0,0 +1,33 @@ + +
+ + + + + + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts new file mode 100644 index 0000000000..0aa568bdd0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { AlarmInfo } from '@shared/models/alarm.models'; + +export interface AlarmCommentDialogData { + alarmId?: string; + alarm?: AlarmInfo; +} + +@Component({ + selector: 'tb-alarm-comment-dialog', + templateUrl: './alarm-comment-dialog.component.html', + styleUrls: [] +}) +export class AlarmCommentDialogComponent extends DialogComponent { + + alarmId: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmCommentDialogData, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + this.alarmId = this.data.alarmId; + } + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html new file mode 100644 index 0000000000..ac2bb73d05 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html @@ -0,0 +1,181 @@ + + +
+
+
+ + alarm-activity.activity + +
+ + + +
+
+
+
+ + + +
+
+
+ + {{ displayDataElement.commentText }} + + + {{ displayDataElement.createdDateAgo }} + +
+ +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+
+
+ {{ displayDataElement.displayName }} + + {{ displayDataElement.createdDateAgo }} + + + Edited + +
+ {{ displayDataElement.commentText }} +
+
+ + +
+
+ +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+ + +
+ + +
+
+
+
+
+
+
+ + + +
+ +
+
+
+ {{ getUserInitials(userDisplayName) }} +
+ + + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss new file mode 100644 index 0000000000..c7b0d65353 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss @@ -0,0 +1,220 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import '../theme.scss'; + +$primary-color: rgba(mat.get-color-from-palette($tb-primary, 50), 0.4); +$border: 1px solid mat.get-color-from-palette($tb-primary); + +:host { + .tb-alarm-comments { + background-color: $primary-color; + max-width: 600px; + width: 100%; + + .comment-input { + position: sticky; + z-index: 1; + + .inner-wrap { + border: $border; + padding: 0 24px; + background-color: $primary-color; + } + + &.oldest-first { + bottom: -24px; + box-shadow: 0px -4px 12px rgba(0, 0, 0, 0.04); + + .inner-wrap { + border-radius: 0 0 8px 8px; + } + + &.activity-only { + bottom: -1px; + + .inner-wrap { + border: none + } + } + } + + &.newest-first { + top: 24px; + box-shadow: 0 8px 10px rgba(23, 33, 90, 0.08); + + .inner-wrap { + border-top: none; + } + + &.activity-only { + top: 48px; + box-shadow: 0 8px 10px rgba(23, 33, 90, 0.08); + + .inner-wrap { + border: none; + } + } + } + } + + .header { + z-index: 1; + position: sticky; + top: -24px; + background-color: white; + + &.activity-only { + top: 0; + } + + &-container { + padding: 0 24px; + background-color: $primary-color; + border: $border; + border-bottom: none; + border-radius: 8px 8px 0px 0px; + + &.activity-only { + border: none; + } + + &.asc { + border-bottom: $border; + box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08); + + &.activity-only { + border: none; + } + } + + .header-title { + color: rgba(0, 0, 0, 0.76); + letter-spacing: 0.25px; + font-weight: 500; + } + + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + } + + .comments-container { + border: $border; + border-top: none; + border-radius: 0px 0px 8px 8px; + + &.activity-only { + border: none; + } + + &.asc { + border-bottom: none; + border-radius: unset; + } + } + + .user-comment { + padding: 4px; + border-radius: 8px; + &:hover { + background-color: $primary-color; + } + } + + .user-avatar { + width: 28px; + min-width: 28px; + height: 28px; + min-height: 28px; + border-radius: 50%; + font-weight: 700; + color: white; + font-size: 13px; + } + + .user-name { + font-size: 16px; + color: rgba(0, 0, 0, 0.76); + font-weight: 500; + letter-spacing: 0.25px; + } + + .time { + font-size: 14px; + font-weight: 400; + color: rgba(0, 0, 0, 0.38); + letter-spacing: 0.2px; + border-radius: 16px; + padding: 0 3px; + &:hover { + background: rgba(0, 0, 0, 0.04); + } + } + + .system-comment { + margin-left: 38px; + @media #{$mat-xs} { + margin-left: 0; + } + } + + .system-text { + font-weight: 500; + font-size: 14px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.38); + } + + .text { + white-space: pre-line; + word-break: break-word; + font-size: 16px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.54); + } + + .action-buttons { + visibility: hidden; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + + .show-buttons { + visibility: visible; + } + + .green-button { + color: #00695C; + } + + .red-button { + color: #D12730; + } + + .mat-form-field { + font-size: 16px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.76); + } + + textarea { + letter-spacing: 0.15px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts new file mode 100644 index 0000000000..5ba3c39a81 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts @@ -0,0 +1,325 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { AlarmCommentService } from '@core/http/alarm-comment.service'; +import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; +import { DialogService } from '@core/services/dialog.service'; +import { AuthUser, User } from '@shared/models/user.model'; +import { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; +import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; +import { map } from 'rxjs/operators'; +import { AlarmComment, AlarmCommentInfo, AlarmCommentType } from '@shared/models/alarm.models'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DatePipe } from '@angular/common'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; + +interface AlarmCommentsDisplayData { + commentId?: string, + displayName?: string, + createdTime: string, + createdDateAgo?: string, + edit?: boolean, + isEdited?: boolean, + editedTime?: string; + editedDateAgo?: string, + showActions?: boolean, + commentText?: string, + isSystemComment?: boolean, + avatarBgColor?: string +} + +@Component({ + selector: 'tb-alarm-comment', + templateUrl: './alarm-comment.component.html', + styleUrls: ['./alarm-comment.component.scss'] +}) +export class AlarmCommentComponent implements OnInit { + @Input() + alarmId: string; + + @Input() + alarmActivityOnly: boolean = false; + + authUser: AuthUser; + + alarmCommentFormGroup: FormGroup; + + alarmComments: Array; + + displayData: Array = new Array(); + + alarmCommentSortOrder: SortOrder = { + property: 'createdTime', + direction: Direction.DESC + }; + + editMode: boolean = false; + + userDisplayName$ = this.store.pipe( + select(selectUserDetails), + map((user) => this.getUserDisplayName(user)) + ); + + currentUserDisplayName: string; + currentUserAvatarColor: string; + + constructor(protected store: Store, + private translate: TranslateService, + private alarmCommentService: AlarmCommentService, + public fb: FormBuilder, + private dialogService: DialogService, + public dateAgoPipe: DateAgoPipe, + private utilsService: UtilsService, + private datePipe: DatePipe, + private importExportService: ImportExportService) { + + this.authUser = getCurrentAuthUser(store); + + this.alarmCommentFormGroup = this.fb.group( + { + alarmCommentEdit: [''], + alarmComment: [''] + } + ); + } + + ngOnInit() { + this.loadAlarmComments(); + this.currentUserAvatarColor = this.utilsService.stringToHslColor(this.currentUserDisplayName, + 60, 40); + } + + loadAlarmComments(): void { + this.alarmCommentService.getAlarmComments(this.alarmId, new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, + this.alarmCommentSortOrder), {ignoreLoading: true}).subscribe( + (pagedData) => { + this.alarmComments = pagedData.data; + this.displayData.length = 0; + for (let alarmComment of pagedData.data) { + let displayDataElement = {} as AlarmCommentsDisplayData; + displayDataElement.createdTime = this.datePipe.transform(alarmComment.createdTime, 'yyyy-MM-dd HH:mm:ss'); + displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime); + displayDataElement.commentText = alarmComment.comment.text; + displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM; + if (alarmComment.type === AlarmCommentType.OTHER) { + displayDataElement.commentId = alarmComment.id.id; + displayDataElement.displayName = this.getUserDisplayName(alarmComment); + displayDataElement.edit = false; + displayDataElement.isEdited = alarmComment.comment.edited; + displayDataElement.editedTime = this.datePipe.transform(alarmComment.comment.editedOn, 'yyyy-MM-dd HH:mm:ss'); + displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn) + '\n'; + displayDataElement.showActions = false; + displayDataElement.isSystemComment = false; + displayDataElement.avatarBgColor = this.utilsService.stringToHslColor(displayDataElement.displayName, + 40, 60); + } + this.displayData.push(displayDataElement); + } + } + ) + } + + changeSortDirection() { + let currentDirection = this.alarmCommentSortOrder.direction; + this.alarmCommentSortOrder.direction = currentDirection === Direction.DESC ? Direction.ASC : Direction.DESC; + this.loadAlarmComments(); + } + + exportAlarmActivity() { + let fileName = this.translate.instant('alarm.alarm') + '_' + this.translate.instant('alarm-activity.activity'); + this.importExportService.exportCsv(this.getDataForExport(), fileName.toLowerCase()); + } + + saveComment(): void { + const commentInputValue: string = this.getAlarmCommentFormControl().value; + if (commentInputValue) { + const comment: AlarmComment = { + alarmId: { + id: this.alarmId, + entityType: EntityType.ALARM + }, + type: AlarmCommentType.OTHER, + comment: { + text: commentInputValue + } + } + this.doSave(comment); + this.clearCommentInput(); + } + } + + saveEditedComment(commentId: string): void { + const commentEditInputValue: string = this.getAlarmCommentEditFormControl().value; + if (commentEditInputValue) { + const editedComment: AlarmComment = this.getAlarmCommentById(commentId); + editedComment.comment.text = commentEditInputValue; + this.doSave(editedComment); + this.clearCommentEditInput(); + this.editMode = false; + this.getAlarmCommentFormControl().enable({emitEvent: false}); + } + } + + private doSave(comment: AlarmComment): void { + this.alarmCommentService.saveAlarmComment(this.alarmId, comment, {ignoreLoading: true}).subscribe( + () => { + this.loadAlarmComments(); + } + ) + } + + editComment(commentId: string): void { + const commentDisplayData = this.getDataElementByCommentId(commentId); + commentDisplayData.edit = true; + this.editMode = true; + this.getAlarmCommentEditFormControl().patchValue(commentDisplayData.commentText); + this.getAlarmCommentFormControl().disable({emitEvent: false}); + } + + cancelEdit(commentId: string): void { + const commentDisplayData = this.getDataElementByCommentId(commentId); + commentDisplayData.edit = false; + this.editMode = false; + this.getAlarmCommentFormControl().enable({emitEvent: false}); + } + + deleteComment(commentId: string): void { + const alarmCommentInfo: AlarmComment = this.getAlarmCommentById(commentId); + const commentText: string = alarmCommentInfo.comment.text; + this.dialogService.confirm( + this.translate.instant('alarm-activity.delete-alarm-comment'), + commentText, + this.translate.instant('action.cancel'), + this.translate.instant('action.delete')).subscribe( + (result) => { + if (result) { + this.alarmCommentService.deleteAlarmComments(this.alarmId, commentId, {ignoreLoading: true}) + .subscribe(() => { + this.loadAlarmComments(); + } + ) + } + } + ) + } + + getSortDirectionIcon() { + return this.alarmCommentSortOrder.direction === Direction.DESC ? 'mdi:sort-descending' : 'mdi:sort-ascending' + } + + getSortDirectionTooltipText() { + let text = this.alarmCommentSortOrder.direction === Direction.DESC ? 'alarm-activity.newest-first' : + 'alarm-activity.oldest-first'; + return this.translate.instant(text); + } + + isDirectionAscending() { + return this.alarmCommentSortOrder.direction === Direction.ASC; + } + + onCommentMouseEnter(commentId: string, displayDataIndex: number): void { + if (!this.editMode) { + const alarmUserId = this.getAlarmCommentById(commentId).userId.id; + if (this.authUser.userId === alarmUserId) { + this.displayData[displayDataIndex].showActions = true; + } + } + } + + onCommentMouseLeave(displayDataIndex: number): void { + this.displayData[displayDataIndex].showActions = false; + } + + getUserInitials(userName: string): string { + let initials = ''; + const userNameSplit = userName.split(' '); + initials += userNameSplit[0].charAt(0).toUpperCase(); + if (userNameSplit.length > 1) { + initials += userNameSplit[userNameSplit.length - 1].charAt(0).toUpperCase(); + } + return initials; + } + + getCurrentUserBgColor(userDisplayName: string) { + return this.utilsService.stringToHslColor(userDisplayName, 40, 60); + } + + private getUserDisplayName(alarmCommentInfo: AlarmCommentInfo | User): string { + let name = ''; + if ((alarmCommentInfo.firstName && alarmCommentInfo.firstName.length > 0) || + (alarmCommentInfo.lastName && alarmCommentInfo.lastName.length > 0)) { + if (alarmCommentInfo.firstName) { + name += alarmCommentInfo.firstName; + } + if (alarmCommentInfo.lastName) { + if (name.length > 0) { + name += ' '; + } + name += alarmCommentInfo.lastName; + } + } else { + name = alarmCommentInfo.email; + } + return name; + } + + getAlarmCommentFormControl(): AbstractControl { + return this.alarmCommentFormGroup.get('alarmComment'); + } + + getAlarmCommentEditFormControl(): AbstractControl { + return this.alarmCommentFormGroup.get('alarmCommentEdit'); + } + + private clearCommentInput(): void { + this.getAlarmCommentFormControl().patchValue(''); + } + + private clearCommentEditInput(): void { + this.getAlarmCommentEditFormControl().patchValue(''); + } + + private getAlarmCommentById(id: string): AlarmComment { + return this.alarmComments.find(comment => comment.id.id === id); + } + + private getDataElementByCommentId(commentId: string): AlarmCommentsDisplayData { + return this.displayData.find(commentDisplayData => commentDisplayData.commentId === commentId); + } + + private getDataForExport() { + let dataToExport = []; + for (let row of this.displayData) { + let exportRow = { + [this.translate.instant('alarm-activity.author')]: row.isSystemComment ? + this.translate.instant('alarm-activity.system') : row.displayName, + [this.translate.instant('alarm-activity.created-date')]: row.createdTime, + [this.translate.instant('alarm-activity.edited-date')]: row.editedTime, + [this.translate.instant('alarm-activity.text')]: row.commentText + } + dataToExport.push(exportRow) + } + return dataToExport; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html index 78f72f55f4..0db5bd9584 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html @@ -1,6 +1,6 @@ -
+

{{ 'alarm.alarm-details' | translate }}

@@ -29,71 +29,63 @@
-
+
- - alarm.created-time - - alarm.originator + + alarm.severity + +
-
+
alarm.start-time - - alarm.end-time - - - -
-
- - alarm.ack-time - - - - alarm.clear-time - + + alarm.duration + -
alarm.type - - alarm.severity - - alarm.status
- - + + + + + + alarm.show-more + + + + + + +
+ + +
-
+ + + + + +
+ + +
+ +
+ +
+
+ + alarm.alarm-status-list + + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + + + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + + +
+
+ + alarm.alarm-type-list + + + {{type}} + cancel + + + + +
+
+ + {{ 'alarm.search-propagated-alarms' | translate }} + + + {{ (userMode ? 'alarm.assigned-to-me' : 'alarm.assigned-to-current-user') | translate }} + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss new file mode 100644 index 0000000000..1c10e244b5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: block; + overflow: hidden; + max-width: 100%; + .mdc-button { + max-width: 100%; + } +} + +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts new file mode 100644 index 0000000000..b46fbf506c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts @@ -0,0 +1,318 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + forwardRef, + Inject, + InjectionToken, + Input, + OnDestroy, + OnInit, + Optional, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { AlarmFilterConfig } from '@shared/models/query/query.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { + AlarmSearchStatus, + alarmSearchStatusTranslations, + AlarmSeverity, + alarmSeverityTranslations +} from '@shared/models/alarm.models'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { TranslateService } from '@ngx-translate/core'; + +export const ALARM_FILTER_CONFIG_DATA = new InjectionToken('AlarmFilterConfigData'); + +export interface AlarmFilterConfigData { + panelMode: boolean; + userMode: boolean; + alarmFilterConfig: AlarmFilterConfig; +} + +// @dynamic +@Component({ + selector: 'tb-alarm-filter-config', + templateUrl: './alarm-filter-config.component.html', + styleUrls: ['./alarm-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmFilterConfigComponent), + multi: true + } + ] +}) +export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { + + @ViewChild('alarmFilterPanel') + alarmFilterPanel: TemplateRef; + + @Input() disabled: boolean; + + @coerceBoolean() + @Input() + buttonMode = true; + + @coerceBoolean() + @Input() + userMode = false; + + @coerceBoolean() + @Input() + propagatedFilter = true; + + panelMode = false; + + readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, + AlarmSearchStatus.CLEARED, + AlarmSearchStatus.ACK, + AlarmSearchStatus.UNACK]; + + alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + + alarmSeverityTranslationMap = alarmSeverityTranslations; + + buttonDisplayValue = this.translate.instant('alarm.alarm-filter'); + + alarmFilterConfigForm: UntypedFormGroup; + + alarmFilterOverlayRef: OverlayRef; + + panelResult: AlarmFilterConfig = null; + + private alarmFilterConfig: AlarmFilterConfig; + + private propagateChange = (_: any) => {}; + + constructor(@Optional() @Inject(ALARM_FILTER_CONFIG_DATA) + private data: AlarmFilterConfigData | undefined, + @Optional() + private overlayRef: OverlayRef, + private fb: UntypedFormBuilder, + private translate: TranslateService, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef) { + } + + ngOnInit(): void { + if (this.data) { + this.panelMode = this.data.panelMode; + this.userMode = this.data.userMode; + this.alarmFilterConfig = this.data.alarmFilterConfig; + } + this.alarmFilterConfigForm = this.fb.group({ + statusList: [null, []], + severityList: [null, []], + typeList: [null, []], + searchPropagatedAlarms: [false, []], + assignedToCurrentUser: [false, []], + assigneeId: [null, []] + }); + this.alarmFilterConfigForm.valueChanges.subscribe( + () => { + this.updateValidators(); + if (!this.buttonMode) { + this.alarmConfigUpdated(this.alarmFilterConfigForm.value); + } + } + ); + if (this.panelMode) { + this.updateAlarmConfigForm(this.alarmFilterConfig); + } + } + + ngOnDestroy(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmFilterConfigForm.disable({emitEvent: false}); + } else { + this.alarmFilterConfigForm.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(alarmFilterConfig?: AlarmFilterConfig): void { + this.alarmFilterConfig = alarmFilterConfig; + this.updateButtonDisplayValue(); + this.updateAlarmConfigForm(alarmFilterConfig); + } + + private updateValidators() { + const assignedToCurrentUser = this.alarmFilterConfigForm.get('assignedToCurrentUser').value; + if (assignedToCurrentUser) { + this.alarmFilterConfigForm.get('assigneeId').disable({emitEvent: false}); + } else { + this.alarmFilterConfigForm.get('assigneeId').enable({emitEvent: false}); + } + this.alarmFilterConfigForm.get('assigneeId').updateValueAndValidity({emitEvent: false}); + } + + toggleAlarmFilterPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const config = new OverlayConfig({ + panelClass: 'tb-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '80vh', + height: 'min-content', + minWidth: '' + }); + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement) + .withPositions([connectedPosition]); + + this.alarmFilterOverlayRef = this.overlay.create(config); + this.alarmFilterOverlayRef.backdropClick().subscribe(() => { + this.alarmFilterOverlayRef.dispose(); + }); + this.alarmFilterOverlayRef.attach(new TemplatePortal(this.alarmFilterPanel, + this.viewContainerRef)); + } + + cancel() { + this.updateAlarmConfigForm(this.alarmFilterConfig); + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.alarmFilterOverlayRef.dispose(); + } + } + + update() { + this.alarmConfigUpdated(this.alarmFilterConfigForm.value); + if (this.panelMode) { + this.panelResult = this.alarmFilterConfig; + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.alarmFilterOverlayRef.dispose(); + } + } + + public alarmTypeList(): string[] { + return this.alarmFilterConfigForm.get('typeList').value; + } + + public removeAlarmType(type: string): void { + const types: string[] = this.alarmFilterConfigForm.get('typeList').value; + const index = types.indexOf(type); + if (index >= 0) { + types.splice(index, 1); + this.alarmFilterConfigForm.get('typeList').setValue(types); + this.alarmFilterConfigForm.get('typeList').markAsDirty(); + } + } + + public addAlarmType(event: MatChipInputEvent): void { + const input = event.chipInput.inputElement; + const value = event.value; + + let types: string[] = this.alarmFilterConfigForm.get('typeList').value; + + if ((value || '').trim()) { + if (!types) { + types = []; + } + types.push(value.trim()); + this.alarmFilterConfigForm.get('typeList').setValue(types); + this.alarmFilterConfigForm.get('typeList').markAsDirty(); + } + + if (input) { + input.value = ''; + } + } + + private updateAlarmConfigForm(alarmFilterConfig?: AlarmFilterConfig) { + this.alarmFilterConfigForm.patchValue({ + statusList: alarmFilterConfig?.statusList, + severityList: alarmFilterConfig?.severityList, + typeList: alarmFilterConfig?.typeList, + searchPropagatedAlarms: alarmFilterConfig?.searchPropagatedAlarms, + assignedToCurrentUser: alarmFilterConfig?.assignedToCurrentUser, + assigneeId: alarmFilterConfig?.assigneeId + }, {emitEvent: false}); + this.updateValidators(); + } + + private alarmConfigUpdated(alarmFilterConfig: AlarmFilterConfig) { + this.alarmFilterConfig = alarmFilterConfig; + this.updateButtonDisplayValue(); + this.propagateChange(this.alarmFilterConfig); + } + + private updateButtonDisplayValue() { + if (this.buttonMode) { + const filterTextParts: string[] = []; + if (this.alarmFilterConfig?.statusList?.length) { + filterTextParts.push(this.alarmFilterConfig.statusList.map(s => + this.translate.instant(alarmSearchStatusTranslations.get(s))).join(', ')); + } + if (this.alarmFilterConfig?.severityList?.length) { + filterTextParts.push(this.alarmFilterConfig.severityList.map(s => + this.translate.instant(alarmSeverityTranslations.get(s))).join(', ')); + } + if (this.alarmFilterConfig?.typeList?.length) { + filterTextParts.push(this.alarmFilterConfig.typeList.join(', ')); + } + if (this.alarmFilterConfig?.assignedToCurrentUser) { + filterTextParts.push(this.translate.instant('alarm.assigned-to-me')); + } else if (this.alarmFilterConfig?.assigneeId) { + filterTextParts.push(this.translate.instant('alarm.assigned')); + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('alarm.alarm-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('alarm.filter-title') + `: ${filterTextParts.join(', ')}`; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 6cc51857e7..28e64738a8 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ /// import { + CellActionDescriptorType, DateEntityTableColumn, EntityTableColumn, EntityTableConfig @@ -31,9 +32,11 @@ import { EntityId } from '@shared/models/id/entity-id'; import { AlarmInfo, AlarmQuery, + AlarmQueryV2, AlarmSearchStatus, alarmSeverityColors, alarmSeverityTranslations, + AlarmsMode, alarmStatusTranslations } from '@app/shared/models/alarm.models'; import { AlarmService } from '@app/core/http/alarm.service'; @@ -43,32 +46,52 @@ import { AlarmDetailsDialogComponent, AlarmDetailsDialogData } from '@home/components/alarm/alarm-details-dialog.component'; -import { DAY, historyInterval } from '@shared/models/time/time.models'; +import { DAY, forAllTimeInterval, historyInterval } from '@shared/models/time/time.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; +import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { + ALARM_ASSIGNEE_PANEL_DATA, + AlarmAssigneePanelComponent, + AlarmAssigneePanelData +} from '@home/components/alarm/alarm-assignee-panel.component'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { isDefinedAndNotNull } from '@core/utils'; +import { UtilsService } from '@core/services/utils.service'; +import { AlarmFilterConfig } from '@shared/models/query/query.models'; +import { EntityService } from '@core/http/entity.service'; export class AlarmTableConfig extends EntityTableConfig { private authUser = getCurrentAuthUser(this.store); - searchStatus: AlarmSearchStatus; + alarmFilterConfig: AlarmFilterConfig; constructor(private alarmService: AlarmService, + private entityService: EntityService, private dialogService: DialogService, private translate: TranslateService, private datePipe: DatePipe, private dialog: MatDialog, + private alarmsMode: AlarmsMode = AlarmsMode.ALL, public entityId: EntityId = null, - private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY, - private store: Store) { + private defaultAlarmFilterConfig: AlarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]}, + private store: Store, + private viewContainerRef: ViewContainerRef, + private overlay: Overlay, + private cd: ChangeDetectorRef, + private utilsService: UtilsService, + pageMode = false) { super(); - this.loadDataOnInit = false; + this.loadDataOnInit = pageMode; this.tableTitle = ''; this.useTimePageLink = true; - this.pageMode = false; - this.defaultTimewindowInterval = historyInterval(DAY * 30); + this.forAllTimeEnabled = true; + this.pageMode = pageMode; + this.defaultTimewindowInterval = forAllTimeInterval(); this.detailsPanelEnabled = false; this.selectionEnabled = false; this.searchEnabled = true; @@ -79,7 +102,7 @@ export class AlarmTableConfig extends EntityTableConfig this.entityTranslations = entityTypeTranslations.get(EntityType.ALARM); this.entityResources = { } as EntityTypeResource; - this.searchStatus = defaultSearchStatus; + this.alarmFilterConfig = defaultAlarmFilterConfig; this.headerComponent = AlarmTableHeaderComponent; @@ -93,14 +116,33 @@ export class AlarmTableConfig extends EntityTableConfig new EntityTableColumn('originatorName', 'alarm.originator', '25%', (entity) => entity.originatorName, entity => ({}), false)); this.columns.push( - new EntityTableColumn('type', 'alarm.type', '25%')); + new EntityTableColumn('type', 'alarm.type', '25%', + entity => this.utilsService.customTranslation(entity.type, entity.type))); this.columns.push( new EntityTableColumn('severity', 'alarm.severity', '25%', (entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)), - entity => ({ - fontWeight: 'bold', - color: alarmSeverityColors.get(entity.severity) - }))); + entity => ({ + fontWeight: 'bold', + color: alarmSeverityColors.get(entity.severity) + }))); + this.columns.push( + new EntityTableColumn('assignee', 'alarm.assignee', '240px', + (entity) => { + return this.getAssigneeTemplate(entity) + }, + () => ({}), + false, + () => ({}), + (entity) => undefined, + false, + { + icon: 'keyboard_arrow_down', + type: CellActionDescriptorType.DEFAULT, + isEnabled: (entity) => true, + name: this.translate.instant('alarm.assign'), + onAction: ($event, entity) => this.openAlarmAssigneePanel($event, entity) + }) + ) this.columns.push( new EntityTableColumn('status', 'alarm.status', '25%', (entity) => this.translate.instant(alarmStatusTranslations.get(entity.status)))); @@ -116,12 +158,18 @@ export class AlarmTableConfig extends EntityTableConfig } fetchAlarms(pageLink: TimePageLink): Observable> { - const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, true); - return this.alarmService.getAlarms(query); + const alarmFilter = this.entityService.resolveAlarmFilter(this.alarmFilterConfig, false); + const query = new AlarmQueryV2(this.entityId, pageLink, alarmFilter); + switch (this.alarmsMode) { + case AlarmsMode.ALL: + return this.alarmService.getAllAlarmsV2(query); + case AlarmsMode.ENTITY: + return this.alarmService.getAlarmsV2(query); + } } showAlarmDetails(entity: AlarmInfo) { - const isPermissionWrite = this.authUser.authority !== Authority.CUSTOMER_USER || entity.customerId.id === this.authUser.customerId; + const isPermissionWrite = this.authUser.authority !== Authority.CUSTOMER_USER || entity.customerId?.id === this.authUser.customerId; this.dialog.open (AlarmDetailsDialogComponent, { @@ -132,7 +180,8 @@ export class AlarmTableConfig extends EntityTableConfig alarm: entity, allowAcknowledgment: isPermissionWrite, allowClear: isPermissionWrite, - displayDetails: true + displayDetails: true, + allowAssign: true } }).afterClosed().subscribe( (res) => { @@ -142,4 +191,107 @@ export class AlarmTableConfig extends EntityTableConfig } ); } + + getAssigneeTemplate(entity: AlarmInfo): string { + return ` + + ${isDefinedAndNotNull(entity.assigneeId) ? + ` + + ${this.getUserInitials(entity)} + + ${this.getUserDisplayName(entity)} + ` + : + ` + account_circle + ${this.translate.instant('alarm.unassigned')} + ` + } + ` + } + + getUserDisplayName(entity: AlarmInfo) { + let displayName = ''; + if ((entity.assignee.firstName && entity.assignee.firstName.length > 0) || + (entity.assignee.lastName && entity.assignee.lastName.length > 0)) { + if (entity.assignee.firstName) { + displayName += entity.assignee.firstName; + } + if (entity.assignee.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.assignee.lastName; + } + } else { + displayName = entity.assignee.email; + } + return displayName; + } + + getUserInitials(entity: AlarmInfo): string { + let initials = ''; + if (entity.assignee.firstName && entity.assignee.firstName.length || + entity.assignee.lastName && entity.assignee.lastName.length) { + if (entity.assignee.firstName) { + initials += entity.assignee.firstName.charAt(0); + } + if (entity.assignee.lastName) { + initials += entity.assignee.lastName.charAt(0); + } + } else { + initials += entity.assignee.email.charAt(0); + } + return initials.toUpperCase(); + } + + getAvatarBgColor(entity: AlarmInfo) { + return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + + openAlarmAssigneePanel($event: Event, entity: AlarmInfo) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + config.minWidth = '260px'; + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: ALARM_ASSIGNEE_PANEL_DATA, + useValue: { + alarmId: entity.id.id, + assigneeId: entity.assigneeId?.id + } as AlarmAssigneePanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const componentRef = overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent, + this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.reassigned) { + this.updateData() + } + }); + } + } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html index 0d0c3e54c8..673386129e 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html @@ -1,6 +1,6 @@ - - alarm.alarm-status - - - {{ alarmSearchStatusTranslationsMap.get(alarmSearchStatusEnum[status]) | translate }} - - - + + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss index e8d3ee982b..202ad4a8ec 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,32 +16,7 @@ @import '../../../../../scss/constants'; :host { - /* flex: 1; - display: flex; - justify-content: flex-start; */ padding-right: 8px; - min-width: 160px; -} - -:host ::ng-deep { - mat-form-field { - font-size: 16px; - width: 200px; - - .mat-form-field-wrapper { - padding-bottom: 0; - } - - .mat-form-field-underline { - bottom: 0; - } - - @media #{$mat-xs} { - width: 100%; - - .mat-form-field-infix { - width: auto !important; - } - } - } + overflow: hidden; + max-width: 100%; } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts index 7378aec16c..f25c086c27 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import { AppState } from '@core/core.state'; import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; import { AlarmInfo, AlarmSearchStatus, alarmSearchStatusTranslations } from '@shared/models/alarm.models'; import { AlarmTableConfig } from './alarm-table-config'; +import { AlarmFilterConfig } from '@shared/models/query/query.models'; @Component({ selector: 'tb-alarm-table-header', @@ -28,11 +29,6 @@ import { AlarmTableConfig } from './alarm-table-config'; }) export class AlarmTableHeaderComponent extends EntityTableHeaderComponent { - alarmSearchStatusTranslationsMap = alarmSearchStatusTranslations; - - alarmSearchStatusTypes = Object.keys(AlarmSearchStatus); - alarmSearchStatusEnum = AlarmSearchStatus; - get alarmTableConfig(): AlarmTableConfig { return this.entitiesTableConfig as AlarmTableConfig; } @@ -41,8 +37,8 @@ export class AlarmTableHeaderComponent extends EntityTableHeaderComponent - + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss index 351d962496..09a2c62c4c 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,48 @@ */ :host ::ng-deep { tb-entities-table { - .mat-drawer-container { - background-color: white; + &.tb-details-mode { + .mat-drawer-container { + background-color: white; + } + } + .assignee-cell { + line-height: normal; + .assigned-container { + display: inline-block; + vertical-align: baseline; + max-width: 180px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + .user-avatar { + display: inline-flex; + justify-content: center; + align-items: center; + margin-right: 8px; + border-radius: 50%; + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + color: white; + font-size: 13px; + font-weight: 700; + } + } + .unassigned-container { + display: inline-flex; + align-items: center; + .material-icons.unassigned-icon { + width: 28px; + height: 28px; + font-size: 28px; + margin-right: 8px; + color: rgba(0, 0, 0, 0.38); + overflow: visible; + } + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts index d642bcef7a..92f5d202a2 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { MatDialog } from '@angular/material/dialog'; @@ -22,10 +22,24 @@ import { EntityId } from '@shared/models/id/entity-id'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { DialogService } from '@core/services/dialog.service'; import { AlarmTableConfig } from './alarm-table-config'; -import { AlarmSearchStatus } from '@shared/models/alarm.models'; +import { AlarmSearchStatus, AlarmSeverity, AlarmsMode } from '@shared/models/alarm.models'; import { AlarmService } from '@app/core/http/alarm.service'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { EntityService } from '@core/http/entity.service'; +import { PageQueryParam } from '@shared/models/page/page-link'; +import { AlarmFilterConfig } from '@shared/models/query/query.models'; + +interface AlarmPageQueryParams extends PageQueryParam { + typeList?: Array; + statusList?: Array; + severityList?: Array; + assignedToMe?: boolean; +} @Component({ selector: 'tb-alarm-table', @@ -37,6 +51,8 @@ export class AlarmTableComponent implements OnInit { activeValue = false; dirtyValue = false; entityIdValue: EntityId; + alarmsMode = AlarmsMode.ENTITY; + detailsMode = true; @Input() set active(active: boolean) { @@ -53,7 +69,7 @@ export class AlarmTableComponent implements OnInit { set entityId(entityId: EntityId) { this.entityIdValue = entityId; if (this.alarmTableConfig && this.alarmTableConfig.entityId !== entityId) { - this.alarmTableConfig.searchStatus = AlarmSearchStatus.ANY; + this.alarmTableConfig.alarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]}; this.alarmTableConfig.entityId = entityId; this.entitiesTable.resetSortAndFilter(this.activeValue); if (!this.activeValue) { @@ -67,24 +83,81 @@ export class AlarmTableComponent implements OnInit { alarmTableConfig: AlarmTableConfig; constructor(private alarmService: AlarmService, + private entityService: EntityService, private dialogService: DialogService, private translate: TranslateService, private datePipe: DatePipe, private dialog: MatDialog, - private store: Store) { + private store: Store, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private utilsService: UtilsService, + private route: ActivatedRoute, + private router: Router) { } ngOnInit() { this.dirtyValue = !this.activeValue; + const pageMode = !!this.route.snapshot.data.isPage; + if (pageMode) { + this.detailsMode = false; + } + if (isDefinedAndNotNull(this.route.snapshot.data.alarmsMode)) { + this.alarmsMode = this.route.snapshot.data.alarmsMode; + } + const defaultAlarmFilterConfig: AlarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]}; + if (pageMode) { + const routerQueryParams: AlarmPageQueryParams = this.route.snapshot.queryParams; + if (routerQueryParams) { + const queryParams = deepClone(routerQueryParams); + let replaceUrl = false; + if (routerQueryParams?.typeList) { + defaultAlarmFilterConfig.typeList = routerQueryParams?.typeList; + delete queryParams.typeList; + replaceUrl = true; + } + if (routerQueryParams?.statusList) { + defaultAlarmFilterConfig.statusList = routerQueryParams?.statusList; + delete queryParams.statusList; + replaceUrl = true; + } + if (routerQueryParams?.severityList) { + defaultAlarmFilterConfig.severityList = routerQueryParams?.severityList; + delete queryParams.severityList; + replaceUrl = true; + } + if (routerQueryParams?.assignedToMe) { + defaultAlarmFilterConfig.assignedToCurrentUser = routerQueryParams?.assignedToMe; + delete queryParams.assignedToMe; + replaceUrl = true; + } + if (replaceUrl) { + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: '', + replaceUrl: true + }); + } + } + } this.alarmTableConfig = new AlarmTableConfig( this.alarmService, + this.entityService, this.dialogService, this.translate, this.datePipe, this.dialog, + this.alarmsMode, this.entityIdValue, - AlarmSearchStatus.ANY, - this.store + defaultAlarmFilterConfig, + this.store, + this.viewContainerRef, + this.overlay, + this.cd, + this.utilsService, + pageMode ); } diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html index 3fea8662a5..e537a27f66 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html @@ -1,6 +1,6 @@ - {{ alias }} + diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts index 7c776385a5..3880c88c3d 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ /// import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; @@ -40,7 +40,7 @@ import { isDefinedAndNotNull } from '@core/utils'; }) export class AliasesEntityAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { - selectEntityInfoFormGroup: FormGroup; + selectEntityInfoFormGroup: UntypedFormGroup; modelValue: EntityInfo | null; @@ -73,7 +73,7 @@ export class AliasesEntityAutocompleteComponent implements ControlValueAccessor, constructor(private store: Store, public translate: TranslateService, private entityService: EntityService, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { this.selectEntityInfoFormGroup = this.fb.group({ entityInfo: [null] }); diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html index bea2e9a9bd..a0986154fc 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html @@ -1,6 +1,6 @@ -
+
{{displayValue}} diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss index 12eec6669d..52271bc631 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ @import "../../../../../scss/constants"; :host { - min-width: 52px; - + &.tb-hide { + display: none; + } section.tb-aliases-entity-select { + min-width: 52px; min-height: 32px; padding: 0 6px; diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts index 3916f8472b..b34b5619ba 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { ChangeDetectorRef, - Component, + Component, HostBinding, Injector, Input, OnDestroy, @@ -47,6 +47,9 @@ import { AliasFilterType } from '@shared/models/alias.models'; }) export class AliasesEntitySelectComponent implements OnInit, OnDestroy { + @HostBinding('class') + aliasesEntitySelectClass = 'tb-hide'; + aliasControllerValue: IAliasController; @Input() @@ -188,12 +191,14 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy { const allEntityAliases = this.aliasController.getEntityAliases(); this.entityAliasesInfo = {}; this.hasSelectableAliasEntities = false; + this.aliasesEntitySelectClass = 'tb-hide'; for (const aliasId of Object.keys(allEntityAliases)) { const aliasInfo = this.aliasController.getInstantAliasInfo(aliasId); if (aliasInfo && !aliasInfo.resolveMultiple && aliasInfo.currentEntity && aliasInfo.entityFilter && aliasInfo.entityFilter.type !== AliasFilterType.singleEntity) { this.entityAliasesInfo[aliasId] = deepClone(aliasInfo); this.hasSelectableAliasEntities = true; + this.aliasesEntitySelectClass = ''; } } } diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html index c6b1e0be89..7dc8729f33 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html @@ -1,6 +1,6 @@ -
+

{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}

diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss index 5a0d6f16fc..45920f4f75 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts index cb3d24ba79..6cc8b0e59b 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -20,9 +20,9 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { - FormBuilder, - FormControl, - FormGroup, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, FormGroupDirective, NgForm, ValidatorFn, @@ -59,7 +59,7 @@ export class EntityAliasDialogComponent extends DialogComponent, - private fb: FormBuilder, + private fb: UntypedFormBuilder, private utils: UtilsService, public translate: TranslateService, private entityService: EntityService) { @@ -103,7 +103,7 @@ export class EntityAliasDialogComponent extends DialogComponent { + return (c: UntypedFormControl) => { const newAlias = c.value.trim(); const found = this.entityAliases.find((entityAlias) => entityAlias.alias === newAlias); if (found) { @@ -122,7 +122,7 @@ export class EntityAliasDialogComponent extends DialogComponent - - - + {{ 'entity.entity-alias' | translate }} + close + diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts index 1b4e2d8bdd..2d1c0379ca 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss index 6132c393a9..66df772d2d 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,3 @@ :host { } - -:host ::ng-deep { - .mat-form-field-infix { - border-top: none; - } -} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts index 15f1152f74..be8b8d66cb 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { ControlValueAccessor, - FormBuilder, - FormControl, - FormGroup, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, FormGroupDirective, NG_VALUE_ACCESSOR, NgForm @@ -48,15 +48,15 @@ import { ErrorStateMatcher } from '@angular/material/core'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EntityAliasSelectComponent), multi: true - }, + }/*, { provide: ErrorStateMatcher, useExisting: EntityAliasSelectComponent - }] + }*/] }) export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { - selectEntityAliasFormGroup: FormGroup; + selectEntityAliasFormGroup: UntypedFormGroup; modelValue: string | null; @@ -107,7 +107,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, private entityService: EntityService, public translate: TranslateService, public truncate: TruncatePipe, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { this.selectEntityAliasFormGroup = this.fb.group({ entityAlias: [null] }); @@ -151,7 +151,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, ); } - isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const customErrorState = this.tbRequired && !this.modelValue; return originalErrorState || customErrorState; @@ -237,16 +237,19 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, } } - createEntityAlias($event: Event, alias: string) { + createEntityAlias($event: Event, alias: string, focusOnCancel = true) { $event.preventDefault(); + $event.stopPropagation(); this.creatingEntityAlias = true; if (this.callbacks && this.callbacks.createEntityAlias) { this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => { if (!newAlias) { - setTimeout(() => { - this.entityAliasInput.nativeElement.blur(); - this.entityAliasInput.nativeElement.focus(); - }, 0); + if (focusOnCancel) { + setTimeout(() => { + this.entityAliasInput.nativeElement.blur(); + this.entityAliasInput.nativeElement.focus(); + }, 0); + } } else { this.entityAliasList.push(newAlias); this.modelValue = newAlias.id; diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html index 035fa992fb..84ba272c74 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html @@ -1,6 +1,6 @@ - +

{{ title | translate }}

@@ -32,7 +32,7 @@
- alias.name + alias.name alias.entity-filter alias.resolve-multiple @@ -45,8 +45,7 @@ *ngFor="let entityAliasControl of entityAliasesFormArray().controls; let $index = index"> {{$index + 1}}.
- - + {{ 'entity.alias-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss index a4314e5efb..c89bd33373 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ } :host ::ng-deep { - .mat-dialog-content { + .mat-mdc-dialog-content { padding-top: 0 !important; padding-bottom: 0 !important; } diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts index 419649eb84..0a2c5194f8 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -21,12 +21,12 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, - FormArray, - FormBuilder, - FormControl, - FormGroup, FormGroupDirective, NgForm, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; @@ -67,7 +67,7 @@ export class EntityAliasesDialogComponent extends DialogComponent} = {}; - entityAliasesFormGroup: FormGroup; + entityAliasesFormGroup: UntypedFormGroup; submitted = false; @@ -76,7 +76,7 @@ export class EntityAliasesDialogComponent extends DialogComponent, - private fb: FormBuilder, + private fb: UntypedFormBuilder, private utils: UtilsService, private translate: TranslateService, private dialogs: DialogService, @@ -107,7 +107,8 @@ export class EntityAliasesDialogComponent extends DialogComponent { - if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { + if ([DatasourceType.entity, DatasourceType.entityCount, DatasourceType.alarmCount].includes(datasource.type) + && datasource.entityAliasId) { this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title); } }); @@ -157,14 +158,14 @@ export class EntityAliasesDialogComponent extends DialogComponent { if (entityAlias) { if (isAdd) { - (this.entityAliasesFormGroup.get('entityAliases') as FormArray) + (this.entityAliasesFormGroup.get('entityAliases') as UntypedFormArray) .push(this.createEntityAliasFormControl(entityAlias.id, entityAlias)); } else { - const aliasFormControl = (this.entityAliasesFormGroup.get('entityAliases') as FormArray).at(index); + const aliasFormControl = (this.entityAliasesFormGroup.get('entityAliases') as UntypedFormArray).at(index); aliasFormControl.get('alias').patchValue(entityAlias.alias); aliasFormControl.get('filter').patchValue(entityAlias.filter); aliasFormControl.get('resolveMultiple').patchValue(entityAlias.filter.resolveMultiple); diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index b68bf2fa33..c48299986e 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -1,6 +1,6 @@
-
- +
+
{{telemetryTypeTranslationsMap.get(attributeScope) | translate}} - + attribute.attributes-scope - @@ -58,7 +58,7 @@
- +
- +
{{ (attributeScope === latestTelemetryTypes.LATEST_TELEMETRY ? @@ -104,7 +104,7 @@
- +
{{ 'widgets-bundle.current' | translate }} @@ -139,7 +139,7 @@ - + @@ -160,17 +160,41 @@ {{ 'attribute.key' | translate }} - - {{ attribute.key }} + +
+ {{ attribute.key }} + + +
{{ 'attribute.value' | translate }} -
- {{attribute.value | tbJson}} +
+
+ {{attribute.value | tbJson}} + + +
edit @@ -249,10 +273,10 @@ widgetBundleSet" fxFlex fxLayoutAlign="center center" style="display: flex;" - class="mat-headline">widgets-bundle.empty + class="mat-headline-5">widgets-bundle.empty widget.select-widgets-bundle + class="mat-headline-5">widget.select-widgets-bundle
diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss index 10e1a87ce6..831762d1e4 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,13 @@ width: 100%; height: 100%; background: #fff; + overflow: hidden; + + &.tb-outlined-border { + box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); + border: solid 1px #e0e0e0; + border-radius: 4px; + } .mat-toolbar-tools{ min-height: auto; @@ -42,6 +49,10 @@ .table-container { overflow: auto; + .mat-mdc-table { + table-layout: fixed; + min-width: 450px; + } } .tb-entity-table-info{ @@ -55,6 +66,22 @@ overflow: hidden; text-overflow: ellipsis; } + + .ellipsis { + overflow: hidden; + text-overflow: ellipsis; + } + + .attribute-copy { + visibility: hidden; + transition: visibility 0.1s; + } + + .attribute:hover { + .attribute-copy { + visibility: visible; + } + } } } @@ -75,26 +102,6 @@ .mat-sort-header-sorted .mat-sort-header-arrow { opacity: 1 !important; } - mat-form-field.tb-attribute-scope { - font-size: 16px; - width: 200px; - - .mat-form-field-wrapper { - padding-bottom: 0; - } - - .mat-form-field-underline { - bottom: 0; - } - - @media #{$mat-xs} { - width: 100%; - - .mat-form-field-infix { - width: auto !important; - } - } - } mat-cell.tb-value-cell { cursor: pointer; mat-icon { diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index 9d6812e0f9..e9d67a3fdd 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html index 6004d37362..9cefd62825 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html @@ -1,6 +1,6 @@ + [formGroup]="attributeFormGroup" (ngSubmit)="update()" style="padding: 5px;">

audit-log.audit-log-details

- + +
-
+
- +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index e147866df9..ada22bba94 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,18 +19,20 @@ import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; -import { Widget, widgetTypesData } from '@shared/models/widget.models'; +import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models'; import { Dashboard } from '@app/shared/models/dashboard.models'; -import { IAliasController } from '@core/api/widget-api.models'; +import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; -import { isDefined, isString } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, isString } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; export interface AddWidgetDialogData { dashboard: Dashboard; aliasController: IAliasController; + stateController: IStateController; widget: Widget; widgetInfo: WidgetInfo; } @@ -38,30 +40,52 @@ export interface AddWidgetDialogData { @Component({ selector: 'tb-add-widget-dialog', templateUrl: './add-widget-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}], + providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/], styleUrls: [] }) export class AddWidgetDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { - widgetFormGroup: FormGroup; + widgetConfigModes = WidgetConfigMode; + + widgetFormGroup: UntypedFormGroup; dashboard: Dashboard; aliasController: IAliasController; + stateController: IStateController; widget: Widget; + widgetConfig: WidgetConfigComponentData; + + previewMode = false; + + hasBasicMode = false; + + get widgetConfigMode(): WidgetConfigMode { + return this.hasBasicMode ? (this.widgetConfig?.config?.configMode || WidgetConfigMode.advanced) : WidgetConfigMode.advanced; + } + + set widgetConfigMode(widgetConfigMode: WidgetConfigMode) { + if (this.hasBasicMode) { + this.widgetConfig.config.configMode = widgetConfigMode; + this.widgetFormGroup.markAsDirty(); + } + } + submitted = false; constructor(protected store: Store, protected router: Router, + public translate: TranslateService, @Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store, router, dialogRef); this.dashboard = this.data.dashboard; this.aliasController = this.data.aliasController; + this.stateController = this.data.stateController; this.widget = this.data.widget; const widgetInfo = this.data.widgetInfo; @@ -91,7 +115,8 @@ export class AddWidgetDialogComponent extends DialogComponent

dashboard.update-image

- + + + +
+ +
+ + +
+ +
+ +
+ + + + device.device-state + + + {{ 'device.any' | translate }} + + + {{ 'device.active' | translate }} + + + {{ 'device.inactive' | translate }} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.scss b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.scss new file mode 100644 index 0000000000..1c10e244b5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: block; + overflow: hidden; + max-width: 100%; + .mdc-button { + max-width: 100%; + } +} + +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts new file mode 100644 index 0000000000..bd542b2d00 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts @@ -0,0 +1,254 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Inject, + InjectionToken, + Input, + OnDestroy, + OnInit, + Optional, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { TranslateService } from '@ngx-translate/core'; +import { DeviceInfoFilter } from '@shared/models/device.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { EntityInfoData } from '@shared/models/entity.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; + +export const DEVICE_FILTER_CONFIG_DATA = new InjectionToken('DeviceFilterConfigData'); + +export interface DeviceFilterConfigData { + panelMode: boolean; + deviceInfoFilter: DeviceInfoFilter; +} + +// @dynamic +@Component({ + selector: 'tb-device-info-filter', + templateUrl: './device-info-filter.component.html', + styleUrls: ['./device-info-filter.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceInfoFilterComponent), + multi: true + } + ] +}) +export class DeviceInfoFilterComponent implements OnInit, OnDestroy, ControlValueAccessor { + + @ViewChild('deviceFilterPanel') + deviceFilterPanel: TemplateRef; + + @Input() disabled: boolean; + + @coerceBoolean() + @Input() + buttonMode = true; + + panelMode = false; + + buttonDisplayValue = this.translate.instant('device.device-filter'); + + deviceInfoFilterForm: UntypedFormGroup; + + deviceFilterOverlayRef: OverlayRef; + + panelResult: DeviceInfoFilter = null; + + private deviceProfileInfo: EntityInfoData; + + private deviceInfoFilter: DeviceInfoFilter; + + private propagateChange = (_: any) => {}; + + constructor(@Optional() @Inject(DEVICE_FILTER_CONFIG_DATA) + private data: DeviceFilterConfigData | undefined, + @Optional() + private overlayRef: OverlayRef, + private fb: UntypedFormBuilder, + private translate: TranslateService, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef, + private deviceProfileService: DeviceProfileService, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + if (this.data) { + this.panelMode = this.data.panelMode; + this.deviceInfoFilter = this.data.deviceInfoFilter; + } + this.deviceInfoFilterForm = this.fb.group({ + deviceProfileId: [null, []], + active: ['', []] + }); + this.deviceInfoFilterForm.valueChanges.subscribe( + () => { + this.updateValidators(); + if (!this.buttonMode) { + this.deviceFilterUpdated(this.deviceInfoFilterForm.value); + } + } + ); + if (this.panelMode) { + this.updateDeviceInfoFilterForm(this.deviceInfoFilter); + } + } + + ngOnDestroy(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceInfoFilterForm.disable({emitEvent: false}); + } else { + this.deviceInfoFilterForm.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(deviceInfoFilter?: DeviceInfoFilter): void { + this.deviceInfoFilter = deviceInfoFilter; + this.updateButtonDisplayValue(); + this.updateDeviceInfoFilterForm(deviceInfoFilter); + } + + private updateValidators() { + } + + toggleDeviceFilterPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const config = new OverlayConfig({ + panelClass: 'tb-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '80vh', + height: 'min-content', + minWidth: '' + }); + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement) + .withPositions([connectedPosition]); + + this.deviceFilterOverlayRef = this.overlay.create(config); + this.deviceFilterOverlayRef.backdropClick().subscribe(() => { + this.deviceFilterOverlayRef.dispose(); + }); + this.deviceFilterOverlayRef.attach(new TemplatePortal(this.deviceFilterPanel, + this.viewContainerRef)); + } + + cancel() { + this.updateDeviceInfoFilterForm(this.deviceInfoFilter); + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.deviceFilterOverlayRef.dispose(); + } + } + + update() { + this.deviceFilterUpdated(this.deviceInfoFilterForm.value); + if (this.panelMode) { + this.panelResult = this.deviceInfoFilter; + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.deviceFilterOverlayRef.dispose(); + } + } + + deviceProfileChanged(deviceProfileInfo: EntityInfoData) { + this.deviceProfileInfo = deviceProfileInfo; + this.updateButtonDisplayValue(); + } + + private updateDeviceInfoFilterForm(deviceInfoFilter?: DeviceInfoFilter) { + this.deviceInfoFilterForm.patchValue({ + deviceProfileId: deviceInfoFilter?.deviceProfileId, + active: isDefinedAndNotNull(deviceInfoFilter?.active) ? deviceInfoFilter?.active : '' + }, {emitEvent: false}); + this.updateValidators(); + } + + private deviceFilterUpdated(deviceInfoFilter: DeviceInfoFilter) { + this.deviceInfoFilter = deviceInfoFilter; + if ((this.deviceInfoFilter.active as any) === '') { + this.deviceInfoFilter.active = null; + } + this.updateButtonDisplayValue(); + this.propagateChange(this.deviceInfoFilter); + } + + private updateButtonDisplayValue() { + if (this.buttonMode) { + const filterTextParts: string[] = []; + if (isDefinedAndNotNull(this.deviceInfoFilter?.deviceProfileId)) { + if (!this.deviceProfileInfo) { + this.deviceProfileService.getDeviceProfileInfo(this.deviceInfoFilter?.deviceProfileId.id, + {ignoreLoading: true, ignoreErrors: true}).subscribe( + (deviceProfileInfo) => { + this.deviceProfileChanged(deviceProfileInfo); + }); + return; + } else { + filterTextParts.push(this.deviceProfileInfo.name); + } + } + if (isDefinedAndNotNull(this.deviceInfoFilter?.active)) { + const translationKey = this.deviceInfoFilter?.active ? 'device.active' : 'device.inactive'; + filterTextParts.push(this.translate.instant(translationKey)); + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('device.device-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('device.filter-title') + `: ${filterTextParts.join(', ')}`; + } + this.cd.detectChanges(); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts index 5816f8b968..5889ebcdf2 100644 --- a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -131,7 +131,7 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig -
+

{{ translations.add }}

diff --git a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.scss b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.scss index 79411aa2a3..46d983eb13 100644 --- a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts index 044cf3d58c..8da7e1abe9 100644 --- a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms'; import { EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models'; import { BaseData, HasId } from '@shared/models/base-data'; import { EntityId } from '@shared/models/id/entity-id'; @@ -40,7 +40,7 @@ export class AddEntityDialogComponent extends DialogComponent> implements OnInit, ErrorStateMatcher { entityComponent: EntityComponent>; - detailsForm: FormGroup; + detailsForm: UntypedFormGroup; entitiesTableConfig: EntityTableConfig>; translations: EntityTypeTranslation; @@ -98,7 +98,7 @@ export class AddEntityDialogComponent extends } } - isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const customErrorState = !!(control && control.invalid && this.submitted); return originalErrorState || customErrorState; diff --git a/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts index ae44b9f086..7a8c8e6f43 100644 --- a/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; import { ContactBased } from '@shared/models/contact-based.model'; import { AfterViewInit, ChangeDetectorRef, Directive } from '@angular/core'; import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models'; @@ -28,14 +28,14 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod export abstract class ContactBasedComponent> extends EntityComponent implements AfterViewInit { protected constructor(protected store: Store, - protected fb: FormBuilder, + protected fb: UntypedFormBuilder, protected entityValue: T, protected entitiesTableConfigValue: EntityTableConfig, protected cd: ChangeDetectorRef) { super(store, fb, entityValue, entitiesTableConfigValue, cd); } - buildForm(entity: T): FormGroup { + buildForm(entity: T): UntypedFormGroup { const entityForm = this.buildEntityForm(entity); entityForm.addControl('country', this.fb.control(entity ? entity.country : '', [Validators.maxLength(255)])); entityForm.addControl('city', this.fb.control(entity ? entity.city : '', [Validators.maxLength(255)])); @@ -82,7 +82,7 @@ export abstract class ContactBasedComponent> exten return zipValidators; } - abstract buildEntityForm(entity: T): FormGroup; + abstract buildEntityForm(entity: T): UntypedFormGroup; abstract updateEntityForm(entity: T); diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 5f023b2efb..65f4bed2ae 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -1,6 +1,6 @@ - + -
+
+
{{ headerTitle }}
{{ headerSubtitle }}
@@ -46,7 +52,7 @@
- + diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss index 27b3d4f2f8..d4ef594449 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,7 +79,6 @@ .tb-header-button { .tb-btn-header { position: relative !important; - display: inline-block !important; animation: tbMoveFromTopFade .3s ease both; &.tb-hide { @@ -92,25 +91,25 @@ :host ::ng-deep { .tb-help { - .mat-icon-button.mat-primary { + .mat-mdc-icon-button.mat-primary { color: rgba(0, 0, 0, 0.52); } } - .mat-card-content { + .mat-mdc-card-content { position: relative; overflow: hidden; - > .mat-tab-group { - > .mat-tab-body-wrapper { + > .mat-mdc-tab-group { + > .mat-mdc-tab-body-wrapper { position: absolute; top: 49px; left: 0; right: 0; bottom: 0; } - > .mat-tab-header { - .mat-tab-label { + > .mat-mdc-tab-header { + .mat-mdc-tab { min-width: 40px; } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts index d0b141256e..68dc59407e 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import { AppState } from '@core/core.state'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { BaseData, HasId } from '@shared/models/base-data'; import { ActivatedRoute, Router } from '@angular/router'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { AssetId } from '@shared/models/id/asset-id'; import { TranslateService } from '@ngx-translate/core'; import { deepClone } from '@core/utils'; @@ -140,10 +140,14 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl }); } - confirmForm(): FormGroup { + confirmForm(): UntypedFormGroup { return this.detailsForm; } + goBack(): void { + this.router.navigate(['../'], { relativeTo: this.route }); + } + private onUpdateEntity() { this.broadcast.broadcast('updateBreadcrumb'); this.isReadOnly = this.entitiesTableConfig.detailsReadonly(this.entity); @@ -164,7 +168,7 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl if (result) { this.entitiesTableConfig.deleteEntity(entity.id).subscribe( () => { - this.router.navigate(['../'], {relativeTo: this.route}); + this.goBack(); } ); } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html index cba0b85ce9..d4977a3ec5 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html @@ -1,6 +1,6 @@
- - + {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index 30807d698e..1f6a50a031 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, - FormBuilder, - FormGroup, + UntypedFormBuilder, + UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, @@ -60,7 +60,7 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, Va valueTypeEnum = EntityKeyValueType; - booleanFilterPredicateFormGroup: FormGroup; + booleanFilterPredicateFormGroup: UntypedFormGroup; booleanOperations = Object.keys(BooleanOperation); booleanOperationEnum = BooleanOperation; @@ -68,7 +68,7 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, Va private propagateChange = null; - constructor(private fb: FormBuilder) { + constructor(private fb: UntypedFormBuilder) { } ngOnInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html index e9b48f0d26..0a4bd2d3cd 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html @@ -1,6 +1,6 @@ - +

filter.complex-filter

diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts index a91d9cddc5..6eca715187 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { @@ -40,7 +40,7 @@ export class ComplexFilterPredicateDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { - complexFilterFormGroup: FormGroup; + complexFilterFormGroup: UntypedFormGroup; complexOperations = Object.keys(ComplexOperation); complexOperationEnum = ComplexOperation; @@ -55,7 +55,7 @@ export class ComplexFilterPredicateDialogComponent extends @Inject(MAT_DIALOG_DATA) public data: ComplexFilterPredicateDialogData, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store, router, dialogRef); this.isAdd = this.data.isAdd; @@ -74,7 +74,7 @@ export class ComplexFilterPredicateDialogComponent extends ngOnInit(): void { } - isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const customErrorState = !!(control && control.invalid && this.submitted); return originalErrorState || customErrorState; diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html index 13ca2d8ddd..1caf878b88 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html @@ -1,6 +1,6 @@ - - - + {{ 'filter.filter' | translate }} + close + diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.models.ts b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.models.ts index fa0de69cef..ba8682da95 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.scss deleted file mode 100644 index 6132c393a9..0000000000 --- a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.scss +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host { - -} - -:host ::ng-deep { - .mat-form-field-infix { - border-top: none; - } -} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts index 2b38898f34..6c844e4a9b 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { ControlValueAccessor, - FormBuilder, - FormControl, - FormGroup, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, FormGroupDirective, NG_VALUE_ACCESSOR, NgForm @@ -41,7 +41,7 @@ import { Filter } from '@shared/models/query/query.models'; @Component({ selector: 'tb-filter-select', templateUrl: './filter-select.component.html', - styleUrls: ['./filter-select.component.scss'], + styleUrls: [], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FilterSelectComponent), @@ -54,7 +54,7 @@ import { Filter } from '@shared/models/query/query.models'; }) export class FilterSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { - selectFilterFormGroup: FormGroup; + selectFilterFormGroup: UntypedFormGroup; modelValue: string | null; @@ -101,7 +101,7 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public translate: TranslateService, public truncate: TruncatePipe, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { this.selectFilterFormGroup = this.fb.group({ filter: [null] }); @@ -140,7 +140,7 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte ); } - isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const customErrorState = this.tbRequired && !this.modelValue; return originalErrorState || customErrorState; @@ -226,16 +226,19 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte } } - createFilter($event: Event, filter: string) { + createFilter($event: Event, filter: string, focusOnCancel = true) { $event.preventDefault(); + $event.stopPropagation(); this.creatingFilter = true; if (this.callbacks && this.callbacks.createFilter) { this.callbacks.createFilter(filter).subscribe((newFilter) => { if (!newFilter) { - setTimeout(() => { - this.filterInput.nativeElement.blur(); - this.filterInput.nativeElement.focus(); - }, 0); + if (focusOnCancel) { + setTimeout(() => { + this.filterInput.nativeElement.blur(); + this.filterInput.nativeElement.focus(); + }, 0); + } } else { this.filterList.push(newFilter); this.modelValue = newFilter.id; diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html index a4f26d10e9..54b69f1de8 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html @@ -1,6 +1,6 @@ - +

{{(data.isAdd ? 'filter.add-key-filter' : (data.readonly ? 'filter.key-filter' : 'filter.edit-key-filter')) | translate}}

@@ -46,7 +46,7 @@ [matAutocompleteDisabled]="!showAutocomplete"> @@ -60,7 +60,7 @@
- + filter.value-type.value-type diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.scss index bfc62d5570..aeb088d38d 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,13 @@ * limitations under the License. */ :host ::ng-deep { - .entity-key { - mat-form-field { - .mat-form-field-wrapper { - .mat-form-field-infix { - width: auto; + .mat-mdc-form-field.tb-value-type { + mat-select-trigger { + .mat-icon { + vertical-align: middle; + margin-right: 8px; + svg { + vertical-align: initial; } } } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index b410d8a529..d8b1a21f6a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { @@ -63,9 +63,9 @@ export class KeyFilterDialogComponent extends private dirty = false; private entityKeysName: Observable>; - private destroy$ = new Subject(); + private destroy$ = new Subject(); - keyFilterFormGroup: FormGroup; + keyFilterFormGroup: UntypedFormGroup; entityKeyTypes = this.data.telemetryKeysOnly ? @@ -96,7 +96,7 @@ export class KeyFilterDialogComponent extends private deviceProfileService: DeviceProfileService, private dialogs: DialogService, private translate: TranslateService, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store, router, dialogRef); this.keyFilterFormGroup = this.fb.group( @@ -191,7 +191,7 @@ export class KeyFilterDialogComponent extends this.destroy$.complete(); } - isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const customErrorState = !!(control && control.invalid && this.submitted); return originalErrorState || customErrorState; diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html index 62a6325e4c..c29ae4ca72 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html @@ -1,6 +1,6 @@
- - + {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts index f83ac2c8cb..54b0ce76ea 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, - FormBuilder, - FormGroup, + UntypedFormBuilder, + UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, @@ -60,7 +60,7 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, Va @Input() valueType: EntityKeyValueType; - numericFilterPredicateFormGroup: FormGroup; + numericFilterPredicateFormGroup: UntypedFormGroup; valueTypeEnum = EntityKeyValueType; @@ -70,7 +70,7 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, Va private propagateChange = null; - constructor(private fb: FormBuilder) { + constructor(private fb: UntypedFormBuilder) { } ngOnInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html index a121cd282e..26db51f8b0 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html @@ -1,6 +1,6 @@
- - + {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts index af5979706d..db233d526f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, - FormBuilder, - FormGroup, + UntypedFormBuilder, + UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, @@ -60,7 +60,7 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, Val valueTypeEnum = EntityKeyValueType; - stringFilterPredicateFormGroup: FormGroup; + stringFilterPredicateFormGroup: UntypedFormGroup; stringOperations = Object.keys(StringOperation); stringOperationEnum = StringOperation; @@ -68,7 +68,7 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, Val private propagateChange = null; - constructor(private fb: FormBuilder) { + constructor(private fb: UntypedFormBuilder) { } ngOnInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html index 960bcd5f9a..e5f1449eb7 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html @@ -1,6 +1,6 @@ + diff --git a/ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts b/ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts new file mode 100644 index 0000000000..8ee5771fe7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts @@ -0,0 +1,112 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + NgZone, + OnDestroy, + Renderer2, + ViewContainerRef +} from '@angular/core'; +import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; +import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs'; +import { distinctUntilChanged, map, share, tap } from 'rxjs/operators'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component'; +import { NotificationSubscriber } from '@shared/models/websocket/notification-ws.models'; +import { select, Store } from '@ngrx/store'; +import { selectIsAuthenticated } from '@core/auth/auth.selectors'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-notification-bell', + templateUrl: './notification-bell.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotificationBellComponent implements OnDestroy { + + private notificationSubscriber: NotificationSubscriber; + private notificationCountSubscriber: Subscription; + private countSubject = new BehaviorSubject(0); + + count$ = this.countSubject.asObservable().pipe( + distinctUntilChanged(), + map((value) => value >= 100 ? '99+' : value), + tap(() => setTimeout(() => this.cd.markForCheck())), + share({ + connector: () => new ReplaySubject(1) + }) + ); + + constructor( + private notificationWsService: NotificationWebsocketService, + private zone: NgZone, + private cd: ChangeDetectorRef, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private store: Store,) { + this.store.pipe(select(selectIsAuthenticated)).subscribe((value) => { + if (value) { + this.initSubscription(); + } + }); + } + + ngOnDestroy() { + this.unsubscribeSubscription(); + } + + showNotification($event: Event, createVersionButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + this.unsubscribeSubscription(); + const trigger = createVersionButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const showNotificationPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ShowNotificationPopoverComponent, 'bottom', true, null, + { + onClose: () => { + showNotificationPopover.hide(); + this.initSubscription(); + }, + counter: this.countSubject + }, + {maxHeight: '90vh', height: '100%', padding: '10px'}, + {width: '400px', minWidth: '100%', maxWidth: '100%'}, + {height: '100%', flexDirection: 'column', boxSizing: 'border-box', display: 'flex', margin: '0 -16px'}, false); + showNotificationPopover.tbComponentRef.instance.popoverComponent = showNotificationPopover; + } + } + + private initSubscription() { + this.notificationSubscriber = NotificationSubscriber.createNotificationCountSubscription(this.notificationWsService, this.zone); + this.notificationCountSubscriber = this.notificationSubscriber.notificationCount$.subscribe(value => this.countSubject.next(value)); + + this.notificationSubscriber.subscribe(); + } + + private unsubscribeSubscription() { + this.notificationCountSubscriber.unsubscribe(); + this.notificationSubscriber.unsubscribe(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html new file mode 100644 index 0000000000..60f069c28e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html @@ -0,0 +1,26 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.ts b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.ts new file mode 100644 index 0000000000..2842e220f9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.ts @@ -0,0 +1,73 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { + RequestNotificationDialogData, + SentNotificationDialogComponent +} from '@home/pages/notification/sent/sent-notification-dialog.componet'; +import { NotificationTemplate } from '@shared/models/notification.models'; +import { MatDialog } from '@angular/material/dialog'; +import { ActiveComponentService } from '@core/services/active-component.service'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; + +@Component({ + selector: 'tb-send-notification-button', + templateUrl: './send-notification-button.component.html', +}) +export class SendNotificationButtonComponent { + + authUser = getCurrentAuthUser(this.store); + + constructor(private dialog: MatDialog, + private store: Store, + private activeComponentService: ActiveComponentService) { + } + + sendNotification($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(SentNotificationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: true + } + }).afterClosed().subscribe((res) => { + if (res) { + const comp = this.activeComponentService.getCurrentActiveComponent(); + if (comp instanceof EntitiesTableComponent) { + const entitiesTableComponent = comp as EntitiesTableComponent; + if (entitiesTableComponent.entitiesTableConfig.entityType === EntityType.NOTIFICATION_REQUEST) { + entitiesTableComponent.entitiesTableConfig.updateData(); + } + } + } + }); + } + + public show(): boolean { + return this.authUser.authority !== Authority.CUSTOMER_USER; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html new file mode 100644 index 0000000000..0a0a5e3cdb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html @@ -0,0 +1,47 @@ + +
+
notification.notification
+ +
+ +
+
+
+ + + +
+
+
+ +
+ +
+ + empty notification + notification.no-notifications-yet + diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts new file mode 100644 index 0000000000..5e94289055 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Input, NgZone, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Notification, NotificationRequest } from '@shared/models/notification.models'; +import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; +import { BehaviorSubject, Observable, ReplaySubject, Subscription } from 'rxjs'; +import { share, tap } from 'rxjs/operators'; +import { Router } from '@angular/router'; +import { NotificationSubscriber } from '@shared/models/websocket/notification-ws.models'; + +@Component({ + selector: 'tb-show-notification-popover', + templateUrl: './show-notification-popover.component.html', + styleUrls: [] +}) +export class ShowNotificationPopoverComponent extends PageComponent implements OnDestroy, OnInit { + + @Input() + onClose: () => void; + + @Input() + counter: BehaviorSubject; + + @Input() + popoverComponent: TbPopoverComponent; + + private notificationSubscriber: NotificationSubscriber; + private notificationCountSubscriber: Subscription; + + notifications$: Observable; + + constructor(protected store: Store, + private notificationWsService: NotificationWebsocketService, + private zone: NgZone, + private cd: ChangeDetectorRef, + private router: Router) { + super(store); + } + + ngOnInit() { + this.notificationSubscriber = NotificationSubscriber.createNotificationsSubscription(this.notificationWsService, this.zone, 6); + this.notifications$ = this.notificationSubscriber.notifications$.pipe( + share({ + connector: () => new ReplaySubject(1) + }), + tap(() => setTimeout(() => this.cd.markForCheck())) + ); + this.notificationCountSubscriber = this.notificationSubscriber.notificationCount$.subscribe(value => this.counter.next(value)); + this.notificationSubscriber.subscribe(); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.notificationCountSubscriber.unsubscribe(); + this.notificationSubscriber.unsubscribe(); + this.onClose(); + } + + markAsRead(id: string) { + const cmd = NotificationSubscriber.createMarkAsReadCommand(this.notificationWsService, [id]); + cmd.subscribe(); + } + + markAsAllRead($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const cmd = NotificationSubscriber.createMarkAllAsReadCommand(this.notificationWsService); + cmd.subscribe(); + } + + viewAll($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.onClose(); + this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {}); + } + + trackById(index: number, item: NotificationRequest): string { + return item.id.id; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index f15a969b19..779bc94f8f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -1,6 +1,6 @@ -
- -

device-profile.add

- -
- -
- - -
-
- - - - {{ 'device-profile.device-profile-details' | translate }} -
- - device-profile.name - - - {{ 'device-profile.name-required' | translate }} - - - {{ 'device-profile.name-max-length' | translate }} - - - - - -
{{'device-profile.mobile-dashboard-hint' | translate}}
-
- - - - device-profile.type - - - {{deviceProfileTypeTranslations.get(type) | translate}} - - - - {{ 'device-profile.type-required' | translate }} - - - - - - device-profile.description - - -
- -
- -
- {{ 'device-profile.transport-configuration' | translate }} + +

device-profile.add

+ +
+ +
+ + +
+
+ + + + {{ 'device-profile.device-profile-details' | translate }} +
- device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} + device-profile.name + + + {{ 'device-profile.name-required' | translate }} + + + {{ 'device-profile.name-max-length' | translate }} + + + + + + {{'device-profile.mobile-dashboard-hint' | translate}} + + + + + {{'device-profile.default-edge-rule-chain-hint' | translate}} + + + device-profile.type + + + {{deviceProfileTypeTranslations.get(type) | translate}} - - {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} + + {{ 'device-profile.type-required' | translate }} - - - - - -
- {{'device-profile.alarm-rules-with-count' | translate: - {count: alarmRulesFormGroup.get('alarms').value ? - alarmRulesFormGroup.get('alarms').value.length : 0} }} - - - -
- -
- {{ 'device-profile.device-provisioning' | translate }} - - - -
- -
-
+ + + + device-profile.description + + + + + + +
+ {{ 'device-profile.transport-configuration' | translate }} + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} + + + {{ 'device-profile.transport-type-required' | translate }} + + + + + +
+ +
+ {{'device-profile.alarm-rules-with-count' | translate: + {count: alarmRulesFormGroup.get('alarms').value ? + alarmRulesFormGroup.get('alarms').value.length : 0} }} + + + +
+ +
+ {{ 'device-profile.device-provisioning' | translate }} + + + +
+ +
+
+
@@ -137,8 +147,8 @@ [disabled]="(isLoading$ | async)" (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}
- -
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss index db53d82cb5..ca54ae2053 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,25 @@ */ @import "../../../../../scss/constants"; -:host-context(.tb-fullscreen-dialog .mat-dialog-container) { +:host { + height: 100%; + display: grid; + + .dialog-actions-row { + padding: 8px; + } +} + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { @media #{$mat-lt-sm} { - .mat-dialog-content { + .mat-mdc-dialog-content { max-height: 75vh; } } } :host ::ng-deep { - .mat-dialog-content { + .mat-mdc-dialog-content { display: flex; flex-direction: column; height: 100%; @@ -32,31 +41,21 @@ .mat-stepper-horizontal { display: flex; - flex-direction: column; height: 100%; overflow: hidden; - @media #{$mat-lt-sm} { - .mat-step-label { - white-space: normal; - overflow: visible; - .mat-step-text-label { - overflow: visible; - } - } + + .mat-horizontal-stepper-wrapper { + flex: 1 1 100%; } + .mat-horizontal-content-container { - height: 530px; + height: 680px; max-height: 100%; width: 100%;; overflow-y: auto; + scrollbar-gutter: stable; @media #{$mat-gt-sm} { - min-width: 800px; - } - } - .mat-horizontal-stepper-content[aria-expanded=true] { - height: 100%; - form { - height: 100%; + min-width: 500px; } } } diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts index 188fad26dc..3426593aed 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { @@ -44,12 +44,17 @@ import { } from '@shared/models/device.models'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { EntityType } from '@shared/models/entity-type.models'; -import { MatHorizontalStepper } from '@angular/material/stepper'; +import { MatStepper, StepperOrientation } from '@angular/material/stepper'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { deepTrim } from '@core/utils'; import { ServiceType } from '@shared/models/queue.models'; import { DashboardId } from '@shared/models/id/dashboard-id'; +import { RuleChainType } from '@shared/models/rule-chain.models'; +import { Observable } from 'rxjs'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { map } from 'rxjs/operators'; export interface AddDeviceProfileDialogData { deviceProfileName: string; @@ -65,8 +70,9 @@ export interface AddDeviceProfileDialogData { export class AddDeviceProfileDialogComponent extends DialogComponent implements AfterViewInit { - @ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatHorizontalStepper; - + @ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatStepper; + stepperOrientation: Observable; + stepperLabelPosition: Observable<'bottom' | 'end'>; selectedIndex = 0; showNext = true; @@ -83,26 +89,35 @@ export class AddDeviceProfileDialogComponent extends deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; - deviceProfileDetailsFormGroup: FormGroup; + deviceProfileDetailsFormGroup: UntypedFormGroup; - transportConfigFormGroup: FormGroup; + transportConfigFormGroup: UntypedFormGroup; - alarmRulesFormGroup: FormGroup; + alarmRulesFormGroup: UntypedFormGroup; - provisionConfigFormGroup: FormGroup; + provisionConfigFormGroup: UntypedFormGroup; serviceType = ServiceType.TB_RULE_ENGINE; + edgeRuleChainType = RuleChainType.EDGE; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData, public dialogRef: MatDialogRef, private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector, + private breakpointObserver: BreakpointObserver, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, private deviceProfileService: DeviceProfileService, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store, router, dialogRef); + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-sm']) + .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); + + this.stepperLabelPosition = this.breakpointObserver.observe(MediaBreakpoints['gt-md']) + .pipe(map(({matches}) => matches ? 'end' : 'bottom')); + this.deviceProfileDetailsFormGroup = this.fb.group( { name: [data.deviceProfileName, [Validators.required, Validators.maxLength(255)]], @@ -111,6 +126,7 @@ export class AddDeviceProfileDialogComponent extends defaultRuleChainId: [null, []], defaultDashboardId: [null, []], defaultQueueName: [null, []], + defaultEdgeRuleChainId: [null, []], description: ['', []] } ); @@ -165,7 +181,7 @@ export class AddDeviceProfileDialogComponent extends } } - selectedForm(): FormGroup { + selectedForm(): UntypedFormGroup { switch (this.selectedIndex) { case 0: return this.deviceProfileDetailsFormGroup; @@ -205,6 +221,9 @@ export class AddDeviceProfileDialogComponent extends if (this.deviceProfileDetailsFormGroup.get('defaultDashboardId').value) { deviceProfile.defaultDashboardId = new DashboardId(this.deviceProfileDetailsFormGroup.get('defaultDashboardId').value); } + if (this.deviceProfileDetailsFormGroup.get('defaultEdgeRuleChainId').value) { + deviceProfile.defaultEdgeRuleChainId = new RuleChainId(this.deviceProfileDetailsFormGroup.get('defaultEdgeRuleChainId').value); + } this.deviceProfileService.saveDeviceProfile(deepTrim(deviceProfile)).subscribe( (savedDeviceProfile) => { this.dialogRef.close(savedDeviceProfile); diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html index 552e0e4c47..d4d8644aad 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html @@ -1,6 +1,6 @@
- - +
- - + {{'filter.no-dynamic-value' | translate}} @@ -53,8 +51,7 @@
- - +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.scss deleted file mode 100644 index e5f3e6532f..0000000000 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host ::ng-deep { - .source-attribute { - .mat-form-field-infix{ - width: 100%; - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.ts index 0b24911ee6..e14dcc6ea7 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { DynamicValueSourceType, dynamicValueSourceTypeTranslationMap, @@ -28,7 +28,7 @@ import { AlarmConditionType } from '@shared/models/device.models'; @Component({ selector: 'tb-alarm-duration-predicate-value', templateUrl: './alarm-duration-predicate-value.component.html', - styleUrls: ['./alarm-duration-predicate-value.component.scss'], + styleUrls: [], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -68,7 +68,7 @@ export class AlarmDurationPredicateValueComponent implements ControlValueAccesso dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap; - alarmDurationPredicateValueFormGroup: FormGroup; + alarmDurationPredicateValueFormGroup: UntypedFormGroup; dynamicMode = false; @@ -76,7 +76,7 @@ export class AlarmDurationPredicateValueComponent implements ControlValueAccesso private propagateChange = null; - constructor(private fb: FormBuilder) { + constructor(private fb: UntypedFormBuilder) { } ngOnInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.html index 6a64ba4800..be17201671 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.html @@ -1,6 +1,6 @@ -
+

{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}

@@ -56,8 +56,7 @@ >
- - + diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss index fc2edaee5f..f0ac3daae1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts index 1df5a6bd4f..dcbceabcd5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { TranslateService } from '@ngx-translate/core'; @@ -52,7 +52,7 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent, - private fb: FormBuilder, + private fb: UntypedFormBuilder, public translate: TranslateService) { super(store, router, dialogRef); @@ -87,7 +87,7 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent { }; constructor(private dialog: MatDialog, - private fb: FormBuilder, + private fb: UntypedFormBuilder, private translate: TranslateService) { } @@ -110,7 +110,7 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit return this.modelValue && this.modelValue.condition.length; } - public validate(c: FormControl) { + public validate(c: UntypedFormControl) { return this.conditionSet() ? null : { alarmRuleCondition: { valid: false, diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html index 69982686b1..5e7ca9b096 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -1,6 +1,6 @@ - +

{{ (readonly ? 'device-profile.schedule' : 'device-profile.edit-schedule') | translate }}

@@ -28,13 +28,9 @@
-
-
- - -
-
+ +
-
+
device-profile.snmp.please-add-communication-config
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss index 6dcf3642ad..9a93f17b23 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,8 @@ } :host ::ng-deep { - .mat-form-field.spec { - .mat-form-field-infix { + .mat-mdc-form-field.spec { + .mat-mdc-form-field-infix { width: 160px; } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts index 6fe20f5e2f..7e0f8dd3b5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -18,16 +18,16 @@ import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, ControlValueAccessor, - FormArray, - FormBuilder, - FormGroup, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator, Validators } from '@angular/forms'; import { SnmpCommunicationConfig, SnmpSpecType, SnmpSpecTypeTranslationMap } from '@shared/models/device.models'; -import { Subject, Subscription } from 'rxjs'; +import { Subject } from 'rxjs'; import { isUndefinedOrNull } from '@core/utils'; import { takeUntil } from 'rxjs/operators'; @@ -52,34 +52,33 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On snmpSpecTypes = Object.values(SnmpSpecType); snmpSpecTypeTranslationMap = SnmpSpecTypeTranslationMap; - deviceProfileCommunicationConfig: FormGroup; + deviceProfileCommunicationConfig: UntypedFormGroup; @Input() disabled: boolean; private usedSpecType: SnmpSpecType[] = []; - private valueChange$: Subscription = null; - private destroy$ = new Subject(); + private destroy$ = new Subject(); private propagateChange = (v: any) => { }; - constructor(private fb: FormBuilder) { } + constructor(private fb: UntypedFormBuilder) { } ngOnInit(): void { this.deviceProfileCommunicationConfig = this.fb.group({ communicationConfig: this.fb.array([]) }); + this.deviceProfileCommunicationConfig.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => this.updateModel()); } ngOnDestroy() { - if (this.valueChange$) { - this.valueChange$.unsubscribe(); - } this.destroy$.next(); this.destroy$.complete(); } - communicationConfigFormArray(): FormArray { - return this.deviceProfileCommunicationConfig.get('communicationConfig') as FormArray; + get communicationConfigFormArray(): UntypedFormArray { + return this.deviceProfileCommunicationConfig.get('communicationConfig') as UntypedFormArray; } registerOnChange(fn: any): void { @@ -99,27 +98,27 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On } writeValue(communicationConfig: SnmpCommunicationConfig[]) { - if (this.valueChange$) { - this.valueChange$.unsubscribe(); - } - const communicationConfigControl: Array = []; - if (communicationConfig) { - communicationConfig.forEach((config) => { - communicationConfigControl.push(this.createdFormGroup(config)); - }); - } - this.deviceProfileCommunicationConfig.setControl('communicationConfig', this.fb.array(communicationConfigControl)); - if (!communicationConfig || !communicationConfig.length) { - this.addCommunicationConfig(); - } - if (this.disabled) { - this.deviceProfileCommunicationConfig.disable({emitEvent: false}); + if (communicationConfig?.length === this.communicationConfigFormArray.length) { + this.communicationConfigFormArray.patchValue(communicationConfig, {emitEvent: false}); } else { - this.deviceProfileCommunicationConfig.enable({emitEvent: false}); + const communicationConfigControl: Array = []; + if (communicationConfig) { + communicationConfig.forEach((config) => { + communicationConfigControl.push(this.createdFormGroup(config)); + }); + } + this.deviceProfileCommunicationConfig.setControl( + 'communicationConfig', this.fb.array(communicationConfigControl), {emitEvent: false} + ); + if (!communicationConfig || !communicationConfig.length) { + this.addCommunicationConfig(); + } + if (this.disabled) { + this.deviceProfileCommunicationConfig.disable({emitEvent: false}); + } else { + this.deviceProfileCommunicationConfig.enable({emitEvent: false}); + } } - this.valueChange$ = this.deviceProfileCommunicationConfig.valueChanges.subscribe(() => { - this.updateModel(); - }); this.updateUsedSpecType(); if (!this.disabled && !this.deviceProfileCommunicationConfig.valid) { this.updateModel(); @@ -133,16 +132,16 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On } public removeCommunicationConfig(index: number) { - this.communicationConfigFormArray().removeAt(index); + this.communicationConfigFormArray.removeAt(index); } get isAddEnabled(): boolean { - return this.communicationConfigFormArray().length !== Object.keys(SnmpSpecType).length; + return this.communicationConfigFormArray.length !== Object.keys(SnmpSpecType).length; } public addCommunicationConfig() { - this.communicationConfigFormArray().push(this.createdFormGroup()); + this.communicationConfigFormArray.push(this.createdFormGroup()); this.deviceProfileCommunicationConfig.updateValueAndValidity(); if (!this.deviceProfileCommunicationConfig.valid) { this.updateModel(); @@ -175,7 +174,7 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On }); } - private createdFormGroup(value?: SnmpCommunicationConfig): FormGroup { + private createdFormGroup(value?: SnmpCommunicationConfig): UntypedFormGroup { if (isUndefinedOrNull(value)) { value = { spec: this.getFirstUnusedSeverity(), diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html index e202370d21..94fe06137f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html @@ -1,6 +1,6 @@ - tenant-profile.tenant-profile +
-
+
relation.any-relation
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss index 0b4ae03f02..b18f226f4f 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,9 @@ .container{ width: 100%; - overflow: auto; } .header { - width: max-content; padding: 0 5px 5px; .cell { @@ -38,8 +36,9 @@ } .body { - width: max-content; padding: 0 5px 20px; + max-height: 300px; + overflow: auto; .row { padding-top: 5px; @@ -56,20 +55,3 @@ } } } - -:host ::ng-deep { - .tb-relation-filters { - .cell { - .mat-form-field { - .mat-form-field-infix { - border-top: none; - } - &.mat-form-field-appearance-standard { - .mat-form-field-flex { - padding-top: 0; - } - } - } - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts index 911aa4fe30..aa027c4846 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,13 +14,13 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, ControlValueAccessor, - FormArray, - FormBuilder, - FormGroup, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; @@ -28,7 +28,8 @@ import { RelationEntityTypeFilter } from '@shared/models/relation.models'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { Subscription } from 'rxjs'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'tb-relation-filters', @@ -42,31 +43,41 @@ import { Subscription } from 'rxjs'; } ] }) -export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit { +export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit, OnDestroy { @Input() disabled: boolean; @Input() allowedEntityTypes: Array; - relationFiltersFormGroup: FormGroup; + relationFiltersFormGroup: UntypedFormGroup; + private destroy$ = new Subject(); private propagateChange = null; - private valueChangeSubscription: Subscription = null; - constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } ngOnInit(): void { - this.relationFiltersFormGroup = this.fb.group({}); - this.relationFiltersFormGroup.addControl('relationFilters', - this.fb.array([])); + this.relationFiltersFormGroup = this.fb.group({ + relationFilters: this.fb.array([]) + }); + + this.relationFiltersFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { + this.updateModel(); + }); } - relationFiltersFormArray(): FormArray { - return this.relationFiltersFormGroup.get('relationFilters') as FormArray; + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + get relationFiltersFormArray(): UntypedFormArray { + return this.relationFiltersFormGroup.get('relationFilters') as UntypedFormArray; } registerOnChange(fn: any): void { @@ -81,32 +92,29 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa } writeValue(filters: Array): void { - if (this.valueChangeSubscription) { - this.valueChangeSubscription.unsubscribe(); - } - const relationFiltersControls: Array = []; - if (filters && filters.length) { - filters.forEach((filter) => { - relationFiltersControls.push(this.createRelationFilterFormGroup(filter)); - }); + if (filters?.length === this.relationFiltersFormArray.length) { + this.relationFiltersFormArray.patchValue(filters, {emitEvent: false}); + } else { + const relationFiltersControls: Array = []; + if (filters && filters.length) { + filters.forEach((filter) => { + relationFiltersControls.push(this.createRelationFilterFormGroup(filter)); + }); + } + this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls), {emitEvent: false}); } - this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls)); - this.valueChangeSubscription = this.relationFiltersFormGroup.valueChanges.subscribe(() => { - this.updateModel(); - }); } public removeFilter(index: number) { - (this.relationFiltersFormGroup.get('relationFilters') as FormArray).removeAt(index); + (this.relationFiltersFormGroup.get('relationFilters') as UntypedFormArray).removeAt(index); } public addFilter() { - const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray; const filter: RelationEntityTypeFilter = { relationType: null, entityTypes: [] }; - relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter)); + this.relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter)); } private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index 5c2c529815..c96b22a1cc 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -1,6 +1,6 @@
-
- +
+
@@ -52,7 +52,7 @@
- +
- +
{{ 'relation.selected-relations' | translate:{count: dataSource.selection.selected.length} }} diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss index 453c4ab897..e39d275075 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,13 @@ width: 100%; height: 100%; background: #fff; + overflow: hidden; + + &.tb-outlined-border { + box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); + border: solid 1px #e0e0e0; + border-radius: 4px; + } .mat-toolbar-tools{ min-height: auto; @@ -67,29 +74,3 @@ } } } - -:host ::ng-deep { - .mat-sort-header-sorted .mat-sort-header-arrow { - opacity: 1 !important; - } - mat-form-field.tb-relation-direction { - font-size: 16px; - width: 200px; - - .mat-form-field-wrapper { - padding-bottom: 0; - } - - .mat-form-field-underline { - bottom: 0; - } - - @media #{$mat-xs} { - width: 100%; - - .mat-form-field-infix { - width: auto !important; - } - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index 6b2649ec9a..04a3f87486 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/router-tabs.component.html b/ui-ngx/src/app/modules/home/components/router-tabs.component.html new file mode 100644 index 0000000000..afcc4d9fa9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/router-tabs.component.html @@ -0,0 +1,37 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/router-tabs.component.scss b/ui-ngx/src/app/modules/home/components/router-tabs.component.scss new file mode 100644 index 0000000000..8fe2c2b305 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/router-tabs.component.scss @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + width: 100%; + height: 100%; + + .mat-mdc-tab-nav-bar.tb-router-tabs { + a.mat-mdc-tab-link { + display: inline-flex; + align-items: center; + overflow: hidden; + line-height: 40px; + min-width: 200px; + border-bottom: none; + mat-icon { + margin-right: 8px; + margin-left: 0; + } + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + + .tb-router-tabs-content { + overflow: auto; + position: relative; + height: 100%; + } +} diff --git a/ui-ngx/src/app/modules/home/components/router-tabs.component.ts b/ui-ngx/src/app/modules/home/components/router-tabs.component.ts new file mode 100644 index 0000000000..65c0074227 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/router-tabs.component.ts @@ -0,0 +1,125 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ComponentRef, OnInit, Type, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { MenuService } from '@core/services/menu.service'; +import { distinctUntilChanged, filter, map, mergeMap, take } from 'rxjs/operators'; +import { merge } from 'rxjs'; +import { MenuSection } from '@core/services/menu.models'; +import { ActiveComponentService } from '@core/services/active-component.service'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; + +@Component({ + selector: 'tb-router-tabs', + templateUrl: './router-tabs.component.html', + styleUrls: ['./router-tabs.component.scss'] +}) +export class RouterTabsComponent extends PageComponent implements OnInit { + + @ViewChild('tabsHeaderComponent', {static: true}) tabsHeaderComponentAnchor: TbAnchorComponent; + + tabsHeaderComponentRef: ComponentRef; + + hideCurrentTabs = false; + + tabs$ = merge(this.menuService.menuSections(), + this.router.events.pipe( + filter((event) => event instanceof NavigationEnd ), + distinctUntilChanged()) + ).pipe( + mergeMap(() => this.menuService.menuSections().pipe(take(1))), + map((sections) => this.buildTabs(this.activatedRoute, sections)) + ); + + constructor(protected store: Store, + private activatedRoute: ActivatedRoute, + public router: Router, + private menuService: MenuService, + private activeComponentService: ActiveComponentService) { + super(store); + } + + ngOnInit() { + this.activatedRoute.data.subscribe( + (data) => this.buildTabsHeaderComponent(data) + ); + } + + activeComponentChanged(activeComponent: any) { + this.activeComponentService.setCurrentActiveComponent(activeComponent); + let snapshot = this.router.routerState.snapshot.root; + this.hideCurrentTabs = false; + let found = false; + while (snapshot.children.length) { + if (snapshot.component && snapshot.component === RouterTabsComponent) { + if (this.activatedRoute.snapshot === snapshot) { + found = true; + } else if (found) { + this.hideCurrentTabs = true; + break; + } + } + snapshot = snapshot.children[0]; + } + } + + private buildTabs(activatedRoute: ActivatedRoute, sections: MenuSection[]): Array { + const sectionPath = '/' + activatedRoute.pathFromRoot.map(r => r.snapshot.url) + .filter(f => !!f[0]).map(f => f.map(f1 => f1.path).join('/')).join('/'); + const found = this.findRootSection(sections, sectionPath); + if (found) { + const rootPath = sectionPath.substring(0, sectionPath.length - found.path.length); + const isRoot = rootPath === ''; + const tabs: Array = found ? found.pages.filter(page => !page.disabled && (!page.rootOnly || isRoot)) : []; + return tabs.map((tab) => ({...tab, path: rootPath + tab.path})); + } else { + return []; + } + } + + private findRootSection(sections: MenuSection[], sectionPath: string): MenuSection { + for (const section of sections) { + if (sectionPath.endsWith(section.path)) { + return section; + } + if (section.pages?.length) { + const found = this.findRootSection(section.pages, sectionPath); + if (found) { + return found; + } + } + } + return null; + } + + private buildTabsHeaderComponent(snapshotData: any) { + const viewContainerRef = this.tabsHeaderComponentAnchor.viewContainerRef; + if (this.tabsHeaderComponentRef) { + this.tabsHeaderComponentRef.destroy(); + this.tabsHeaderComponentRef = null; + viewContainerRef.clear(); + } + const tabsHeaderComponentType: Type = snapshotData.routerTabsHeaderComponent; + if (tabsHeaderComponentType) { + this.tabsHeaderComponentRef = viewContainerRef.createComponent(tabsHeaderComponentType, {index: 0}); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html index fd99e1bc54..e282d62f49 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html @@ -1,6 +1,6 @@ - {{ labelText | translate }} + @@ -54,4 +55,7 @@ {{ 'rulechain.rulechain-required' | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts index 3db424012f..f371f4201f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ /// import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; @@ -43,18 +43,19 @@ import { RuleChainType } from '@app/shared/models/rule-chain.models'; }) export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnInit { - selectRuleChainFormGroup: FormGroup; - - ruleChainLabel = 'rulechain.rulechain'; + selectRuleChainFormGroup: UntypedFormGroup; modelValue: string | null; @Input() - labelText: string; + labelText: string = 'rulechain.rulechain'; @Input() requiredText: string; + @Input() + ruleChainType: RuleChainType = RuleChainType.CORE; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -83,7 +84,7 @@ export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnI public truncate: TruncatePipe, private entityService: EntityService, private ruleChainService: RuleChainService, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { this.selectRuleChainFormGroup = this.fb.group({ ruleChainId: [null] }); @@ -191,9 +192,8 @@ export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnI fetchRuleChain(searchText?: string): Observable>> { this.searchText = searchText; - // @voba: at the moment device profiles are not supported by edge, so 'core' hardcoded return this.entityService.getEntitiesByNameFilter(EntityType.RULE_CHAIN, searchText, - 50, RuleChainType.CORE, {ignoreLoading: true}).pipe( + 50, this.ruleChainType, {ignoreLoading: true}).pipe( catchError(() => of([])) ); } diff --git a/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts index 4052958e86..865e7cf224 100644 --- a/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@app/shared/shared.module'; import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component'; import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; +import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component'; +import { AlarmCommentDialogComponent } from '@home/components/alarm/alarm-comment-dialog.component'; +import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.component'; @NgModule({ providers: [ @@ -26,14 +29,20 @@ import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; ], declarations: [ - AlarmDetailsDialogComponent + AlarmDetailsDialogComponent, + AlarmCommentComponent, + AlarmCommentDialogComponent, + AlarmAssigneeComponent ], imports: [ CommonModule, SharedModule ], exports: [ - AlarmDetailsDialogComponent + AlarmDetailsDialogComponent, + AlarmCommentComponent, + AlarmCommentDialogComponent, + AlarmAssigneeComponent ] }) export class SharedHomeComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html index e72cd7f077..8b29a5fbdc 100644 --- a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html @@ -1,6 +1,6 @@
- + admin.smpp-provider.smpp-version @@ -32,7 +32,7 @@ {{'admin.smpp-provider.smpp-host-required' | translate}} - + admin.smpp-provider.smpp-port diff --git a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts index 605af2cb51..f4e305ef1c 100644 --- a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { AwsSnsSmsProviderConfiguration, BindTypes, @@ -46,7 +46,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; }) export class SmppSmsProviderConfigurationComponent implements ControlValueAccessor, OnInit{ - constructor(private fb: FormBuilder) { + constructor(private fb: UntypedFormBuilder) { } private requiredValue: boolean; @@ -61,7 +61,7 @@ export class SmppSmsProviderConfigurationComponent implements ControlValueAcces @Input() disabled: boolean; - smppSmsProviderConfigurationFormGroup: FormGroup; + smppSmsProviderConfigurationFormGroup: UntypedFormGroup; smppVersions = smppVersions; diff --git a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html index f4d9d0d1c9..77b22386a1 100644 --- a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html @@ -1,6 +1,6 @@ - + admin.number-from diff --git a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts index 556828c138..4627089cd3 100644 --- a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; @@ -39,7 +39,7 @@ import { }) export class TwilioSmsProviderConfigurationComponent implements ControlValueAccessor, OnInit { - twilioSmsProviderConfigurationFormGroup: FormGroup; + twilioSmsProviderConfigurationFormGroup: UntypedFormGroup; phoneNumberPatternTwilio = phoneNumberPatternTwilio; @@ -60,7 +60,7 @@ export class TwilioSmsProviderConfigurationComponent implements ControlValueAcce private propagateChange = (v: any) => { }; constructor(private store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { } registerOnChange(fn: any): void { diff --git a/ui-ngx/src/app/modules/home/components/tokens.ts b/ui-ngx/src/app/modules/home/components/tokens.ts index 62b580432f..abba6a0636 100644 --- a/ui-ngx/src/app/modules/home/components/tokens.ts +++ b/ui-ngx/src/app/modules/home/components/tokens.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -23,6 +23,9 @@ export const SHARED_HOME_COMPONENTS_MODULE_TOKEN: InjectionToken> = export const HOME_COMPONENTS_MODULE_TOKEN: InjectionToken> = new InjectionToken>('HOME_COMPONENTS_MODULE_TOKEN'); +export const WIDGET_COMPONENTS_MODULE_TOKEN: InjectionToken> = + new InjectionToken>('WIDGET_COMPONENTS_MODULE_TOKEN'); + export const COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN: InjectionToken> = new InjectionToken>('COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN'); diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html index 8336eb728d..b619e9d846 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html @@ -1,6 +1,6 @@
- - -
- admin.auto-commit-settings - -
-
-
+ + + + admin.auto-commit-settings + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss index b8bb1ee760..36617d9ab9 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ :host { - mat-card.auto-commit-settings { + .mat-mdc-card.auto-commit-settings { margin: 8px; .mat-divider { position: relative; diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts index 980d89db4f..d94cec9457 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; -import { AbstractControl, FormArray, FormBuilder, FormGroup, FormGroupDirective, Validators } from '@angular/forms'; +import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, FormGroupDirective, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AdminService } from '@core/http/admin.service'; @@ -36,7 +36,7 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; }) export class AutoCommitSettingsComponent extends PageComponent implements OnInit { - autoCommitSettingsForm: FormGroup; + autoCommitSettingsForm: UntypedFormGroup; settings: AutoCommitSettings = null; entityTypes = EntityType; @@ -48,7 +48,7 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit private dialogService: DialogService, private sanitizer: DomSanitizer, private translate: TranslateService, - public fb: FormBuilder) { + public fb: UntypedFormBuilder) { super(store); } @@ -76,8 +76,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit this.isReadOnly = this.adminService.getRepositorySettingsInfo().pipe(map(settings => settings.readOnly)); } - entityTypesFormGroupArray(): FormGroup[] { - return (this.autoCommitSettingsForm.get('entityTypes') as FormArray).controls as FormGroup[]; + entityTypesFormGroupArray(): UntypedFormGroup[] { + return (this.autoCommitSettingsForm.get('entityTypes') as UntypedFormArray).controls as UntypedFormGroup[]; } entityTypesFormGroupExpanded(entityTypeControl: AbstractControl): boolean { @@ -89,17 +89,17 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit } public removeEntityType(index: number) { - (this.autoCommitSettingsForm.get('entityTypes') as FormArray).removeAt(index); + (this.autoCommitSettingsForm.get('entityTypes') as UntypedFormArray).removeAt(index); this.autoCommitSettingsForm.markAsDirty(); } public addEnabled(): boolean { - const entityTypesArray = this.autoCommitSettingsForm.get('entityTypes') as FormArray; + const entityTypesArray = this.autoCommitSettingsForm.get('entityTypes') as UntypedFormArray; return entityTypesArray.length < exportableEntityTypes.length; } public addEntityType() { - const entityTypesArray = this.autoCommitSettingsForm.get('entityTypes') as FormArray; + const entityTypesArray = this.autoCommitSettingsForm.get('entityTypes') as UntypedFormArray; const config: AutoVersionCreateConfig = { branch: null, saveAttributes: true, @@ -119,7 +119,7 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit } public removeAll() { - const entityTypesArray = this.autoCommitSettingsForm.get('entityTypes') as FormArray; + const entityTypesArray = this.autoCommitSettingsForm.get('entityTypes') as UntypedFormArray; entityTypesArray.clear(); this.autoCommitSettingsForm.updateValueAndValidity(); this.autoCommitSettingsForm.markAsDirty(); @@ -187,7 +187,7 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit }); } - private prepareEntityTypesFormArray(settings: AutoCommitSettings | null): FormArray { + private prepareEntityTypesFormArray(settings: AutoCommitSettings | null): UntypedFormArray { const entityTypesControls: Array = []; if (settings) { for (const entityType of Object.keys(settings)) { diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html index 317d6e9cb6..d19ed2b51d 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html @@ -1,6 +1,6 @@ -

{{ 'version-control.create-entities-version' | translate }}

@@ -70,7 +69,7 @@
-
+
- +
diff --git a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss index 158f7ba5f4..80df7d9d79 100644 --- a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss +++ b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,9 @@ * limitations under the License. */ :host { - mat-card.repository-settings { + .mat-mdc-card.repository-settings { margin: 8px; } - mat-checkbox { - padding-bottom: 16px; - } .fields-group { padding: 0 16px 8px; margin-bottom: 10px; @@ -37,8 +34,3 @@ } } } -:host ::ng-deep { - .mat-checkbox-layout { - white-space: normal !important; - } -} diff --git a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts index 1bb1487f0a..b42bca156d 100644 --- a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; -import { FormBuilder, FormGroup, FormGroupDirective, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, FormGroupDirective, Validators } from '@angular/forms'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AdminService } from '@core/http/admin.service'; @@ -48,7 +48,7 @@ export class RepositorySettingsComponent extends PageComponent implements OnInit @Input() popoverComponent: TbPopoverComponent; - repositorySettingsForm: FormGroup; + repositorySettingsForm: UntypedFormGroup; settings: RepositorySettings = null; repositoryAuthMethod = RepositoryAuthMethod; @@ -66,7 +66,7 @@ export class RepositorySettingsComponent extends PageComponent implements OnInit private dialogService: DialogService, private translate: TranslateService, private cd: ChangeDetectorRef, - public fb: FormBuilder) { + public fb: UntypedFormBuilder) { super(store); } @@ -75,6 +75,7 @@ export class RepositorySettingsComponent extends PageComponent implements OnInit repositoryUri: [null, [Validators.required]], defaultBranch: ['main', []], readOnly: [false, []], + showMergeCommits: [false, []], authMethod: [RepositoryAuthMethod.USERNAME_PASSWORD, [Validators.required]], username: [null, []], password: [null, []], diff --git a/ui-ngx/src/app/modules/home/components/vc/version-control.component.html b/ui-ngx/src/app/modules/home/components/vc/version-control.component.html index eaf80fd664..7b14dde065 100644 --- a/ui-ngx/src/app/modules/home/components/vc/version-control.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/version-control.component.html @@ -1,6 +1,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.scss index 611394ee12..4eebb52161 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,10 +33,10 @@ box-sizing: border-box; } - mat-tab-group { - .mat-tab-body-wrapper { + .mat-mdc-tab-group { + .mat-mdc-tab-body-wrapper { height: 100%; - mat-tab-body { + .mat-mdc-tab-body { height: 100%; & > div { height: 100%; @@ -81,20 +81,20 @@ &.tb-fullscreen-editor { position: relative; right: 0; - .mat-button { + /* .mat-mdc-button { .mat-icon { margin-right: 5px; } - } + } */ } - .mat-button { + /* .mat-mdc-button { min-width: 36px; padding: 0; .mat-icon { margin-right: 0; } - } + } */ } .tb-custom-action-editor { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts index 7c36347f2a..5c2111c76e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ /// limitations under the License. /// -// tslint:disable-next-line:no-reference +// eslint-disable-next-line @typescript-eslint/triple-slash-reference /// import { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html index 63dc06c1d4..7014570235 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html @@ -1,6 +1,6 @@ - + diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw index 96dc54d998..30e25a0a5b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw @@ -136,9 +136,9 @@ // vm.relations().markAsDirty(); // }; // -// vm.removeOldRelation = function(index) { +// vm.removeOldRelation = function(index, relation) { // vm.oldRelations().removeAt(index); -// vm.relationsToDelete.push(vm.oldRelationsData[index]); +// vm.relationsToDelete.push(relation); // vm.oldRelations().markAsDirty(); // }; // diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html new file mode 100644 index 0000000000..36f58f27c6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html @@ -0,0 +1,52 @@ + + + +

{{ data.widgetTitle }}: {{ 'widget-config.actions' | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts new file mode 100644 index 0000000000..0133c375f2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { widgetType } from '@shared/models/widget.models'; +import { + WidgetActionCallbacks, + WidgetActionsData +} from '@home/components/widget/action/manage-widget-actions.component.models'; +import { Component, Inject, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; + +export interface ManageWidgetActionsDialogData { + widgetTitle: string; + actionsData: WidgetActionsData; + callbacks: WidgetActionCallbacks; + widgetType: widgetType; +} + +@Component({ + selector: 'tb-manage-widget-actions-dialog', + templateUrl: './manage-widget-actions-dialog.component.html', + providers: [], + styleUrls: [] +}) +export class ManageWidgetActionsDialogComponent extends DialogComponent implements OnInit { + + actionSources = this.data.actionsData.actionSources; + actionsSettings: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ManageWidgetActionsDialogData, + private fb: UntypedFormBuilder, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + } + + ngOnInit() { + this.actionsSettings = this.fb.group({ + actions: [this.data.actionsData.actionsMap, []] + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.actionsSettings.get('actions').value); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html index 0941a20d00..1c29a715b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html @@ -1,6 +1,6 @@ -
-
- +
+
+
widget-config.actions @@ -36,7 +36,7 @@
- +
+ +
+ + + + + + + + notifications + + + timeline + + + + + +
+
+ entity.no-keys-found +
+ + + {{ translate.get('entity.no-key-matching', + {key: truncate.transform(keySearchText, true, 6, '...')}) | async }} + + + entity.create-new-key + + + {{'entity.create-new-key' | translate }} + notifications + + + timeline + + +
+
+
+ + + + +
+ + +
+
+ + +
+
+ + + +
+
+ + + f() + + + + + + + + {{ modelValue?.aggregationType }}({{ modelValue?.name }}) + + + {{modelValue?.name}} + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss new file mode 100644 index 0000000000..5b3d4f1a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-data-key-row { + height: 38px; + display: flex; + flex-direction: row; + gap: 12px; + padding-left: 12px; + + .mat-mdc-form-field.tb-inline-field.tb-key-field { + .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { + .mat-mdc-form-field-infix { + padding-top: 0; + padding-bottom: 6px; + .mdc-evolution-chip-set .mdc-evolution-chip { + margin: 0; + } + input.mat-mdc-chip-input { + height: 32px; + margin-left: 0; + } + } + } + .mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { + .tb-attribute-chip { + .tb-chip-labels { + background: transparent; + } + } + } + } + + .tb-color-field, .tb-units-field, .tb-decimals-field { + width: 60px; + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts new file mode 100644 index 0000000000..d434ef74e3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts @@ -0,0 +1,426 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; +import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AggregationType } from '@shared/models/time/time.models'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { MatChipGrid, MatChipInputEvent } from '@angular/material/chips'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { Observable, of } from 'rxjs'; +import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/config/data-key-config-dialog.component'; +import { deepClone } from '@core/utils'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { IAliasController } from '@core/api/widget-api.models'; + +export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors | null => { + const dataKey: DataKey = control.value; + if (!dataKey || !dataKey.type || !dataKey.name) { + return { + dataKey: true + }; + } + return null; +}; + +@Component({ + selector: 'tb-data-key-row', + templateUrl: './data-key-row.component.html', + styleUrls: ['./data-key-row.component.scss', '../../data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { + + dataKeyTypes = DataKeyType; + widgetTypes = widgetType; + + separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + @ViewChild('keyInput') keyInput: ElementRef; + @ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; + @ViewChild('chipList') chipList: MatChipGrid; + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + @Input() + entityAliasId: string; + + @Input() + deviceId: string; + + keyFormControl: UntypedFormControl; + + keyRowFormGroup: UntypedFormGroup; + + modelValue: DataKey; + + filteredKeys: Observable>; + + keySearchText = ''; + + private latestKeySearchTextResult: Array = null; + private keyFetchObservable$: Observable> = null; + + get dataKeyType(): DataKeyType { + return this.dataKeysPanelComponent.dataKeyType; + } + + get alarmKeys(): Array { + return this.dataKeysPanelComponent.alarmKeys; + } + + get functionTypeKeys(): Array { + return this.dataKeysPanelComponent.functionTypeKeys; + } + + get hideDataKeyColor(): boolean { + return this.dataKeysPanelComponent.hideDataKeyColor; + } + + get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get widget(): Widget { + return this.widgetConfigComponent.widget; + } + + get dashboard(): Dashboard { + return this.widgetConfigComponent.dashboard; + } + + get aliasController(): IAliasController { + return this.widgetConfigComponent.aliasController; + } + + get datakeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + get dataKeySettingsDirective(): string { + return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective; + } + + get isEntityDatasource(): boolean { + return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); + } + + get displayUnitsOrDigits() { + return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type); + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + public translate: TranslateService, + public truncate: TruncatePipe, + private dataKeysPanelComponent: DataKeysPanelComponent, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keyFormControl = this.fb.control(''); + this.keyRowFormGroup = this.fb.group({ + label: [null, []], + color: [null, []], + units: [null, []], + decimals: [null, []], + }); + this.keyRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.filteredKeys = this.keyFormControl.valueChanges + .pipe( + tap((value: string | DataKey) => { + if (value && typeof value !== 'string') { + this.addKeyFromChipValue(value); + } else if (value === null) { + this.clearKeyChip(this.keyInput.nativeElement.value); + } + }), + filter((value) => typeof value === 'string'), + map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchKeys(name) ), + share() + ); + } + + private reset() { + if (this.keyInput) { + this.keyInput.nativeElement.value = ''; + } + this.keyFormControl.patchValue('', {emitEvent: false}); + this.latestKeySearchTextResult = null; + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['deviceId', 'entityAliasId'].includes(propName)) { + this.clearKeySearchCache(); + } else if (['datasourceType'].includes(propName)) { + if ([DatasourceType.device, DatasourceType.entity].includes(change.previousValue) && + [DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) { + this.clearKeySearchCache(); + } else { + this.clearKeySearchCache(); + setTimeout(() => { + this.reset(); + }, 1); + } + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keyRowFormGroup.disable({emitEvent: false}); + } else { + this.keyRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey): void { + this.modelValue = value || {} as DataKey; + this.keyRowFormGroup.patchValue( + { + label: value?.label, + color: value?.color, + units: value?.units, + decimals: value?.decimals + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + dataKeyHasAggregation(): boolean { + return this.widgetConfigComponent.widgetType === widgetType.latest && this.modelValue?.type === DataKeyType.timeseries + && this.modelValue?.aggregationType && this.modelValue?.aggregationType !== AggregationType.NONE; + } + + dataKeyHasPostprocessing(): boolean { + return !!this.modelValue?.postFuncBody; + } + + displayKeyFn(key?: DataKey): string | undefined { + return key ? key.name : undefined; + } + + createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) { + this.addKeyFromChipValue({name: name ? name.trim() : '', type: dataKeyType}); + } + + addKey(event: MatChipInputEvent): void { + const value = event.value; + if ((value || '').trim() && this.dataKeyType) { + this.addKeyFromChipValue({name: value.trim(), type: this.dataKeyType}); + } else { + this.clearKeyChip(); + } + } + + editKey() { + this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(this.modelValue), + dataKeySettingsSchema: this.datakeySettingsSchema, + dataKeySettingsDirective: this.dataKeySettingsDirective, + dashboard: this.dashboard, + aliasController: this.aliasController, + widget: this.widget, + widgetType: this.widgetType, + deviceId: this.deviceId, + entityAliasId: this.entityAliasId, + showPostProcessing: this.widgetType !== widgetType.alarm, + callbacks: this.callbacks, + hideDataKeyLabel: false, + hideDataKeyColor: this.hideDataKeyColor, + hideDataKeyUnits: !this.displayUnitsOrDigits, + hideDataKeyDecimals: !this.displayUnitsOrDigits + } + }).afterClosed().subscribe((updatedDataKey) => { + if (updatedDataKey) { + this.modelValue = updatedDataKey; + this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); + this.keyRowFormGroup.get('color').patchValue(this.modelValue.color, {emitEvent: false}); + this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false}); + this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false}); + this.updateModel(); + } + }); + } + + removeKey() { + this.modelValue = {} as DataKey; + this.updateModel(); + this.clearKeyChip(); + } + + textIsNotEmpty(text: string): boolean { + return text && text.length > 0; + } + + clearKeyChip(value: string = '', focus = true) { + this.autocomplete.closePanel(); + this.keyInput.nativeElement.value = value; + this.keyFormControl.patchValue(value, {emitEvent: focus}); + if (focus) { + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + } + + onKeyInputFocus() { + if (!this.modelValue.type) { + this.keyFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true}); + } + } + + private fetchKeys(searchText?: string): Observable> { + if (this.keySearchText !== searchText || this.latestKeySearchTextResult === null) { + this.keySearchText = searchText; + const dataKeyFilter = this.createDataKeyFilter(this.keySearchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestKeySearchTextResult = res) + ); + } + return of(this.latestKeySearchTextResult); + } + + private getKeys(): Observable> { + if (this.keyFetchObservable$ === null) { + let fetchObservable: Observable>; + if (this.datasourceType === DatasourceType.function) { + const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; + fetchObservable = of(targetKeysList); + } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId || + this.datasourceType === DatasourceType.device && this.deviceId) { + const dataKeyTypes = [DataKeyType.timeseries]; + if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.attribute); + dataKeyTypes.push(DataKeyType.entityField); + if (this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.alarm); + } + } + if (this.datasourceType === DatasourceType.device) { + fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes); + } else { + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + } + } else { + fetchObservable = of([]); + } + this.keyFetchObservable$ = fetchObservable.pipe( + publishReplay(1), + refCount() + ); + } + return this.keyFetchObservable$; + } + + private createDataKeyFilter(query: string): (key: DataKey) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.name.toLowerCase().startsWith(lowercaseQuery); + } + + private addKeyFromChipValue(chip: DataKey) { + this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema); + if (!this.keyRowFormGroup.get('label').value) { + this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); + } + this.updateModel(); + this.clearKeyChip('', false); + } + + private clearKeySearchCache() { + this.keySearchText = ''; + this.keyFetchObservable$ = null; + this.latestKeySearchTextResult = null; + } + + private updateModel() { + const value: DataKey = this.keyRowFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html new file mode 100644 index 0000000000..754f0052c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html @@ -0,0 +1,67 @@ + +
+
{{ panelTitle }}
+
+
+
datakey.key
+
datakey.label
+
datakey.color
+
widget-config.units-short
+
widget-config.decimals-short
+
+
+
+
+ + +
+ + +
+
+
+
+
+ +
+
+ + {{ noKeysText }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss new file mode 100644 index 0000000000..df8b187535 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-data-keys-table { + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + display: flex; + flex-direction: column; + gap: 12px; + padding-bottom: 12px; + .tb-data-keys-header { + height: 48px; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; + gap: 12px; + padding-left: 12px; + .tb-data-keys-header-cell { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.54); + &.tb-color-header, &.tb-units-header, &.tb-decimals-header { + width: 60px; + } + &.tb-actions-header { + width: 76px; + } + } + } + .tb-data-keys-body { + display: flex; + flex-direction: column; + gap: 12px; + } + .tb-prompt { + height: 38px; + } +} + +.tb-data-keys-table-row { + height: 38px; + display: flex; + flex-direction: row; + gap: 12px; + background: #fff; + + .tb-data-keys-table-row-buttons { + display: flex; + flex-direction: row; + button.mat-mdc-icon-button.mat-mdc-button-base { + padding: 7px; + width: 38px; + height: 38px; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts new file mode 100644 index 0000000000..3d8b80c1df --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts @@ -0,0 +1,237 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models'; +import { dataKeyRowValidator } from '@home/components/widget/config/basic/common/data-key-row.component'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { alarmFields } from '@shared/models/alarm.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-data-keys-panel', + templateUrl: './data-keys-panel.component.html', + styleUrls: ['./data-keys-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeysPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataKeysPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges, Validator { + + @Input() + disabled: boolean; + + @Input() + panelTitle: string; + + @Input() + addKeyTitle: string; + + @Input() + removeKeyTitle: string; + + @Input() + noKeysText: string; + + @Input() + datasourceType: DatasourceType; + + @Input() + entityAliasId: string; + + @Input() + deviceId: string; + + @Input() + @coerceBoolean() + hideDataKeyColor = false; + + dataKeyType: DataKeyType; + alarmKeys: Array; + functionTypeKeys: Array; + + keysListFormGroup: UntypedFormGroup; + + get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get datakeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + private utils: UtilsService, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keysListFormGroup = this.fb.group({ + keys: [this.fb.array([]), []] + }); + this.keysListFormGroup.valueChanges.subscribe( + (val) => this.propagateChange(this.keysListFormGroup.get('keys').value) + ); + this.alarmKeys = []; + for (const name of Object.keys(alarmFields)) { + this.alarmKeys.push({ + name, + type: DataKeyType.alarm + }); + } + this.functionTypeKeys = []; + for (const type of this.utils.getPredefinedFunctionsList()) { + this.functionTypeKeys.push({ + name: type, + type: DataKeyType.function + }); + } + this.updateParams(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['datasourceType'].includes(propName)) { + this.updateParams(); + } + } + } + } + + private updateParams() { + if (this.datasourceType === DatasourceType.function) { + this.dataKeyType = DataKeyType.function; + } else { + if (this.widgetType !== widgetType.latest && this.widgetType !== widgetType.alarm) { + this.dataKeyType = DataKeyType.timeseries; + } else { + this.dataKeyType = null; + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey[] | undefined): void { + this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + return this.keysListFormGroup.valid ? null : { + dataKeyRows: { + valid: false, + }, + }; + } + + keyDrop(event: CdkDragDrop) { + const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; + const key = keysArray.at(event.previousIndex); + keysArray.removeAt(event.previousIndex); + keysArray.insert(event.currentIndex, key); + } + + keysFormArray(): UntypedFormArray { + return this.keysListFormGroup.get('keys') as UntypedFormArray; + } + + trackByKey(index: number, keyControl: AbstractControl): any { + return keyControl; + } + + removeKey(index: number) { + (this.keysListFormGroup.get('keys') as UntypedFormArray).removeAt(index); + } + + addKey() { + const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema); + dataKey.label = ''; + dataKey.decimals = 0; + const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; + const keyControl = this.fb.control(dataKey, [dataKeyRowValidator]); + keysArray.push(keyControl); + } + + private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray { + const keysControls: Array = []; + if (keys) { + keys.forEach((key) => { + keysControls.push(this.fb.control(key, [dataKeyRowValidator])); + }); + } + return this.fb.array(keysControls); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html new file mode 100644 index 0000000000..95e71efd5e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html @@ -0,0 +1,36 @@ + +
+
+
widget-config.actions
+ + + + {{ widgetAction.icon }} + {{ widgetAction.name }} + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts new file mode 100644 index 0000000000..469ff191ed --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { WidgetActionsData } from '@home/components/widget/action/manage-widget-actions.component.models'; +import { WidgetActionDescriptor } from '@shared/models/widget.models'; +import { + ManageWidgetActionsDialogComponent, + ManageWidgetActionsDialogData +} from '@home/components/widget/action/manage-widget-actions-dialog.component'; +import { deepClone } from '@core/utils'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'tb-widget-actions-panel', + templateUrl: './widget-actions-panel.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetActionsPanelComponent), + multi: true + } + ] +}) +export class WidgetActionsPanelComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + actionsFormGroup: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.actionsFormGroup = this.fb.group({ + actions: [null, []] + }); + this.actionsFormGroup.get('actions').valueChanges.subscribe( + (val) => this.propagateChange(val) + ); + } + + writeValue(actions?: {[actionSourceId: string]: Array}): void { + this.actionsFormGroup.get('actions').patchValue(actions || {}, {emitEvent: false}); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.actionsFormGroup.disable({emitEvent: false}); + } else { + this.actionsFormGroup.enable({emitEvent: false}); + } + } + + public get widgetActionSourceIds(): Array { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + return actions ? Object.keys(actions) : []; + } + + public widgetActionsByActionSourceId(actionSourceId: string): Array { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + return actions[actionSourceId] || []; + } + + public get hasWidgetActions(): boolean { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + if (actions) { + for (const actionSourceId of Object.keys(actions)) { + if (actions[actionSourceId] && actions[actionSourceId].length) { + return true; + } + } + } + return false; + } + + public manageWidgetActions() { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + const actionsData: WidgetActionsData = { + actionsMap: deepClone(actions), + actionSources: this.widgetConfigComponent.modelValue.actionSources || {} + }; + this.dialog.open}>(ManageWidgetActionsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + widgetTitle: this.widgetConfigComponent.modelValue.widgetName, + callbacks: this.widgetConfigComponent.widgetConfigCallbacks, + actionsData, + widgetType: this.widgetConfigComponent.widgetType + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.actionsFormGroup.get('actions').patchValue(res); + this.cd.markForCheck(); + } + } + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html new file mode 100644 index 0000000000..354008393e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html @@ -0,0 +1,63 @@ + + + +

{{ 'datakey.configuration' | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.scss new file mode 100644 index 0000000000..5d9067f719 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-mdc-dialog-content { + padding: 0 !important; + margin: 0 !important; + overflow: hidden; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts new file mode 100644 index 0000000000..d2bdd2f8a1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; +import { DataKeysCallbacks } from './data-keys.component.models'; +import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { IAliasController } from '@core/api/widget-api.models'; + +export interface DataKeyConfigDialogData { + dataKey: DataKey; + dataKeySettingsSchema: any; + dataKeySettingsDirective: string; + dashboard: Dashboard; + aliasController: IAliasController; + widget: Widget; + widgetType: widgetType; + deviceId?: string; + entityAliasId?: string; + showPostProcessing?: boolean; + callbacks?: DataKeysCallbacks; + hideDataKeyLabel: boolean; + hideDataKeyColor: boolean; + hideDataKeyUnits: boolean; + hideDataKeyDecimals: boolean; +} + +@Component({ + selector: 'tb-data-key-config-dialog', + templateUrl: './data-key-config-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DataKeyConfigDialogComponent}], + styleUrls: ['./data-key-config-dialog.component.scss'] +}) +export class DataKeyConfigDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + @ViewChild('dataKeyConfig', {static: true}) dataKeyConfig: DataKeyConfigComponent; + + dataKeyFormGroup: UntypedFormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DataKeyConfigDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.dataKeyFormGroup = this.fb.group({ + dataKey: [this.data.dataKey, [Validators.required]] + }); + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.dataKeyConfig.validateOnSubmit(); + if (this.dataKeyFormGroup.valid) { + const dataKey: DataKey = this.dataKeyFormGroup.get('dataKey').value; + this.dialogRef.close(dataKey); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html new file mode 100644 index 0000000000..daf70c19e4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html @@ -0,0 +1,177 @@ + + + +
+ + {{ 'entity.key' | translate }} + + + + + + + + +
+ + datakey.label + + + + +
+
+ + datakey.units + + + + datakey.decimals + + +
+
+ + datakey.aggregation + + + {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} + + + +
+ {{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }} +
+ {{ 'datakey.aggregation-type-hint-common' | translate }} +
+
+
+ datakey.delta-calculation + + + + + {{ 'datakey.enable-delta-calculation' | translate }} + + {{ 'datakey.enable-delta-calculation-hint' | translate }} + + + +
+ + widgets.chart.time-for-comparison + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + + + widgets.chart.custom-interval-value + + + + datakey.delta-calculation-result + + + {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} + + + +
+
+
+
+
+
+ datakey.data-generation-func +
+ + +
+
+ + {{ 'datakey.use-data-post-processing-func' | translate }} + + + + +
+
+
+ +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss new file mode 100644 index 0000000000..2ef3488e53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../scss/constants'; + +:host { + .tb-datakey-config { + height: 100%; + + .settings{ + &> :first-child{ + padding-top: 12px; + } + &> :nth-child(n){ + padding-right: 12px; + padding-left: 12px; + } + } + + .fields-group { + padding: 0 16px 8px; + margin-bottom: 10px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + legend { + color: rgba(0, 0, 0, .7); + width: fit-content; + } + + legend + * { + display: block; + margin-top: 16px; + } + + &.fields-group-slider { + padding: 0; + + legend { + margin-left: 16px; + } + + > .tb-settings { + margin-top: 0; + padding: 0 16px 8px; + } + } + } + + .tb-hint.after-fields { + margin-top: -1.25em; + max-width: fit-content; + line-height: 15px; + } + } +} + +:host ::ng-deep { + .tb-datakey-config { + .mat-mdc-tab-body.mat-mdc-tab-body-active { + .mat-mdc-tab-body-content > div { + height: calc(65vh - 50px); + overflow: initial; + @media #{$mat-xs} { + height: 500px; + } + } + } + .mat-expansion-panel { + &.tb-settings { + box-shadow: none; + + .mat-content { + overflow: visible; + } + + .mat-expansion-panel-header { + padding: 0; + color: rgba(0, 0, 0, 0.87); + + &:hover { + background: none; + } + + .mat-expansion-indicator { + padding: 2px; + } + } + + &.comparison { + .mat-expansion-panel-header { + height: fit-content; + } + } + + .mat-expansion-panel-header-description { + align-items: center; + } + + > .mat-expansion-panel-content { + > .mat-expansion-panel-body { + padding: 0; + } + } + } + + .mat-expansion-panel-content { + font: inherit; + } + } + + .mat-slide { + margin: 8px 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts new file mode 100644 index 0000000000..a7cf923c44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts @@ -0,0 +1,461 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + ComparisonResultType, + comparisonResultTypeTranslationMap, + DataKey, + dataKeyAggregationTypeHintTranslationMap, + Widget, + widgetType +} from '@shared/models/widget.models'; +import { + ControlValueAccessor, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityService } from '@core/http/entity.service'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; +import { alarmFields } from '@shared/models/alarm.models'; +import { JsFuncComponent } from '@shared/components/js-func.component'; +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; +import { WidgetService } from '@core/http/widget.service'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models'; +import { genNextLabel } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-data-key-config', + templateUrl: './data-key-config.component.html', + styleUrls: ['./data-key-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeyConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataKeyConfigComponent), + multi: true, + } + ] +}) +export class DataKeyConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + dataKeyTypes = DataKeyType; + + widgetTypes = widgetType; + + aggregations = [AggregationType.NONE, ...Object.keys(AggregationType).filter(type => type !== AggregationType.NONE)]; + + aggregationTypes = AggregationType; + + aggregationTypesTranslations = aggregationTranslations; + + dataKeyAggregationTypeHintTranslations = dataKeyAggregationTypeHintTranslationMap; + + comparisonResultTypes = ComparisonResultType; + + comparisonResults = Object.keys(ComparisonResultType); + + comparisonResultTypeTranslations = comparisonResultTypeTranslationMap; + + @Input() + deviceId: string; + + @Input() + entityAliasId: string; + + @Input() + callbacks: DataKeysCallbacks; + + @Input() + dashboard: Dashboard; + + @Input() + aliasController: IAliasController; + + @Input() + widget: Widget; + + @Input() + widgetType: widgetType; + + @Input() + dataKeySettingsSchema: any; + + @Input() + dataKeySettingsDirective: string; + + @Input() + showPostProcessing = true; + + @Input() + @coerceBoolean() + hideDataKeyLabel = false; + + @Input() + @coerceBoolean() + hideDataKeyColor = false; + + @Input() + @coerceBoolean() + hideDataKeyUnits = false; + + @Input() + @coerceBoolean() + hideDataKeyDecimals = false; + + @ViewChild('keyInput') keyInput: ElementRef; + + @ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent; + @ViewChild('postFuncBodyEdit') postFuncBodyEdit: JsFuncComponent; + + displayAdvanced = false; + + modelValue: DataKey; + + private propagateChange = null; + + public dataKeyFormGroup: UntypedFormGroup; + + public dataKeySettingsFormGroup: UntypedFormGroup; + + private dataKeySettingsData: JsonFormComponentData; + + private alarmKeys: Array; + private functionTypeKeys: Array; + + filteredKeys: Observable>; + private latestKeySearchResult: Array = null; + private fetchObservable$: Observable> = null; + + keySearchText = ''; + + functionScopeVariables: string[]; + + constructor(protected store: Store, + private utils: UtilsService, + private entityService: EntityService, + private dialog: MatDialog, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: UntypedFormBuilder) { + super(store); + this.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + } + + ngOnInit(): void { + this.alarmKeys = []; + for (const name of Object.keys(alarmFields)) { + this.alarmKeys.push({ + name, + type: DataKeyType.alarm + }); + } + this.functionTypeKeys = []; + for (const type of this.utils.getPredefinedFunctionsList()) { + this.functionTypeKeys.push({ + name: type, + type: DataKeyType.function + }); + } + if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema || + this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) { + this.displayAdvanced = true; + this.dataKeySettingsData = { + schema: this.dataKeySettingsSchema?.schema || { + type: 'object', + properties: {} + }, + form: this.dataKeySettingsSchema?.form || ['*'], + settingsDirective: this.dataKeySettingsDirective + }; + this.dataKeySettingsFormGroup = this.fb.group({ + settings: [null, []] + }); + this.dataKeySettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + this.dataKeyFormGroup = this.fb.group({ + name: [null, []], + aggregationType: [null, []], + comparisonEnabled: [null, []], + timeForComparison: [null, [Validators.required]], + comparisonCustomIntervalValue: [null, [Validators.required, Validators.min(1000)]], + comparisonResultType: [null, [Validators.required]], + label: [null, [Validators.required]], + color: [null, [Validators.required]], + units: [null, []], + decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], + funcBody: [null, []], + usePostProcessing: [null, []], + postFuncBody: [null, []] + }); + + this.dataKeyFormGroup.get('aggregationType').valueChanges.subscribe( + (aggType) => { + if (!this.dataKeyFormGroup.get('label').dirty) { + let newLabel = this.dataKeyFormGroup.get('name').value; + if (aggType !== AggregationType.NONE) { + const prefix = this.translate.instant(aggregationTranslations.get(aggType)); + newLabel = genNextLabel(prefix + ' ' + newLabel, this.widget.config.datasources); + } + this.dataKeyFormGroup.get('label').patchValue(newLabel); + } + this.updateComparisonValidators(); + } + ); + + this.dataKeyFormGroup.get('comparisonEnabled').valueChanges.subscribe( + () => { + this.updateComparisonValues(); + } + ); + + this.dataKeyFormGroup.get('timeForComparison').valueChanges.subscribe( + () => { + this.updateComparisonValues(); + } + ); + + this.dataKeyFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + + this.dataKeyFormGroup.get('usePostProcessing').valueChanges.subscribe((usePostProcessing: boolean) => { + const postFuncBody: string = this.dataKeyFormGroup.get('postFuncBody').value; + if (usePostProcessing && (!postFuncBody || !postFuncBody.length)) { + this.dataKeyFormGroup.get('postFuncBody').patchValue('return value;'); + } else if (!usePostProcessing && postFuncBody && postFuncBody.length) { + this.dataKeyFormGroup.get('postFuncBody').patchValue(null); + } + }); + + this.filteredKeys = this.dataKeyFormGroup.get('name').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchKeys(name) ) + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + } + + writeValue(value: DataKey): void { + this.modelValue = value; + if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) { + this.modelValue.usePostProcessing = true; + } + if (this.widgetType === widgetType.latest && this.modelValue.type === DataKeyType.timeseries && !this.modelValue.aggregationType) { + this.modelValue.aggregationType = AggregationType.NONE; + } + this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); + this.updateValidators(); + if (this.displayAdvanced) { + this.dataKeySettingsData.model = this.modelValue.settings; + this.dataKeySettingsFormGroup.patchValue({ + settings: this.dataKeySettingsData + }, {emitEvent: false}); + } + } + + private updateValidators() { + this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function && + this.modelValue.type !== DataKeyType.count + ? [Validators.required] : []); + if (this.modelValue.type === DataKeyType.count) { + this.dataKeyFormGroup.get('name').disable({emitEvent: false}); + } else { + this.dataKeyFormGroup.get('name').enable({emitEvent: false}); + } + this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); + this.updateComparisonValidators(); + } + + private updateComparisonValues() { + const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value; + if (comparisonEnabled) { + const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value; + if (!timeForComparison) { + this.dataKeyFormGroup.get('timeForComparison').patchValue('previousInterval', {emitEvent: false}); + } else if (timeForComparison === 'customInterval') { + const comparisonCustomIntervalValue = this.dataKeyFormGroup.get('comparisonCustomIntervalValue').value; + if (!comparisonCustomIntervalValue) { + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').patchValue(7200000, {emitEvent: false}); + } + } + const comparisonResultType: ComparisonResultType = this.dataKeyFormGroup.get('comparisonResultType').value; + if (!comparisonResultType) { + this.dataKeyFormGroup.get('comparisonResultType').patchValue(ComparisonResultType.DELTA_ABSOLUTE, {emitEvent: false}); + } + } + this.updateComparisonValidators(); + } + + private updateComparisonValidators() { + const aggregationType: AggregationType = this.dataKeyFormGroup.get('aggregationType').value; + if (aggregationType && aggregationType !== AggregationType.NONE) { + this.dataKeyFormGroup.get('comparisonEnabled').enable({emitEvent: false}); + const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value; + if (comparisonEnabled) { + this.dataKeyFormGroup.get('timeForComparison').enable({emitEvent: false}); + const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value; + if (timeForComparison) { + this.dataKeyFormGroup.get('comparisonResultType').enable({emitEvent: false}); + if (timeForComparison === 'customInterval') { + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').enable({emitEvent: false}); + } else { + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); + } + } else { + this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); + } + } else { + this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); + } + } else { + this.dataKeyFormGroup.get('comparisonEnabled').disable({emitEvent: false}); + this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); + } + this.dataKeyFormGroup.get('comparisonEnabled').updateValueAndValidity({emitEvent: false}); + this.dataKeyFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonResultType').updateValueAndValidity({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false}); + } + + private updateModel() { + this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; + if (this.displayAdvanced) { + this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model; + } + if (this.modelValue.name) { + this.modelValue.name = this.modelValue.name.trim(); + } + this.propagateChange(this.modelValue); + } + + clearKey() { + this.dataKeyFormGroup.get('name').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + + private fetchKeys(searchText?: string): Observable> { + if (this.keySearchText !== searchText || this.latestKeySearchResult === null) { + this.keySearchText = searchText; + const dataKeyFilter = this.createKeyFilter(this.keySearchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestKeySearchResult = res) + ); + } + return of(this.latestKeySearchResult); + } + + private getKeys() { + if (this.fetchObservable$ === null) { + let fetchObservable: Observable>; + if (this.modelValue.type === DataKeyType.alarm) { + fetchObservable = of(this.alarmKeys); + } else if (this.modelValue.type === DataKeyType.function) { + fetchObservable = of(this.functionTypeKeys); + } else { + if (this.deviceId || this.entityAliasId) { + const dataKeyTypes = [this.modelValue.type]; + if (this.deviceId) { + fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes); + } else { + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + } + } else { + fetchObservable = of([]); + } + } + this.fetchObservable$ = fetchObservable.pipe( + map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), + publishReplay(1), + refCount() + ); + } + return this.fetchObservable$; + } + + private createKeyFilter(query: string): (key: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.toLowerCase().startsWith(lowercaseQuery); + } + + public validateOnSubmit() { + if (this.modelValue.type === DataKeyType.function) { + this.funcBodyEdit.validateOnSubmit(); + } else if ((this.modelValue.type === DataKeyType.timeseries || + this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value) { + this.postFuncBodyEdit.validateOnSubmit(); + } + } + + public validate(c: UntypedFormControl) { + if (!this.dataKeyFormGroup.valid) { + return { + dataKey: { + valid: false + } + }; + } + if (this.displayAdvanced && (!this.dataKeySettingsFormGroup.valid || !this.modelValue.settings)) { + return { + dataKeySettings: { + valid: false + } + }; + } + return null; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html new file mode 100644 index 0000000000..39fffd79e7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html @@ -0,0 +1,190 @@ + + + {{placeholder}} + +
+
+
+ +
+
+
+ drag_indicator +
+
+
+ + notifications + + + timeline + + {{key.label}} +
+
:
+
+ + + +
+
+
+
+
+
+
+ + +
+
+ +
+
+ + + + + notifications + + + timeline + + + + + +
+
+ entity.no-keys-found +
+ + + {{ translate.get('entity.no-key-matching', + {key: truncate.transform(searchText, true, 6, '...')}) | async }} + + + entity.create-new-key + + + {{'entity.create-new-key' | translate }} + notifications + + + timeline + + +
+
+
+ + {{ requiredText }} + +
+
+ {{ maxDataKeysText() }} +
+ + + f() + + + + + + + + {{ key.aggregationType }}({{ key.name }}) + + + {{key.name}} + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts new file mode 100644 index 0000000000..a5519be953 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts @@ -0,0 +1,25 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { DataKey, JsonSettingsSchema } from '@shared/models/widget.models'; +import { Observable } from 'rxjs'; + +export interface DataKeysCallbacks { + generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey; + fetchEntityKeys: (entityAliasId: string, types: Array) => Observable>; + fetchEntityKeysForDevice: (deviceId: string, types: Array) => Observable>; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss new file mode 100644 index 0000000000..7d01f41cb5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss @@ -0,0 +1,124 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-datakeys-container { + display: flex; + flex-wrap: wrap; + width: 100%; + + input.tb-dragging { + display: none; + } +} + +.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { + overflow: hidden; + line-height: 20px; + height: 32px; + + &.mdc-evolution-chip--with-trailing-action { + .mdc-evolution-chip__action--primary { + padding-left: 4px; + padding-right: 12px; + } + } + + .mat-mdc-chip-action { + overflow: hidden; + .mat-mdc-chip-action-label { + overflow: hidden; + } + } + .tb-attribute-chip { + max-width: 100%; + color: rgb(66, 66, 66); + .tb-chip-drag-handle { + padding: 3px; + height: 24px; + cursor: move; + mat-icon { + pointer-events: none; + } + } + .tb-chip-labels { + display: flex; + flex-direction: row; + align-items: center; + min-width: 0; + background: rgba(0, 0, 0, 0.04); + border-radius: 100px; + padding: 2px 10px; + .tb-chip-label { + font-weight: normal; + font-size: 14px; + line-height: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + .mat-icon.tb-datakey-icon { + margin-right: 4px; + margin-left: 4px; + } + .tb-agg-func { + font-style: italic; + color: #0c959c; + } + } + .tb-chip-separator { + white-space: pre; + } + } + .mat-mdc-chip-remove.mat-mdc-icon-button { + color: inherit; + opacity: inherit; + } + } + + &.tb-datakey-chip-dnd-placeholder { + min-width: 120px; + border: 2px dashed rgba(0, 0, 0, 0.2); + } + &.tb-chip-dragging { + display: none; + } + .tb-dragging-chip-image-fill { + background-color: rgba(0,0,0,0.3); + border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); + display: none; + pointer-events: none; + } + .tb-dragging-chip-image { + background-color: var(--mdc-chip-elevated-container-color, transparent); + border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); + overflow: hidden; + height: 32px; + line-height: 20px; + .tb-dragging-chip-image-fill { + display: block; + } + } +} + +.mat-icon.tb-datakey-icon { + vertical-align: middle; + & > svg { + vertical-align: initial; + } + &.new-key { + margin-left: 8px; + margin-right: 8px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts new file mode 100644 index 0000000000..95fe5bc444 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts @@ -0,0 +1,624 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + Renderer2, + SimpleChanges, + SkipSelf, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormGroupDirective, + NG_VALUE_ACCESSOR, + NgForm, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors +} from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { MatChipGrid, MatChipInputEvent, MatChipRow } from '@angular/material/chips'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { DataKeysCallbacks } from './data-keys.component.models'; +import { alarmFields } from '@shared/models/alarm.models'; +import { UtilsService } from '@core/services/utils.service'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { DialogService } from '@core/services/dialog.service'; +import { MatDialog } from '@angular/material/dialog'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/config/data-key-config-dialog.component'; +import { deepClone, guid, isDefinedAndNotNull, isUndefined } from '@core/utils'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { AggregationType } from '@shared/models/time/time.models'; +import { DndDropEvent } from 'ngx-drag-drop/lib/dnd-dropzone.directive'; +import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { DatasourceComponent } from '@home/components/widget/config/datasource.component'; + +@Component({ + selector: 'tb-data-keys', + templateUrl: './data-keys.component.html', + styleUrls: ['./data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeysComponent), + multi: true + } /*, + { + provide: ErrorStateMatcher, + useExisting: DataKeysComponent + } */ + ], + encapsulation: ViewEncapsulation.None +}) +export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChanges, ErrorStateMatcher { + + public get hideDataKeyLabel(): boolean { + return this.datasourceComponent.hideDataKeyLabel; + } + + public get hideDataKeyColor(): boolean { + return this.datasourceComponent.hideDataKeyColor; + } + + public get hideDataKeyUnits(): boolean { + return this.datasourceComponent.hideDataKeyUnits; + } + + public get hideDataKeyDecimals(): boolean { + return this.datasourceComponent.hideDataKeyDecimals; + } + + datasourceTypes = DatasourceType; + widgetTypes = widgetType; + dataKeyTypes = DataKeyType; + + keysListFormGroup: UntypedFormGroup; + + modelValue: Array | null; + + @Input() + widgetType: widgetType; + + @Input() + datasourceType: DatasourceType; + + private maxDataKeysValue: number; + get maxDataKeys(): number { + return this.isCountDatasource ? 1 : this.maxDataKeysValue; + } + + @Input() + set maxDataKeys(value: number) { + this.maxDataKeysValue = value; + } + + @Input() + optDataKeys: boolean; + + @Input() + @coerceBoolean() + simpleDataKeysLabel = false; + + @Input() + aliasController: IAliasController; + + @Input() + datakeySettingsSchema: JsonSettingsSchema; + + @Input() + dataKeySettingsDirective: string; + + @Input() + dashboard: Dashboard; + + @Input() + widget: Widget; + + @Input() + callbacks: DataKeysCallbacks; + + @Input() + entityAliasId: string; + + @Input() + deviceId: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue || !this.optDataKeys || this.isCountDatasource; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('keyInput') keyInput: ElementRef; + @ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; + @ViewChild('chipList') chipList: MatChipGrid; + + keys: Array = []; + filteredKeys: Observable>; + + separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + alarmKeys: Array; + functionTypeKeys: Array; + + dataKeyType: DataKeyType; + placeholder: string; + secondaryPlaceholder: string; + requiredText: string; + + searchText = ''; + + dndId = guid(); + + dragIndex: number; + + private latestSearchTextResult: Array = null; + private fetchObservable$: Observable> = null; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private datasourceComponent: DatasourceComponent, + public translate: TranslateService, + private utils: UtilsService, + private dialogs: DialogService, + private dialog: MatDialog, + private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + public truncate: TruncatePipe) { + } + + updateValidators() { + this.keysListFormGroup.get('keys').setValidators(this.required ? [this.keysRequired] : []); + this.keysListFormGroup.get('keys').updateValueAndValidity(); + } + + keysRequired(control: AbstractControl): ValidationErrors | null { + const value = control.value; + if (value && Array.isArray(value) && value.length) { + return null; + } else { + return {required: true}; + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.keysListFormGroup = this.fb.group({ + keys: [null, this.required ? [this.keysRequired] : []], + key: [null] + }); + this.alarmKeys = []; + for (const name of Object.keys(alarmFields)) { + this.alarmKeys.push({ + name, + type: DataKeyType.alarm + }); + } + this.functionTypeKeys = []; + for (const type of this.utils.getPredefinedFunctionsList()) { + this.functionTypeKeys.push({ + name: type, + type: DataKeyType.function + }); + } + this.updateParams(); + this.filteredKeys = this.keysListFormGroup.get('key').valueChanges + .pipe( + tap((value: string | DataKey) => { + if (value && typeof value !== 'string') { + this.addFromChipValue(value); + } else if (value === null) { + this.clear(this.keyInput.nativeElement.value); + } + }), + filter((value) => typeof value === 'string'), + map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchKeys(name) ), + share() + ); + } + + public maxDataKeysText(): string { + if (this.maxDataKeys !== null && this.maxDataKeys > -1) { + if (this.datasourceType === DatasourceType.function) { + return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys}); + } else if (!this.isCountDatasource) { + return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys}); + } else { + return ''; + } + } else { + return ''; + } + } + + private updateParams() { + const singleKey = this.maxDataKeysSet && this.maxDataKeys === 1; + this.secondaryPlaceholder = '+' + this.translate.instant('action.add'); + if (this.datasourceType === DatasourceType.function) { + this.dataKeyType = DataKeyType.function; + this.requiredText = this.translate.instant('datakey.function-types-required'); + if (this.widgetType === widgetType.latest) { + this.placeholder = this.translate.instant(singleKey ? 'datakey.latest-key-function' : 'datakey.latest-key-functions'); + } else if (this.widgetType === widgetType.alarm) { + this.placeholder = this.translate.instant(singleKey ? 'datakey.alarm-key-function' : 'datakey.alarm-key-functions'); + } else { + this.placeholder = this.translate.instant(singleKey ? 'datakey.timeseries-key-function' : 'datakey.timeseries-key-functions'); + } + } else { + if (this.widgetType !== widgetType.latest && this.widgetType !== widgetType.alarm) { + this.dataKeyType = DataKeyType.timeseries; + } else { + this.dataKeyType = null; + } + if (this.simpleDataKeysLabel && this.widgetType !== widgetType.alarm) { + this.placeholder = this.translate.instant(singleKey ? 'datakey.data-key' : 'datakey.data-keys'); + this.requiredText = this.translate.instant(singleKey ? 'datakey.data-key-required' : 'datakey.data-keys-required'); + } else { + if (this.widgetType === widgetType.latest) { + this.placeholder = this.translate.instant(singleKey ? 'datakey.latest-key' : 'datakey.latest-keys'); + this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required'); + } else if (this.widgetType === widgetType.alarm) { + this.placeholder = this.translate.instant(singleKey ? 'datakey.alarm-key' : 'datakey.alarm-keys'); + this.requiredText = this.translate.instant('datakey.alarm-fields-timeseries-or-attributes-required'); + } else { + this.placeholder = this.translate.instant(singleKey ? 'datakey.timeseries-key' : 'datakey.timeseries-keys'); + this.requiredText = this.translate.instant('datakey.timeseries-required'); + } + } + } + } + + private reset() { + if (this.widgetType === widgetType.alarm) { + this.keys = this.utils.getDefaultAlarmDataKeys(); + } else if (this.isCountDatasource) { + this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema)]; + } else { + this.keys = []; + } + this.keysListFormGroup.get('keys').setValue(this.keys); + this.modelValue = this.keys.length ? [...this.keys] : null; + if (this.keyInput) { + this.keyInput.nativeElement.value = ''; + } + this.keysListFormGroup.get('key').patchValue('', {emitEvent: false}); + this.propagateChange(this.modelValue); + this.dirty = true; + this.latestSearchTextResult = null; + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['deviceId', 'entityAliasId'].includes(propName)) { + this.clearSearchCache(); + this.dirty = true; + } else if (['widgetType', 'datasourceType', 'maxDataKeys', 'simpleDataKeysLabel'].includes(propName)) { + if (propName === 'datasourceType' && + [DatasourceType.device, DatasourceType.entity].includes(change.previousValue) && + [DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) { + this.clearSearchCache(); + } else { + this.clearSearchCache(); + this.updateParams(); + setTimeout(() => { + this.reset(); + }, 1); + } + } else if (['required', 'optDataKeys'].includes(propName)) { + this.updateValidators(); + } + } + } + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = this.required && (!this.modelValue || !this.modelValue.length); + return originalErrorState || customErrorState; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Array | null): void { + this.searchText = ''; + if (value != null && value.length) { + this.modelValue = [...value]; + this.keys = [...value]; + this.keysListFormGroup.get('keys').setValue(this.keys); + } else { + this.keys = []; + this.keysListFormGroup.get('keys').setValue(this.keys); + this.modelValue = null; + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.keysListFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + private addFromChipValue(chip: DataKey) { + const key = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema); + this.addKey(key); + } + + addKey(key: DataKey): void { + if (!this.maxDataKeysSet || + !this.modelValue || this.modelValue.length < this.maxDataKeys) { + if (!this.modelValue) { + this.modelValue = []; + } + this.modelValue.push(key); + this.keys.push(key); + this.keysListFormGroup.get('keys').setValue(this.keys); + } + this.propagateChange(this.modelValue); + const focus = !this.maxDataKeysSet || this.modelValue.length < this.maxDataKeys; + this.clear('', focus); + } + + add(event: MatChipInputEvent): void { + const value = event.value; + if ((value || '').trim() && this.dataKeyType) { + this.addFromChipValue({name: value.trim(), type: this.dataKeyType}); + } else { + this.clear(); + } + } + + remove(key: DataKey) { + const index = this.keys.indexOf(key); + if (index >= 0) { + this.keys.splice(index, 1); + this.keysListFormGroup.get('keys').setValue(this.keys); + this.modelValue.splice(index, 1); + if (!this.modelValue.length) { + this.modelValue = null; + } + this.propagateChange(this.modelValue); + this.clear(); + } + } + + chipDragStart(index: number, chipRow: MatChipRow, placeholderChipRow: Element) { + this.autocomplete.closePanel(); + this.renderer.setStyle(placeholderChipRow, + 'width', chipRow._elementRef.nativeElement.offsetWidth + 'px'); + this.dragIndex = index; + } + + chipDragEnd() { + this.dragIndex = -1; + } + + onChipDrop(event: DndDropEvent) { + let index = event.index; + if (isUndefined(index)) { + index = this.keys.length; + } + moveItemInArray(this.keys, this.dragIndex, index); + this.keysListFormGroup.get('keys').setValue(this.keys); + moveItemInArray(this.modelValue, this.dragIndex, index); + this.dragIndex = -1; + this.propagateChange(this.modelValue); + } + + showColorPicker(key: DataKey) { + this.dialogs.colorPicker(key.color).subscribe( + (color) => { + if (color && key.color !== color) { + key.color = color; + this.propagateChange(this.modelValue); + } + } + ); + } + + editDataKey(key: DataKey, index: number) { + this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(key), + dataKeySettingsSchema: this.datakeySettingsSchema, + dataKeySettingsDirective: this.dataKeySettingsDirective, + dashboard: this.dashboard, + aliasController: this.aliasController, + widget: this.widget, + widgetType: this.widgetType, + deviceId: this.deviceId, + entityAliasId: this.entityAliasId, + showPostProcessing: this.widgetType !== widgetType.alarm, + callbacks: this.callbacks, + hideDataKeyLabel: this.hideDataKeyLabel, + hideDataKeyColor: this.hideDataKeyColor, + hideDataKeyUnits: this.hideDataKeyUnits, + hideDataKeyDecimals: this.hideDataKeyDecimals + } + }).afterClosed().subscribe((updatedDataKey) => { + if (updatedDataKey) { + this.keys[index] = updatedDataKey; + this.keysListFormGroup.get('keys').setValue(this.keys); + this.modelValue[index] = updatedDataKey; + this.propagateChange(this.modelValue); + } + }); + } + + createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) { + this.addFromChipValue({name: name ? name.trim() : '', type: dataKeyType}); + } + + displayKeyFn(key?: DataKey): string | undefined { + return key ? key.name : undefined; + } + + dataKeyHasAggregation(key: DataKey): boolean { + return this.widgetType === widgetType.latest && key.type === DataKeyType.timeseries + && key.aggregationType && key.aggregationType !== AggregationType.NONE; + } + + dataKeyHasPostprocessing(key: DataKey): boolean { + return this.datasourceType !== DatasourceType.function && !!key.postFuncBody; + } + + private fetchKeys(searchText?: string): Observable> { + if (this.searchText !== searchText || this.latestSearchTextResult === null) { + this.searchText = searchText; + const dataKeyFilter = this.createDataKeyFilter(this.searchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestSearchTextResult = res) + ); + } + return of(this.latestSearchTextResult); + } + + private getKeys(): Observable> { + if (this.fetchObservable$ === null) { + let fetchObservable: Observable>; + if (this.datasourceType === DatasourceType.function) { + const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; + fetchObservable = of(targetKeysList); + } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId || + this.datasourceType === DatasourceType.device && this.deviceId) { + const dataKeyTypes = [DataKeyType.timeseries]; + if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.attribute); + dataKeyTypes.push(DataKeyType.entityField); + if (this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.alarm); + } + } + if (this.datasourceType === DatasourceType.device) { + fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes); + } else { + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + } + } else { + fetchObservable = of([]); + } + this.fetchObservable$ = fetchObservable.pipe( + publishReplay(1), + refCount() + ); + } + return this.fetchObservable$; + } + + private createDataKeyFilter(query: string): (key: DataKey) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.name.toLowerCase().startsWith(lowercaseQuery); + } + + textIsNotEmpty(text: string): boolean { + return text && text.length > 0; + } + + clear(value: string = '', focus = true) { + this.autocomplete.closePanel(); + this.keyInput.nativeElement.value = value; + this.keysListFormGroup.get('key').patchValue(value, {emitEvent: focus}); + if (focus) { + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + } + + get isCountDatasource(): boolean { + return [DatasourceType.entityCount, DatasourceType.alarmCount].includes(this.datasourceType); + } + + get isEntityDatasource(): boolean { + return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); + } + + get inputDisabled(): boolean { + return this.isCountDatasource || (this.maxDataKeysSet && this.keys.length >= this.maxDataKeys); + } + + get dragDisabled(): boolean { + return this.keys.length < 2; + } + + get maxDataKeysSet(): boolean { + return isDefinedAndNotNull(this.maxDataKeysValue) && this.maxDataKeysValue > -1; + } + + private clearSearchCache() { + this.searchText = ''; + this.fetchObservable$ = null; + this.latestSearchTextResult = null; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html new file mode 100644 index 0000000000..ab7411488e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -0,0 +1,103 @@ + +
+ + widget-config.datasource-type + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + + +
+ + + datasource.label + + + + + + + + + + + + + +
+
+ + + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts new file mode 100644 index 0000000000..0664058fb4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts @@ -0,0 +1,21 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; +import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; + +export type DatasourceCallbacks = EntityAliasSelectCallbacks & FilterSelectCallbacks & DataKeysCallbacks; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.scss new file mode 100644 index 0000000000..163e74d336 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.scss @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-datasource-section { + display: flex; + flex-direction: column; + align-items: stretch; + flex: 1; +} + +:host ::ng-deep { + .tb-datasource-section { + tb-alarm-filter-config { + .mdc-button { + width: 100%; + height: 100%; + justify-content: flex-start; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts new file mode 100644 index 0000000000..b1f203baaa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts @@ -0,0 +1,279 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit, Optional } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + Validators +} from '@angular/forms'; +import { + Datasource, + DatasourceType, + datasourceTypeTranslationMap, + JsonSettingsSchema, + Widget, WidgetConfigMode, + widgetType +} from '@shared/models/widget.models'; +import { AlarmSearchStatus } from '@shared/models/alarm.models'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { IAliasController } from '@core/api/widget-api.models'; +import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; +import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DatasourcesComponent } from '@home/components/widget/config/datasources.component'; + +@Component({ + selector: 'tb-datasource', + templateUrl: './datasource.component.html', + styleUrls: ['./datasource.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatasourceComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DatasourceComponent), + multi: true, + } + ] +}) +export class DatasourceComponent implements ControlValueAccessor, OnInit, Validator { + + public get basicMode(): boolean { + return !this.widgetConfigComponent.widgetEditMode && this.widgetConfigComponent.widgetConfigMode === WidgetConfigMode.basic; + } + + public get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + public get aliasController(): IAliasController { + return this.widgetConfigComponent.aliasController; + } + + public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + public get filterSelectCallbacks(): FilterSelectCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + public get dataKeysCallbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + public get hasAdditionalLatestDataKeys(): boolean { + return this.widgetConfigComponent.widgetType === widgetType.timeseries && + this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys; + } + + public get dataKeysOptional(): boolean { + return this.widgetConfigComponent.modelValue?.typeParameters?.dataKeysOptional; + } + + public get maxDataKeys(): number { + return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys; + } + + public get dataKeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + public get dataKeySettingsDirective(): string { + return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective; + } + + public get latestDataKeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema; + } + + public get latestDataKeySettingsDirective(): string { + return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective; + } + + public get dashboard(): Dashboard { + return this.widgetConfigComponent.dashboard; + } + + public get widget(): Widget { + return this.widgetConfigComponent.widget; + } + + public get hideDataKeyLabel(): boolean { + return this.datasourcesComponent?.hideDataKeyLabel; + } + + public get hideDataKeyColor(): boolean { + return this.datasourcesComponent?.hideDataKeyColor; + } + + public get hideDataKeyUnits(): boolean { + return this.datasourcesComponent?.hideDataKeyUnits; + } + + public get hideDataKeyDecimals(): boolean { + return this.datasourcesComponent?.hideDataKeyDecimals; + } + + public get hideDataKeys(): boolean { + return this.datasourcesComponent?.hideDataKeys; + } + + @Input() + disabled: boolean; + + widgetTypes = widgetType; + + entityType = EntityType; + + datasourceType = DatasourceType; + datasourceTypes: Array = []; + datasourceTypesTranslations = datasourceTypeTranslationMap; + + datasourceFormGroup: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + @Optional() + private datasourcesComponent: DatasourcesComponent, + private widgetConfigComponent: WidgetConfigComponent) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (!this.datasourceFormGroup.valid) { + setTimeout(() => { + this.datasourceUpdated(this.datasourceFormGroup.value); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.datasourceFormGroup.disable({emitEvent: false}); + } else { + this.datasourceFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + ngOnInit() { + if (this.widgetConfigComponent.functionsOnly) { + this.datasourceTypes = [DatasourceType.function]; + } else { + this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; + if (this.widgetConfigComponent.widgetType === widgetType.latest) { + this.datasourceTypes.push(DatasourceType.entityCount); + this.datasourceTypes.push(DatasourceType.alarmCount); + } + } + + this.datasourceFormGroup = this.fb.group( + { + type: [null, [Validators.required]], + name: [null, []], + deviceId: [null, []], + entityAliasId: [null, []], + filterId: [null, []], + dataKeys: [null, []], + alarmFilterConfig: [null, []] + } + ); + if (this.hasAdditionalLatestDataKeys) { + this.datasourceFormGroup.addControl('latestDataKeys', this.fb.control(null)); + } + this.datasourceFormGroup.get('type').valueChanges.subscribe(() => { + this.updateValidators(); + }); + this.datasourceFormGroup.valueChanges.subscribe( + () => { + this.datasourceUpdated(this.datasourceFormGroup.value); + } + ); + } + + writeValue(datasource?: Datasource): void { + this.datasourceFormGroup.patchValue({ + type: datasource?.type, + name: datasource?.name, + deviceId: datasource?.deviceId, + entityAliasId: datasource?.entityAliasId, + filterId: datasource?.filterId, + dataKeys: datasource?.dataKeys, + alarmFilterConfig: datasource?.alarmFilterConfig ? + datasource?.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE] } + }, {emitEvent: false}); + if (this.hasAdditionalLatestDataKeys) { + this.datasourceFormGroup.patchValue({ + latestDataKeys: datasource?.latestDataKeys + }, {emitEvent: false}); + } + this.updateValidators(); + } + + validate(c: UntypedFormControl) { + return (this.datasourceFormGroup.valid) ? null : { + datasource: { + valid: false, + }, + }; + } + + public isDataKeysOptional(type?: DatasourceType): boolean { + if (this.hasAdditionalLatestDataKeys) { + return true; + } else { + return this.dataKeysOptional + && type !== DatasourceType.entityCount && type !== DatasourceType.alarmCount; + } + } + + private datasourceUpdated(datasource: Datasource) { + this.propagateChange(datasource); + } + + private updateValidators() { + const type: DatasourceType = this.datasourceFormGroup.get('type').value; + this.datasourceFormGroup.get('deviceId').setValidators( + type === DatasourceType.device ? [Validators.required] : [] + ); + this.datasourceFormGroup.get('entityAliasId').setValidators( + (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] + ); + const newDataKeysRequired = !this.isDataKeysOptional(type); + this.datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []); + this.datasourceFormGroup.get('deviceId').updateValueAndValidity({emitEvent: false}); + this.datasourceFormGroup.get('entityAliasId').updateValueAndValidity({emitEvent: false}); + this.datasourceFormGroup.get('dataKeys').updateValueAndValidity({emitEvent: false}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html new file mode 100644 index 0000000000..a5a8a89004 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html @@ -0,0 +1,101 @@ + +
+
+
+
{{ (singleDatasource ? 'widget-config.datasource' : 'widget-config.datasources') | translate }}
+
+ {{ 'widget-config.timeseries-key-error' | translate }} +
+ + +
+
{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}
+
+
+ datasource.add-datasource-prompt +
+ +
+ + +
+
+
{{$index + 1}}
+
+
+
+ + +
+ + +
+
+ +
+
+
+
+
+
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.scss new file mode 100644 index 0000000000..5b356afcf2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.scss @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../theme"; + +.tb-datasource-list-item { + &.mat-mdc-list-item { + height: auto; + display: block; + padding: 0; + &.bordered { + padding-top: 16px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + } + } + &.tb-draggable { + &.cdk-drag-preview { + background: #fff; + } + } +} + +.tb-datasource-list-item + .tb-datasource-list-item { + margin-top: 16px; +} + +.tb-datasource-index { + width: 24px; + height: 24px; + position: relative; + font-weight: 400; + font-size: 16px; + text-align: center; + color: $tb-primary-color; + &:before { + content: ""; + display: block; + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + background-color: $tb-primary-color; + opacity: 0.06; + border-radius: 100%; + } +} + +.tb-datasource-params { + position: relative; + tb-error.tb-datasource-error { + position: absolute; + bottom: 4px; + left: 8px; + } +} + +:host { + .tb-datasources { + + .handle { + cursor: move; + } + + .mat-mdc-list { + min-height: 68px; + padding-left: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts new file mode 100644 index 0000000000..ae674a4d55 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts @@ -0,0 +1,333 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormControl, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { + Datasource, + DatasourceType, + JsonSettingsSchema, + WidgetConfigMode, + widgetType +} from '@shared/models/widget.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { deepClone } from '@core/utils'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-datasources', + templateUrl: './datasources.component.html', + styleUrls: ['./datasources.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatasourcesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DatasourcesComponent), + multi: true, + } + ] +}) +export class DatasourcesComponent implements ControlValueAccessor, OnInit, Validator, OnChanges { + + datasourceType = DatasourceType; + + public get basicMode(): boolean { + return !this.widgetConfigComponent.widgetEditMode && this.configMode === WidgetConfigMode.basic; + } + + public get maxDatasources(): number { + return this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources; + } + + public get singleDatasource(): boolean { + return this.maxDatasources === 1; + } + + public get showAddDatasource(): boolean { + return this.widgetConfigComponent.modelValue?.typeParameters && + (this.maxDatasources === -1 || this.datasourcesFormArray.length < this.maxDatasources); + } + + public get dragDisabled(): boolean { + return this.disabled || this.singleDatasource || this.datasourcesFormArray.length < 2; + } + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + hideDataKeyLabel = false; + + @Input() + @coerceBoolean() + hideDataKeyColor = false; + + @Input() + @coerceBoolean() + hideDataKeyUnits = false; + + @Input() + @coerceBoolean() + hideDataKeyDecimals = false; + + @Input() + @coerceBoolean() + hideDataKeys = false; + + @Input() + configMode: WidgetConfigMode; + + datasourcesFormGroup: UntypedFormGroup; + + timeseriesKeyError = false; + + datasourceError: string[] = []; + + datasourcesMode: DatasourceType; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private utils: UtilsService, + public translate: TranslateService, + private widgetConfigComponent: WidgetConfigComponent) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.validate(null)) { + setTimeout(() => { + this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.datasourcesFormGroup.disable({emitEvent: false}); + } else { + this.datasourcesFormGroup.enable({emitEvent: false}); + } + } + + ngOnInit() { + this.datasourcesFormGroup = this.fb.group({ + datasources: this.fb.array([]) + }); + this.datasourcesFormGroup.valueChanges.subscribe( + () => { + this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); + } + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'configMode') { + this.configModeChanged(); + } + } + } + } + + writeValue(datasources?: Datasource[]): void { + this.datasourcesFormArray.clear({emitEvent: false}); + this.datasourcesMode = this.detectDatasourcesMode(datasources); + let changed = false; + if (datasources) { + datasources.forEach((datasource) => { + if (this.basicMode && datasource.type !== this.datasourcesMode) { + datasource.type = this.datasourcesMode; + changed = true; + } + this.datasourcesFormArray.push(this.fb.control(datasource, []), {emitEvent: false}); + }); + } + if (this.singleDatasource && !this.datasourcesFormArray.length) { + this.addDatasource(false); + } + if (changed) { + setTimeout(() => { + this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); + }, 0); + } + } + + validate(c: UntypedFormControl) { + this.timeseriesKeyError = false; + this.datasourceError = []; + if (!this.datasourcesFormGroup.valid) { + return { + datasources: { + valid: false, + } + }; + } + const datasources: Datasource[] = this.datasourcesFormGroup.get('datasources').value; + if (!this.datasourcesOptional && (!datasources || !datasources.length)) { + return { + datasources: { + valid: false + } + }; + } + if (this.hasAdditionalLatestDataKeys) { + let valid = datasources.filter(datasource => datasource?.dataKeys?.length).length > 0; + if (!valid) { + this.timeseriesKeyError = true; + return { + timeseriesDataKeys: { + valid: false + } + }; + } else { + const emptyDatasources = datasources.filter(datasource => !datasource?.dataKeys?.length && + !datasource?.latestDataKeys?.length); + valid = emptyDatasources.length === 0; + if (!valid) { + for (const emptyDatasource of emptyDatasources) { + const i = datasources.indexOf(emptyDatasource); + this.datasourceError[i] = 'At least one data key should be specified'; + } + return { + dataKeys: { + valid: false + } + }; + } + } + } + return null; + } + + datasourcesModeChange(datasourcesMode: DatasourceType) { + this.datasourcesMode = datasourcesMode; + if (this.basicMode) { + for (const datasourceControl of this.datasourcesControls) { + const datasource: Datasource = datasourceControl.value; + if (datasource.type !== datasourcesMode) { + datasource.type = datasourcesMode; + datasourceControl.patchValue(datasource); + } + } + } + } + + private configModeChanged() { + if (this.basicMode) { + let datasourcesMode = this.detectDatasourcesMode(this.datasourcesFormGroup.get('datasources').value); + this.datasourcesModeChange(datasourcesMode); + } + } + + private detectDatasourcesMode(datasources?: Datasource[]) { + let datasourcesMode = DatasourceType.device; + if (datasources && datasources.length) { + datasourcesMode = datasources[0].type; + } + if (datasourcesMode !== DatasourceType.device && datasourcesMode !== DatasourceType.entity) { + datasourcesMode = DatasourceType.device; + } + return datasourcesMode; + } + + get datasourcesFormArray(): UntypedFormArray { + return this.datasourcesFormGroup.get('datasources') as UntypedFormArray; + } + + get datasourcesControls(): FormControl[] { + return this.datasourcesFormArray.controls as FormControl[]; + } + + public trackByDatasource(index: number, datasourceControl: AbstractControl): any { + return datasourceControl; + } + + private datasourcesUpdated(datasources: Datasource[]) { + this.propagateChange(datasources); + } + + public onDatasourceDrop(event: CdkDragDrop) { + const datasourceForm = this.datasourcesFormArray.at(event.previousIndex); + this.datasourcesFormArray.removeAt(event.previousIndex); + this.datasourcesFormArray.insert(event.currentIndex, datasourceForm); + } + + public removeDatasource(index: number) { + this.datasourcesFormArray.removeAt(index); + } + + public addDatasource(emitEvent = true) { + let newDatasource: Datasource; + if (this.widgetConfigComponent.functionsOnly) { + newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema)); + newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema)]; + } else { + const type = this.basicMode ? this.datasourcesMode : DatasourceType.entity; + newDatasource = { type, + dataKeys: [] + }; + } + if (this.hasAdditionalLatestDataKeys) { + newDatasource.latestDataKeys = []; + } + this.datasourcesFormArray.push(this.fb.control(newDatasource, []), {emitEvent}); + } + + private get dataKeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + private get dataKeysCallbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + private get hasAdditionalLatestDataKeys(): boolean { + return this.widgetConfigComponent.widgetType === widgetType.timeseries && + this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys; + } + + private get datasourcesOptional(): boolean { + return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html new file mode 100644 index 0000000000..985db7d942 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html @@ -0,0 +1,42 @@ + +
+
+
timewindow.timewindow
+ + +
+
+ + + {{ 'widget-config.display-timewindow' | translate }} + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts new file mode 100644 index 0000000000..31436c9415 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts @@ -0,0 +1,115 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { widgetType } from '@shared/models/widget.models'; +import { Timewindow } from '@shared/models/time/time.models'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +export interface TimewindowConfigData { + useDashboardTimewindow: boolean; + displayTimewindow: boolean; + timewindow: Timewindow; +} + +@Component({ + selector: 'tb-timewindow-config-panel', + templateUrl: './timewindow-config-panel.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimewindowConfigPanelComponent), + multi: true + } + ] +}) +export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnInit { + + widgetTypes = widgetType; + + public get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + onlyHistoryTimewindow = false; + + + timewindowConfig: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + public translate: TranslateService, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.timewindowConfig = this.fb.group({ + useDashboardTimewindow: [null, []], + displayTimewindow: [null, []], + timewindow: [null, []] + }); + this.timewindowConfig.valueChanges.subscribe( + (val) => this.propagateChange(val) + ); + this.timewindowConfig.get('useDashboardTimewindow').valueChanges.subscribe(() => { + this.updateTimewindowConfigEnabledState(); + }); + } + + writeValue(data?: TimewindowConfigData): void { + this.timewindowConfig.patchValue(data || {}, {emitEvent: false}); + this.updateTimewindowConfigEnabledState(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.timewindowConfig.disable({emitEvent: false}); + } else { + this.timewindowConfig.enable({emitEvent: false}); + this.updateTimewindowConfigEnabledState(); + } + } + + private updateTimewindowConfigEnabledState() { + const useDashboardTimewindow: boolean = this.timewindowConfig.get('useDashboardTimewindow').value; + if (useDashboardTimewindow) { + this.timewindowConfig.get('displayTimewindow').disable({emitEvent: false}); + this.timewindowConfig.get('timewindow').disable({emitEvent: false}); + } else { + this.timewindowConfig.get('displayTimewindow').enable({emitEvent: false}); + this.timewindowConfig.get('timewindow').enable({emitEvent: false}); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts new file mode 100644 index 0000000000..186a89537e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component'; +import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component'; +import { DataKeysComponent } from '@home/components/widget/config/data-keys.component'; +import { DataKeyConfigDialogComponent } from '@home/components/widget/config/data-key-config-dialog.component'; +import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; +import { DatasourceComponent } from '@home/components/widget/config/datasource.component'; +import { DatasourcesComponent } from '@home/components/widget/config/datasources.component'; +import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component'; +import { FilterSelectComponent } from '@home/components/filter/filter-select.component'; +import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; +import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component'; +import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component'; +import { WidgetUnitsComponent } from '@home/components/widget/config/widget-units.component'; + +@NgModule({ + declarations: + [ + AlarmAssigneeSelectComponent, + AlarmFilterConfigComponent, + DataKeysComponent, + DataKeyConfigDialogComponent, + DataKeyConfigComponent, + DatasourceComponent, + DatasourcesComponent, + EntityAliasSelectComponent, + FilterSelectComponent, + TimewindowConfigPanelComponent, + WidgetUnitsComponent, + WidgetSettingsComponent + ], + imports: [ + CommonModule, + SharedModule, + WidgetSettingsModule + ], + exports: [ + AlarmAssigneeSelectComponent, + AlarmFilterConfigComponent, + DataKeysComponent, + DataKeyConfigDialogComponent, + DataKeyConfigComponent, + DatasourceComponent, + DatasourcesComponent, + EntityAliasSelectComponent, + FilterSelectComponent, + TimewindowConfigPanelComponent, + WidgetUnitsComponent, + WidgetSettingsComponent + ] +}) +export class WidgetConfigComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts new file mode 100644 index 0000000000..9bda9345dc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts @@ -0,0 +1,161 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; +import { DatasourceCallbacks } from '@home/components/widget/config/datasource.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { Observable } from 'rxjs'; +import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, UntypedFormGroup } from '@angular/forms'; +import { DataKey, DatasourceType, KeyInfo, WidgetConfigMode } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { isDefinedAndNotNull } from '@core/utils'; + +export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; + +export interface IBasicWidgetConfigComponent { + isAdd: boolean; + widgetConfig: WidgetConfigComponentData; + widgetConfigChanged: Observable; + validateConfig(): boolean; + +} + +@Directive() +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export abstract class BasicWidgetConfigComponent extends PageComponent implements + IBasicWidgetConfigComponent, OnInit, AfterViewInit { + + isAdd = false; + + basicMode = WidgetConfigMode.basic; + + widgetConfigValue: WidgetConfigComponentData; + + set widgetConfig(value: WidgetConfigComponentData) { + this.widgetConfigValue = value; + this.setupConfig(this.widgetConfigValue); + } + + get widgetConfig(): WidgetConfigComponentData { + return this.widgetConfigValue; + } + + widgetConfigChangedEmitter = new EventEmitter(); + widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable(); + + protected constructor(@Inject(Store) protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent) { + super(store); + } + + ngOnInit() {} + + ngAfterViewInit(): void { + setTimeout(() => { + if (!this.validateConfig()) { + this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue())); + } + }, 0); + } + + protected setupConfig(widgetConfig: WidgetConfigComponentData) { + if (this.isAdd) { + this.setupDefaults(widgetConfig); + } + this.onConfigSet(widgetConfig); + this.updateValidators(false); + for (const trigger of this.validatorTriggers()) { + const path = trigger.split('.'); + let control: AbstractControl = this.configForm(); + for (const part of path) { + control = control.get(part); + } + control.valueChanges.subscribe(() => { + this.updateValidators(true, trigger); + }); + } + this.configForm().valueChanges.subscribe(() => { + this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue())); + }); + } + + protected setupDefaults(configData: WidgetConfigComponentData) {} + + protected updateValidators(emitEvent: boolean, trigger?: string) { + } + + protected validatorTriggers(): string[] { + return []; + } + + protected onConfigChanged(widgetConfig: WidgetConfigComponentData) { + this.widgetConfigValue = widgetConfig; + this.widgetConfigChangedEmitter.emit(this.widgetConfigValue); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + return config; + } + + public validateConfig(): boolean { + return this.configForm().valid; + } + + protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[]) { + let datasources = configData.config.datasources; + if (!datasources || !datasources.length) { + datasources = [ + { + type: DatasourceType.device, + dataKeys: [] + } + ]; + configData.config.datasources = datasources; + } + let dataKeys = datasources[0].dataKeys; + if (!dataKeys) { + dataKeys = []; + datasources[0].dataKeys = dataKeys; + } + if (keys && keys.length) { + dataKeys.length = 0; + keys.forEach(key => { + const dataKey = + this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema); + if (key.label) { + dataKey.label = key.label; + } + if (key.units) { + dataKey.units = key.units; + } + if (isDefinedAndNotNull(key.decimals)) { + dataKey.decimals = key.decimals; + } + dataKeys.push(dataKey); + }); + } + } + + protected abstract configForm(): UntypedFormGroup; + + protected abstract onConfigSet(configData: WidgetConfigComponentData); + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html new file mode 100644 index 0000000000..ec14b6e787 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html @@ -0,0 +1,24 @@ + +
+ +
{{definedDirectiveError}}
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.scss new file mode 100644 index 0000000000..a31dd354ce --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-settings-directive-error { + font-size: 13px; + font-weight: 400; + color: rgb(221, 44, 0); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts new file mode 100644 index 0000000000..cc482bfd4e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts @@ -0,0 +1,230 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + ComponentRef, + forwardRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; +import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; +import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models'; +import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { WidgetService } from '@core/http/widget.service'; +import { IAliasController } from '@core/api/widget-api.models'; + +@Component({ + selector: 'tb-widget-settings', + templateUrl: './widget-settings.component.html', + styleUrls: ['./widget-settings.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetSettingsComponent), + multi: true + }] +}) +export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, OnDestroy, AfterViewInit, OnChanges { + + @ViewChild('definedSettingsContent', {read: ViewContainerRef, static: true}) definedSettingsContainer: ViewContainerRef; + + @ViewChild('jsonFormComponent') jsonFormComponent: JsonFormComponent; + + @Input() + disabled: boolean; + + @Input() + aliasController: IAliasController; + + @Input() + dashboard: Dashboard; + + @Input() + widget: Widget; + + private settingsDirective: string; + + definedDirectiveError: string; + + widgetSettingsFormGroup: UntypedFormGroup; + + changeSubscription: Subscription; + + private definedSettingsComponentRef: ComponentRef; + private definedSettingsComponent: IWidgetSettingsComponent; + + private widgetSettingsFormData: JsonFormComponentData; + + private propagateChange = (v: any) => { }; + + constructor(private translate: TranslateService, + private cfr: ComponentFactoryResolver, + private widgetService: WidgetService, + private fb: UntypedFormBuilder) { + this.widgetSettingsFormGroup = this.fb.group({ + settings: [null, Validators.required] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit(): void { + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'widget') { + if (this.definedSettingsComponent) { + this.definedSettingsComponent.widget = this.widget; + } + } + if (propName === 'dashboard') { + if (this.definedSettingsComponent) { + this.definedSettingsComponent.dashboard = this.dashboard; + } + } + if (propName === 'aliasController') { + if (this.definedSettingsComponent) { + this.definedSettingsComponent.aliasController = this.aliasController; + } + } + } + } + } + + ngOnDestroy(): void { + if (this.definedSettingsComponentRef) { + this.definedSettingsComponentRef.destroy(); + } + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.widgetSettingsFormGroup.disable({emitEvent: false}); + } else { + this.widgetSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: JsonFormComponentData): void { + this.widgetSettingsFormData = value; + if (this.changeSubscription) { + this.changeSubscription.unsubscribe(); + this.changeSubscription = null; + } + if (this.settingsDirective !== this.widgetSettingsFormData.settingsDirective) { + this.settingsDirective = this.widgetSettingsFormData.settingsDirective; + this.validateDefinedDirective(); + } + if (this.definedSettingsComponent) { + this.definedSettingsComponent.settings = this.widgetSettingsFormData.model; + this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { + this.updateModel(settings); + }); + } else { + this.widgetSettingsFormGroup.get('settings').patchValue(this.widgetSettingsFormData, {emitEvent: false}); + this.changeSubscription = this.widgetSettingsFormGroup.get('settings').valueChanges.subscribe( + (widgetSettingsFormData: JsonFormComponentData) => { + this.updateModel(widgetSettingsFormData.model); + } + ); + } + } + + useDefinedDirective(): boolean { + return this.settingsDirective && + this.settingsDirective.length && !this.definedDirectiveError; + } + + useJsonForm(): boolean { + return !this.settingsDirective || !this.settingsDirective.length; + } + + private updateModel(settings: WidgetSettings) { + this.widgetSettingsFormData.model = settings; + if (this.definedSettingsComponent || this.widgetSettingsFormGroup.valid) { + this.propagateChange(this.widgetSettingsFormData); + } else { + this.propagateChange(null); + } + } + + private validateDefinedDirective() { + if (this.definedSettingsComponentRef) { + this.definedSettingsComponentRef.destroy(); + this.definedSettingsComponentRef = null; + this.definedSettingsComponent = null; + } + if (this.settingsDirective && this.settingsDirective.length) { + const componentType = widgetSettingsComponentsMap[this.settingsDirective]; + if (!componentType) { + this.definedDirectiveError = this.translate.instant('widget-config.settings-component-not-found', + {selector: this.settingsDirective}); + } else { + if (this.changeSubscription) { + this.changeSubscription.unsubscribe(); + this.changeSubscription = null; + } + this.definedSettingsContainer.clear(); + const factory = this.cfr.resolveComponentFactory(componentType); + this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory); + this.definedSettingsComponent = this.definedSettingsComponentRef.instance; + this.definedSettingsComponent.aliasController = this.aliasController; + this.definedSettingsComponent.dashboard = this.dashboard; + this.definedSettingsComponent.widget = this.widget; + this.definedSettingsComponent.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { + this.updateModel(settings); + }); + } + } + } + + validate() { + if (this.useDefinedDirective()) { + this.definedSettingsComponent.validate(); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html new file mode 100644 index 0000000000..a2ed480388 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html @@ -0,0 +1,20 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts new file mode 100644 index 0000000000..e61511698d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts @@ -0,0 +1,68 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms'; + +@Component({ + selector: 'tb-widget-units', + templateUrl: './widget-units.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetUnitsComponent), + multi: true + } + ] +}) +export class WidgetUnitsComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + unitsFormControl: FormControl; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.unitsFormControl = this.fb.control('', []); + this.unitsFormControl.valueChanges.subscribe(val => this.propagateChange(val)); + } + + writeValue(units?: string): void { + this.unitsFormControl.patchValue(units, {emitEvent: false}); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.unitsFormControl.disable({emitEvent: false}); + } else { + this.unitsFormControl.enable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html deleted file mode 100644 index 2327d708da..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html +++ /dev/null @@ -1,58 +0,0 @@ - -
- -

{{ 'datakey.configuration' | translate }}

- - -
- - -
-
- - -
-
- - -
- diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.scss deleted file mode 100644 index cf9fb505a2..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host { - .mat-dialog-content { - padding: 0 !important; - margin: 0 !important; - overflow: hidden; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts deleted file mode 100644 index a3d5d8fd65..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts +++ /dev/null @@ -1,92 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { Component, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; -import { ErrorStateMatcher } from '@angular/material/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; -import { DialogComponent } from '@shared/components/dialog.component'; -import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; -import { DataKeysCallbacks } from './data-keys.component.models'; -import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; -import { Dashboard } from '@shared/models/dashboard.models'; -import { IAliasController } from '@core/api/widget-api.models'; - -export interface DataKeyConfigDialogData { - dataKey: DataKey; - dataKeySettingsSchema: any; - dataKeySettingsDirective: string; - dashboard: Dashboard; - aliasController: IAliasController; - widget: Widget; - widgetType: widgetType; - entityAliasId?: string; - showPostProcessing?: boolean; - callbacks?: DataKeysCallbacks; -} - -@Component({ - selector: 'tb-data-key-config-dialog', - templateUrl: './data-key-config-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: DataKeyConfigDialogComponent}], - styleUrls: ['./data-key-config-dialog.component.scss'] -}) -export class DataKeyConfigDialogComponent extends DialogComponent - implements OnInit, ErrorStateMatcher { - - @ViewChild('dataKeyConfig', {static: true}) dataKeyConfig: DataKeyConfigComponent; - - dataKeyFormGroup: FormGroup; - - submitted = false; - - constructor(protected store: Store, - protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: DataKeyConfigDialogData, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public dialogRef: MatDialogRef, - public fb: FormBuilder) { - super(store, router, dialogRef); - } - - ngOnInit(): void { - this.dataKeyFormGroup = this.fb.group({ - dataKey: [this.data.dataKey, [Validators.required]] - }); - } - - isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { - const originalErrorState = this.errorStateMatcher.isErrorState(control, form); - const customErrorState = !!(control && control.invalid && this.submitted); - return originalErrorState || customErrorState; - } - - cancel(): void { - this.dialogRef.close(null); - } - - save(): void { - this.submitted = true; - this.dataKeyConfig.validateOnSubmit(); - if (this.dataKeyFormGroup.valid) { - const dataKey: DataKey = this.dataKeyFormGroup.get('dataKey').value; - this.dialogRef.close(dataKey); - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html deleted file mode 100644 index 1ca6b131b0..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ /dev/null @@ -1,178 +0,0 @@ - - - -
- - {{ 'entity.key' | translate }} - - - - - - - - -
- - datakey.label - - - - -
-
- - datakey.units - - - - datakey.decimals - - -
-
- - datakey.aggregation - - - {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} - - - -
- {{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }} -
- {{ 'datakey.aggregation-type-hint-common' | translate }} -
-
-
- datakey.delta-calculation - - - - - {{ 'datakey.enable-delta-calculation' | translate }} - - {{ 'datakey.enable-delta-calculation-hint' | translate }} - - - -
- - widgets.chart.time-for-comparison - - - {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - - {{ 'widgets.chart.time-for-comparison-days' | translate }} - - - {{ 'widgets.chart.time-for-comparison-weeks' | translate }} - - - {{ 'widgets.chart.time-for-comparison-months' | translate }} - - - {{ 'widgets.chart.time-for-comparison-years' | translate }} - - - {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} - - - - - widgets.chart.custom-interval-value - - - - datakey.delta-calculation-result - - - {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} - - - -
-
-
-
-
-
- datakey.data-generation-func -
- - -
-
- - {{ 'datakey.use-data-post-processing-func' | translate }} - - - - -
-
-
- -
- - -
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss deleted file mode 100644 index c4ea8019d6..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import '../../../../../scss/constants'; - -:host { - .tb-datakey-config { - height: 100%; - - .settings{ - &> :first-child{ - padding-top: 12px; - } - &> :nth-child(n){ - padding-right: 12px; - padding-left: 12px; - } - } - - .fields-group { - padding: 0 16px 8px; - margin-bottom: 10px; - border: 1px groove rgba(0, 0, 0, .25); - border-radius: 4px; - - legend { - color: rgba(0, 0, 0, .7); - width: fit-content; - } - - legend + * { - display: block; - margin-top: 16px; - } - - &.fields-group-slider { - padding: 0; - - legend { - margin-left: 16px; - } - - > .tb-settings { - margin-top: 0; - padding: 0 16px 8px; - } - } - } - - .tb-hint.after-fields { - margin-top: -1.25em; - max-width: fit-content; - line-height: 15px; - } - } -} - -:host ::ng-deep { - .tb-datakey-config { - .mat-tab-body.mat-tab-body-active { - .mat-tab-body-content > div { - height: calc(65vh - 50px); - overflow: initial; - @media #{$mat-xs} { - height: 500px; - } - } - } - .mat-expansion-panel { - &.tb-settings { - box-shadow: none; - - .mat-content { - overflow: visible; - } - - .mat-expansion-panel-header { - padding: 0; - color: rgba(0, 0, 0, 0.87); - - &:hover { - background: none; - } - - .mat-expansion-indicator { - padding: 2px; - } - } - - &.comparison { - .mat-expansion-panel-header { - height: 100%; - } - } - - .mat-expansion-panel-header-description { - align-items: center; - } - - > .mat-expansion-panel-content { - > .mat-expansion-panel-body { - padding: 0; - } - } - } - - .mat-expansion-panel-content { - font: inherit; - } - } - - .mat-slide { - margin: 8px 0; - } - - .mat-slide-toggle-content { - white-space: normal; - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts deleted file mode 100644 index a461796111..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ /dev/null @@ -1,425 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; -import { PageComponent } from '@shared/components/page.component'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { - ComparisonResultType, comparisonResultTypeTranslationMap, - DataKey, - dataKeyAggregationTypeHintTranslationMap, - Widget, - widgetType -} from '@shared/models/widget.models'; -import { - ControlValueAccessor, - FormBuilder, - FormControl, - FormGroup, - NG_VALIDATORS, - NG_VALUE_ACCESSOR, - Validator, - Validators -} from '@angular/forms'; -import { UtilsService } from '@core/services/utils.service'; -import { TranslateService } from '@ngx-translate/core'; -import { MatDialog } from '@angular/material/dialog'; -import { EntityService } from '@core/http/entity.service'; -import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { Observable, of } from 'rxjs'; -import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; -import { alarmFields } from '@shared/models/alarm.models'; -import { JsFuncComponent } from '@shared/components/js-func.component'; -import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; -import { WidgetService } from '@core/http/widget.service'; -import { Dashboard } from '@shared/models/dashboard.models'; -import { IAliasController } from '@core/api/widget-api.models'; -import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models'; - -@Component({ - selector: 'tb-data-key-config', - templateUrl: './data-key-config.component.html', - styleUrls: ['./data-key-config.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataKeyConfigComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DataKeyConfigComponent), - multi: true, - } - ] -}) -export class DataKeyConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { - - dataKeyTypes = DataKeyType; - - widgetTypes = widgetType; - - aggregations = [AggregationType.NONE, ...Object.keys(AggregationType).filter(type => type !== AggregationType.NONE)]; - - aggregationTypes = AggregationType; - - aggregationTypesTranslations = aggregationTranslations; - - dataKeyAggregationTypeHintTranslations = dataKeyAggregationTypeHintTranslationMap; - - comparisonResultTypes = ComparisonResultType; - - comparisonResults = Object.keys(ComparisonResultType); - - comparisonResultTypeTranslations = comparisonResultTypeTranslationMap; - - @Input() - entityAliasId: string; - - @Input() - callbacks: DataKeysCallbacks; - - @Input() - dashboard: Dashboard; - - @Input() - aliasController: IAliasController; - - @Input() - widget: Widget; - - @Input() - widgetType: widgetType; - - @Input() - dataKeySettingsSchema: any; - - @Input() - dataKeySettingsDirective: string; - - @Input() - showPostProcessing = true; - - @ViewChild('keyInput') keyInput: ElementRef; - - @ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent; - @ViewChild('postFuncBodyEdit') postFuncBodyEdit: JsFuncComponent; - - displayAdvanced = false; - - modelValue: DataKey; - - private propagateChange = null; - - public dataKeyFormGroup: FormGroup; - - public dataKeySettingsFormGroup: FormGroup; - - private dataKeySettingsData: JsonFormComponentData; - - private alarmKeys: Array; - - filteredKeys: Observable>; - private latestKeySearchResult: Array = null; - private fetchObservable$: Observable> = null; - - keySearchText = ''; - - functionScopeVariables: string[]; - - constructor(protected store: Store, - private utils: UtilsService, - private entityService: EntityService, - private dialog: MatDialog, - private translate: TranslateService, - private widgetService: WidgetService, - private fb: FormBuilder) { - super(store); - this.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); - } - - ngOnInit(): void { - this.alarmKeys = []; - for (const name of Object.keys(alarmFields)) { - this.alarmKeys.push({ - name, - type: DataKeyType.alarm - }); - } - if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema || - this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) { - this.displayAdvanced = true; - this.dataKeySettingsData = { - schema: this.dataKeySettingsSchema?.schema || { - type: 'object', - properties: {} - }, - form: this.dataKeySettingsSchema?.form || ['*'], - settingsDirective: this.dataKeySettingsDirective - }; - this.dataKeySettingsFormGroup = this.fb.group({ - settings: [null, []] - }); - this.dataKeySettingsFormGroup.valueChanges.subscribe(() => { - this.updateModel(); - }); - } - this.dataKeyFormGroup = this.fb.group({ - name: [null, []], - aggregationType: [null, []], - comparisonEnabled: [null, []], - timeForComparison: [null, [Validators.required]], - comparisonCustomIntervalValue: [null, [Validators.required, Validators.min(1000)]], - comparisonResultType: [null, [Validators.required]], - label: [null, [Validators.required]], - color: [null, [Validators.required]], - units: [null, []], - decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], - funcBody: [null, []], - usePostProcessing: [null, []], - postFuncBody: [null, []] - }); - - this.dataKeyFormGroup.get('aggregationType').valueChanges.subscribe( - (aggType) => { - if (!this.dataKeyFormGroup.get('label').dirty) { - let newLabel = this.dataKeyFormGroup.get('name').value; - if (aggType !== AggregationType.NONE) { - const prefix = this.translate.instant(aggregationTranslations.get(aggType)); - newLabel = prefix + ' ' + newLabel; - } - this.dataKeyFormGroup.get('label').patchValue(newLabel); - } - this.updateComparisonValidators(); - } - ); - - this.dataKeyFormGroup.get('comparisonEnabled').valueChanges.subscribe( - () => { - this.updateComparisonValues(); - } - ); - - this.dataKeyFormGroup.get('timeForComparison').valueChanges.subscribe( - () => { - this.updateComparisonValues(); - } - ); - - this.dataKeyFormGroup.valueChanges.subscribe(() => { - this.updateModel(); - }); - - this.dataKeyFormGroup.get('usePostProcessing').valueChanges.subscribe((usePostProcessing: boolean) => { - const postFuncBody: string = this.dataKeyFormGroup.get('postFuncBody').value; - if (usePostProcessing && (!postFuncBody || !postFuncBody.length)) { - this.dataKeyFormGroup.get('postFuncBody').patchValue('return value;'); - } else if (!usePostProcessing && postFuncBody && postFuncBody.length) { - this.dataKeyFormGroup.get('postFuncBody').patchValue(null); - } - }); - - this.filteredKeys = this.dataKeyFormGroup.get('name').valueChanges - .pipe( - map(value => value ? value : ''), - mergeMap(name => this.fetchKeys(name) ) - ); - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { - } - - setDisabledState(isDisabled: boolean): void { - } - - writeValue(value: DataKey): void { - this.modelValue = value; - if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) { - this.modelValue.usePostProcessing = true; - } - if (this.widgetType === widgetType.latest && this.modelValue.type === DataKeyType.timeseries && !this.modelValue.aggregationType) { - this.modelValue.aggregationType = AggregationType.NONE; - } - this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); - this.updateValidators(); - if (this.displayAdvanced) { - this.dataKeySettingsData.model = this.modelValue.settings; - this.dataKeySettingsFormGroup.patchValue({ - settings: this.dataKeySettingsData - }, {emitEvent: false}); - } - } - - private updateValidators() { - this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function && - this.modelValue.type !== DataKeyType.count - ? [Validators.required] : []); - if (this.modelValue.type === DataKeyType.count) { - this.dataKeyFormGroup.get('name').disable({emitEvent: false}); - } else { - this.dataKeyFormGroup.get('name').enable({emitEvent: false}); - } - this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); - this.updateComparisonValidators(); - } - - private updateComparisonValues() { - const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value; - if (comparisonEnabled) { - const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value; - if (!timeForComparison) { - this.dataKeyFormGroup.get('timeForComparison').patchValue('previousInterval', {emitEvent: false}); - } else if (timeForComparison === 'customInterval') { - const comparisonCustomIntervalValue = this.dataKeyFormGroup.get('comparisonCustomIntervalValue').value; - if (!comparisonCustomIntervalValue) { - this.dataKeyFormGroup.get('comparisonCustomIntervalValue').patchValue(7200000, {emitEvent: false}); - } - } - const comparisonResultType: ComparisonResultType = this.dataKeyFormGroup.get('comparisonResultType').value; - if (!comparisonResultType) { - this.dataKeyFormGroup.get('comparisonResultType').patchValue(ComparisonResultType.DELTA_ABSOLUTE, {emitEvent: false}); - } - } - this.updateComparisonValidators(); - } - - private updateComparisonValidators() { - const aggregationType: AggregationType = this.dataKeyFormGroup.get('aggregationType').value; - if (aggregationType && aggregationType !== AggregationType.NONE) { - this.dataKeyFormGroup.get('comparisonEnabled').enable({emitEvent: false}); - const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value; - if (comparisonEnabled) { - this.dataKeyFormGroup.get('timeForComparison').enable({emitEvent: false}); - const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value; - if (timeForComparison) { - this.dataKeyFormGroup.get('comparisonResultType').enable({emitEvent: false}); - if (timeForComparison === 'customInterval') { - this.dataKeyFormGroup.get('comparisonCustomIntervalValue').enable({emitEvent: false}); - } else { - this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); - } - } else { - this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false}); - this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); - } - } else { - this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false}); - this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false}); - this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); - } - } else { - this.dataKeyFormGroup.get('comparisonEnabled').disable({emitEvent: false}); - this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false}); - this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false}); - this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); - } - this.dataKeyFormGroup.get('comparisonEnabled').updateValueAndValidity({emitEvent: false}); - this.dataKeyFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false}); - this.dataKeyFormGroup.get('comparisonResultType').updateValueAndValidity({emitEvent: false}); - this.dataKeyFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false}); - } - - private updateModel() { - this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; - if (this.displayAdvanced) { - this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model; - } - if (this.modelValue.name) { - this.modelValue.name = this.modelValue.name.trim(); - } - this.propagateChange(this.modelValue); - } - - clearKey() { - this.dataKeyFormGroup.get('name').patchValue(null, {emitEvent: true}); - setTimeout(() => { - this.keyInput.nativeElement.blur(); - this.keyInput.nativeElement.focus(); - }, 0); - } - - private fetchKeys(searchText?: string): Observable> { - if (this.keySearchText !== searchText || this.latestKeySearchResult === null) { - this.keySearchText = searchText; - const dataKeyFilter = this.createKeyFilter(this.keySearchText); - return this.getKeys().pipe( - map(name => name.filter(dataKeyFilter)), - tap(res => this.latestKeySearchResult = res) - ); - } - return of(this.latestKeySearchResult); - } - - private getKeys() { - if (this.fetchObservable$ === null) { - let fetchObservable: Observable>; - if (this.modelValue.type === DataKeyType.alarm) { - fetchObservable = of(this.alarmKeys); - } else { - if (this.entityAliasId) { - const dataKeyTypes = [this.modelValue.type]; - fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); - } else { - fetchObservable = of([]); - } - } - this.fetchObservable$ = fetchObservable.pipe( - map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), - publishReplay(1), - refCount() - ); - } - return this.fetchObservable$; - } - - private createKeyFilter(query: string): (key: string) => boolean { - const lowercaseQuery = query.toLowerCase(); - return key => key.toLowerCase().startsWith(lowercaseQuery); - } - - public validateOnSubmit() { - if (this.modelValue.type === DataKeyType.function) { - this.funcBodyEdit.validateOnSubmit(); - } else if ((this.modelValue.type === DataKeyType.timeseries || - this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value) { - this.postFuncBodyEdit.validateOnSubmit(); - } - } - - public validate(c: FormControl) { - if (!this.dataKeyFormGroup.valid) { - return { - dataKey: { - valid: false - } - }; - } - if (this.displayAdvanced && (!this.dataKeySettingsFormGroup.valid || !this.modelValue.settings)) { - return { - dataKeySettings: { - valid: false - } - }; - } - return null; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html deleted file mode 100644 index 5df377cf07..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html +++ /dev/null @@ -1,169 +0,0 @@ - - - - -
-
- drag_handle -
-
-
-
-
-
- - - notifications - - - - - - - - - timeline - - - {{key.label}} -
-
:
-
-
- - close -
-
- -
- - - - - - notifications - - - - - - - - - timeline - - - - - - -
-
- entity.no-keys-found -
- - - {{ translate.get('entity.no-key-matching', - {key: truncate.transform(searchText, true, 6, '...')}) | async }} - - - entity.create-new-key - - - {{'entity.create-new-key' | translate }} - - notifications - - - - - - - - - timeline - - - -
-
-
- - {{ requiredText }} - -
-
- {{ maxDataKeysText() }} -
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts deleted file mode 100644 index a04221fd3b..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts +++ /dev/null @@ -1,24 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { DataKey, JsonSettingsSchema } from '@shared/models/widget.models'; -import { Observable } from 'rxjs'; - -export interface DataKeysCallbacks { - generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey; - fetchEntityKeys: (entityAliasId: string, types: Array) => Observable>; -} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss deleted file mode 100644 index 334f533dd4..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -.mat-chip.mat-standard-chip.tb-datakey-chip { - overflow: hidden; - - .tb-attribute-chip { - max-width: 100%; - color: rgb(66, 66, 66); - font-weight: normal; - font-size: 16px; - - .tb-chip-drag-handle { - cursor: move; - - mat-icon { - pointer-events: none; - } - } - - .tb-chip-labels { - display: flex; - flex-direction: row; - align-items: flex-end; - min-width: 0; - .tb-chip-label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .tb-chip-separator { - white-space: pre; - } - } - - .mat-chip-remove.mat-icon { - width: 24px; - height: 24px; - font-size: 24px; - margin-left: 0; - color: inherit; - opacity: inherit; - } - } -} - -:host ::ng-deep { - .mat-form-field-flex { - padding-top: 0; - .mat-form-field-infix { - border-top: 0; - } - } - - .tb-chip-label { - .tb-agg-func { - font-style: italic; - color: #0c959c; - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts deleted file mode 100644 index 940e4c4c9b..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ /dev/null @@ -1,549 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; -import { - AfterViewInit, - Component, - ElementRef, - forwardRef, - Input, - OnChanges, - OnInit, - SimpleChanges, - SkipSelf, - ViewChild -} from '@angular/core'; -import { - ControlValueAccessor, - FormBuilder, - FormControl, - FormGroup, - FormGroupDirective, - NG_VALUE_ACCESSOR, - NgForm, - Validators -} from '@angular/forms'; -import { Observable, of } from 'rxjs'; -import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; -import { AppState } from '@app/core/core.state'; -import { TranslateService } from '@ngx-translate/core'; -import { MatAutocomplete } from '@angular/material/autocomplete'; -import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; -import { IAliasController } from '@core/api/widget-api.models'; -import { DataKeysCallbacks } from './data-keys.component.models'; -import { alarmFields } from '@shared/models/alarm.models'; -import { UtilsService } from '@core/services/utils.service'; -import { ErrorStateMatcher } from '@angular/material/core'; -import { TruncatePipe } from '@shared/pipe/truncate.pipe'; -import { DialogService } from '@core/services/dialog.service'; -import { MatDialog } from '@angular/material/dialog'; -import { - DataKeyConfigDialogComponent, - DataKeyConfigDialogData -} from '@home/components/widget/data-key-config-dialog.component'; -import { deepClone } from '@core/utils'; -import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; -import { Dashboard } from '@shared/models/dashboard.models'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -import { AggregationType } from '@shared/models/time/time.models'; - -@Component({ - selector: 'tb-data-keys', - templateUrl: './data-keys.component.html', - styleUrls: ['./data-keys.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataKeysComponent), - multi: true - }, - { - provide: ErrorStateMatcher, - useExisting: DataKeysComponent - } - ] -}) -export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges, ErrorStateMatcher { - - datasourceTypes = DatasourceType; - widgetTypes = widgetType; - dataKeyTypes = DataKeyType; - - keysListFormGroup: FormGroup; - - modelValue: Array | null; - - @Input() - widgetType: widgetType; - - @Input() - datasourceType: DatasourceType; - - private maxDataKeysValue: number; - get maxDataKeys(): number { - return this.datasourceType === DatasourceType.entityCount ? 1 : this.maxDataKeysValue; - } - - @Input() - set maxDataKeys(value: number) { - this.maxDataKeysValue = value; - } - - @Input() - optDataKeys: boolean; - - @Input() - aliasController: IAliasController; - - @Input() - datakeySettingsSchema: JsonSettingsSchema; - - @Input() - dataKeySettingsDirective: string; - - @Input() - dashboard: Dashboard; - - @Input() - widget: Widget; - - @Input() - callbacks: DataKeysCallbacks; - - @Input() - entityAliasId: string; - - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue || !this.optDataKeys || this.isEntityCountDatasource; - } - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } - - @Input() - disabled: boolean; - - @ViewChild('keyInput') keyInput: ElementRef; - @ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; - @ViewChild('chipList') chipList: MatChipList; - - keys: Array = []; - filteredKeys: Observable>; - - separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; - - alarmKeys: Array; - functionTypeKeys: Array; - - dataKeyType: DataKeyType; - placeholder: string; - secondaryPlaceholder: string; - requiredText: string; - - searchText = ''; - private latestSearchTextResult: Array = null; - private fetchObservable$: Observable> = null; - - private dirty = false; - - private propagateChange = (v: any) => { }; - - constructor(private store: Store, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public translate: TranslateService, - private utils: UtilsService, - private dialogs: DialogService, - private dialog: MatDialog, - private fb: FormBuilder, - private sanitizer: DomSanitizer, - public truncate: TruncatePipe) { - } - - updateValidators() { - this.keysListFormGroup.get('keys').setValidators(this.required ? [Validators.required] : []); - this.keysListFormGroup.get('keys').updateValueAndValidity(); - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { - } - - ngOnInit() { - this.keysListFormGroup = this.fb.group({ - keys: [null, this.required ? [Validators.required] : []], - key: [null] - }); - this.alarmKeys = []; - for (const name of Object.keys(alarmFields)) { - this.alarmKeys.push({ - name, - type: DataKeyType.alarm - }); - } - this.functionTypeKeys = []; - for (const type of this.utils.getPredefinedFunctionsList()) { - this.functionTypeKeys.push({ - name: type, - type: DataKeyType.function - }); - } - this.updateParams(); - this.filteredKeys = this.keysListFormGroup.get('key').valueChanges - .pipe( - tap((value: string | DataKey) => { - if (value && typeof value !== 'string') { - this.addFromChipValue(value); - } else if (value === null) { - this.clear(this.keyInput.nativeElement.value); - } - }), - filter((value) => typeof value === 'string'), - map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), - mergeMap(name => this.fetchKeys(name) ), - share() - ); - } - - public maxDataKeysText(): string { - if (this.maxDataKeys !== null && this.maxDataKeys > -1) { - if (this.datasourceType === DatasourceType.function) { - return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys}); - } else if (!this.isEntityCountDatasource) { - return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys}); - } else { - return ''; - } - } else { - return ''; - } - } - - private updateParams() { - if (this.datasourceType === DatasourceType.function) { - this.dataKeyType = DataKeyType.function; - this.requiredText = this.translate.instant('datakey.function-types-required'); - if (this.widgetType === widgetType.latest) { - this.placeholder = this.translate.instant('datakey.latest-key-functions'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key-function'); - } else if (this.widgetType === widgetType.alarm) { - this.placeholder = this.translate.instant('datakey.alarm-key-functions'); - this.secondaryPlaceholder = '+' + this.translate.instant('alarm-key-function'); - } else { - this.placeholder = this.translate.instant('datakey.timeseries-key-functions'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key-function'); - } - } else { - if (this.widgetType === widgetType.latest) { - this.dataKeyType = null; - this.placeholder = this.translate.instant('datakey.latest-keys'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key'); - this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required'); - } else if (this.widgetType === widgetType.alarm) { - this.dataKeyType = null; - this.placeholder = this.translate.instant('datakey.alarm-keys'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.alarm-key'); - this.requiredText = this.translate.instant('datakey.alarm-fields-timeseries-or-attributes-required'); - } else { - this.dataKeyType = DataKeyType.timeseries; - this.placeholder = this.translate.instant('datakey.timeseries-keys'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key'); - this.requiredText = this.translate.instant('datakey.timeseries-required'); - } - } - } - - private reset() { - if (this.widgetType === widgetType.alarm) { - this.keys = this.utils.getDefaultAlarmDataKeys(); - } else if (this.isEntityCountDatasource) { - this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema)]; - } else { - this.keys = []; - } - this.keysListFormGroup.get('keys').setValue(this.keys); - this.modelValue = this.keys.length ? [...this.keys] : null; - if (this.keyInput) { - this.keyInput.nativeElement.value = ''; - } - this.keysListFormGroup.get('key').patchValue('', {emitEvent: false}); - this.propagateChange(this.modelValue); - this.dirty = true; - this.latestSearchTextResult = null; - } - - ngOnChanges(changes: SimpleChanges): void { - for (const propName of Object.keys(changes)) { - const change = changes[propName]; - if (!change.firstChange && change.currentValue !== change.previousValue) { - if (propName === 'entityAliasId') { - this.clearSearchCache(); - this.dirty = true; - } else if (['widgetType', 'datasourceType'].includes(propName)) { - this.clearSearchCache(); - this.updateParams(); - setTimeout(() => { - this.reset(); - }, 1); - } else if (['required', 'optDataKeys'].includes('propName')) { - this.updateValidators(); - } - } - } - } - - isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { - const originalErrorState = this.errorStateMatcher.isErrorState(control, form); - const customErrorState = this.required && !this.modelValue; - return originalErrorState || customErrorState; - } - - ngAfterViewInit(): void {} - - setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - if (this.disabled) { - this.keysListFormGroup.disable({emitEvent: false}); - } else { - this.keysListFormGroup.enable({emitEvent: false}); - } - } - - writeValue(value: Array | null): void { - this.searchText = ''; - if (value != null && value.length) { - this.modelValue = [...value]; - this.keys = [...value]; - this.keysListFormGroup.get('keys').setValue(this.keys); - } else { - this.keys = []; - this.keysListFormGroup.get('keys').setValue(this.keys); - this.modelValue = null; - } - this.dirty = true; - } - - onFocus() { - if (this.dirty) { - this.keysListFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true}); - this.dirty = false; - } - } - - private addFromChipValue(chip: DataKey) { - const key = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema); - this.addKey(key); - } - - addKey(key: DataKey): void { - if (!this.maxDataKeys || this.maxDataKeys < 0 || - !this.modelValue || this.modelValue.length < this.maxDataKeys) { - if (!this.modelValue) { - this.modelValue = []; - } - this.modelValue.push(key); - this.keys.push(key); - this.keysListFormGroup.get('keys').setValue(this.keys); - } - this.propagateChange(this.modelValue); - this.clear(); - } - - add(event: MatChipInputEvent): void { - const value = event.value; - if ((value || '').trim()) { - if (this.dataKeyType) { - this.addFromChipValue({name: value.trim(), type: this.dataKeyType}); - } - } - this.clear(); - } - - remove(key: DataKey) { - const index = this.keys.indexOf(key); - if (index >= 0) { - this.keys.splice(index, 1); - this.keysListFormGroup.get('keys').setValue(this.keys); - this.modelValue.splice(index, 1); - if (!this.modelValue.length) { - this.modelValue = null; - } - this.propagateChange(this.modelValue); - this.clear(); - } - } - - onChipDrop(event: MatChipDropEvent) { - const from = event.from; - const to = event.to; - this.keys.splice(to, 0, this.keys.splice(from, 1)[0]); - this.keysListFormGroup.get('keys').setValue(this.keys); - this.modelValue.splice(to, 0, this.modelValue.splice(from, 1)[0]); - this.propagateChange(this.modelValue); - } - - showColorPicker(key: DataKey) { - this.dialogs.colorPicker(key.color).subscribe( - (color) => { - if (color && key.color !== color) { - key.color = color; - this.propagateChange(this.modelValue); - } - } - ); - } - - editDataKey(key: DataKey, index: number) { - this.dialog.open(DataKeyConfigDialogComponent, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - dataKey: deepClone(key), - dataKeySettingsSchema: this.datakeySettingsSchema, - dataKeySettingsDirective: this.dataKeySettingsDirective, - dashboard: this.dashboard, - aliasController: this.aliasController, - widget: this.widget, - widgetType: this.widgetType, - entityAliasId: this.entityAliasId, - showPostProcessing: this.widgetType !== widgetType.alarm, - callbacks: this.callbacks - } - }).afterClosed().subscribe((updatedDataKey) => { - if (updatedDataKey) { - this.keys[index] = updatedDataKey; - this.keysListFormGroup.get('keys').setValue(this.keys); - this.modelValue[index] = updatedDataKey; - this.propagateChange(this.modelValue); - } - }); - } - - createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) { - this.addFromChipValue({name: name ? name.trim() : '', type: dataKeyType}); - } - - displayKeyFn(key?: DataKey): string | undefined { - return key ? key.name : undefined; - } - - displayDataKeyNameFn(key: DataKey): SafeHtml { - let keyName = key.name; - if (this.widgetType === widgetType.latest && key.type === DataKeyType.timeseries - && key.aggregationType && key.aggregationType !== AggregationType.NONE) { - let aggFuncName: string; - switch (key.aggregationType) { - case AggregationType.MIN: - aggFuncName = 'MIN'; - break; - case AggregationType.MAX: - aggFuncName = 'MAX'; - break; - case AggregationType.AVG: - aggFuncName = 'AVG'; - break; - case AggregationType.SUM: - aggFuncName = 'SUM'; - break; - case AggregationType.COUNT: - aggFuncName = 'COUNT'; - break; - } - keyName = `${aggFuncName}(${keyName})`; - } - if (this.datasourceType !== DatasourceType.function && key.postFuncBody) { - keyName = `f(${keyName})`; - } - return this.sanitizer.bypassSecurityTrustHtml(`${keyName}`); - } - - private fetchKeys(searchText?: string): Observable> { - if (this.searchText !== searchText || this.latestSearchTextResult === null) { - this.searchText = searchText; - const dataKeyFilter = this.createDataKeyFilter(this.searchText); - return this.getKeys().pipe( - map(name => name.filter(dataKeyFilter)), - tap(res => this.latestSearchTextResult = res) - ); - } - return of(this.latestSearchTextResult); - } - - private getKeys(): Observable> { - if (this.fetchObservable$ === null) { - let fetchObservable: Observable>; - if (this.datasourceType === DatasourceType.function) { - const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; - fetchObservable = of(targetKeysList); - } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId) { - const dataKeyTypes = [DataKeyType.timeseries]; - if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { - dataKeyTypes.push(DataKeyType.attribute); - dataKeyTypes.push(DataKeyType.entityField); - if (this.widgetType === widgetType.alarm) { - dataKeyTypes.push(DataKeyType.alarm); - } - } - fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); - } else { - fetchObservable = of([]); - } - this.fetchObservable$ = fetchObservable.pipe( - publishReplay(1), - refCount() - ); - } - return this.fetchObservable$; - } - - private createDataKeyFilter(query: string): (key: DataKey) => boolean { - const lowercaseQuery = query.toLowerCase(); - return key => key.name.toLowerCase().startsWith(lowercaseQuery); - } - - textIsNotEmpty(text: string): boolean { - return (text && text != null && text.length > 0) ? true : false; - } - - clear(value: string = '') { - this.keyInput.nativeElement.value = value; - this.keysListFormGroup.get('key').patchValue(value, {emitEvent: true}); - setTimeout(() => { - this.keyInput.nativeElement.blur(); - this.keyInput.nativeElement.focus(); - }, 0); - } - - get isEntityCountDatasource(): boolean { - return this.datasourceType === DatasourceType.entityCount; - } - - private clearSearchCache() { - this.searchText = ''; - this.fetchObservable$ = null; - this.latestSearchTextResult = null; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts index be717873da..dc3e9c7ac2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -33,6 +33,8 @@ import { CustomDialogComponent, CustomDialogData } from '@home/components/widget/dialog/custom-dialog.component'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; export interface CustomDialogContainerData { controller: (instance: CustomDialogComponent) => void; @@ -54,6 +56,8 @@ export class CustomDialogContainerComponent extends DialogComponent, + private dialogService: DialogService, + private translate: TranslateService, @Inject(MAT_DIALOG_DATA) public data: CustomDialogContainerData) { super(store, router, dialogRef); let customDialogData: CustomDialogData = { @@ -72,7 +76,19 @@ export class CustomDialogContainerComponent extends DialogComponent('ConfigDialogData'); @@ -32,7 +32,7 @@ export interface CustomDialogData { } @Directive() -// tslint:disable-next-line:directive-class-suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix export class CustomDialogComponent extends PageComponent { [key: string]: any; @@ -40,7 +40,7 @@ export class CustomDialogComponent extends PageComponent { constructor(@TbInject(Store) protected store: Store, @TbInject(Router) protected router: Router, @TbInject(MatDialogRef) public dialogRef: MatDialogRef, - @TbInject(FormBuilder) public fb: FormBuilder, + @TbInject(UntypedFormBuilder) public fb: UntypedFormBuilder, @TbInject(CUSTOM_DIALOG_DATA) public data: CustomDialogData) { super(store); // @ts-ignore diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts index 4de6abdd31..c18367d864 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Inject, Injectable, Type } from '@angular/core'; import { Observable } from 'rxjs'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { AuthService } from '@core/auth/auth.service'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; @@ -33,6 +33,8 @@ import { HOME_COMPONENTS_MODULE_TOKEN, SHARED_HOME_COMPONENTS_MODULE_TOKEN } fro @Injectable() export class CustomDialogService { + private customModules: Array> + constructor( private translate: TranslateService, private authService: AuthService, @@ -44,24 +46,35 @@ export class CustomDialogService { ) { } - customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any): Observable { + setAdditionalModules(modules: Array>) { + this.customModules = modules; + } + + customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any, + config?: MatDialogConfig): Observable { + const modules = [this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule]; + if (Array.isArray(this.customModules)) { + modules.push(...this.customModules); + } return this.dynamicComponentFactoryService.createDynamicComponentFactory( - class CustomDialogComponentInstance extends CustomDialogComponent {}, - template, - [this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule]).pipe( + class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe( mergeMap((factory) => { const dialogData: CustomDialogContainerData = { controller, customComponentFactory: factory, data }; + let dialogConfig: MatDialogConfig = { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: dialogData + }; + if (config) { + dialogConfig = {...dialogConfig, ...config}; + } return this.dialog.open( CustomDialogContainerComponent, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: dialogData - }).afterClosed().pipe( + dialogConfig).afterClosed().pipe( tap(() => { this.dynamicComponentFactoryService.destroyDynamicComponentFactory(factory); }) diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog-token.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog-token.ts index 5d0ee4733f..14b9af98ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog-token.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog-token.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html index 4936cb5ba6..36837414ed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html @@ -1,6 +1,6 @@ -
-
- - legend.direction - - - {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} - - - - - legend.position - - - {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} - - - -
-
- - {{ 'legend.show-min' | translate }} - - - {{ 'legend.show-max' | translate }} - -
-
- - {{ 'legend.show-avg' | translate }} - - - {{ 'legend.show-total' | translate }} - -
-
- - {{ 'legend.show-latest' | translate }} - - - {{ 'legend.sort-legend' | translate }} - -
- diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts deleted file mode 100644 index 16e32bc866..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts +++ /dev/null @@ -1,138 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { isDefined } from '@core/utils'; -import { - LegendConfig, - LegendDirection, - legendDirectionTranslationMap, - LegendPosition, - legendPositionTranslationMap -} from '@shared/models/widget.models'; -import { Subscription } from 'rxjs'; - -// @dynamic -@Component({ - selector: 'tb-legend-config', - templateUrl: './legend-config.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => LegendConfigComponent), - multi: true - } - ] -}) -export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { - - @Input() disabled: boolean; - - legendConfigForm: FormGroup; - legendDirection = LegendDirection; - legendDirections = Object.keys(LegendDirection); - legendDirectionTranslations = legendDirectionTranslationMap; - legendPosition = LegendPosition; - legendPositions = Object.keys(LegendPosition); - legendPositionTranslations = legendPositionTranslationMap; - - private legendSettingsFormChanges$: Subscription; - private legendSettingsFormDirectionChanges$: Subscription; - private propagateChange = (_: any) => {}; - - constructor(private fb: FormBuilder) { - } - - ngOnInit(): void { - this.legendConfigForm = this.fb.group({ - direction: [null, []], - position: [null, []], - sortDataKeys: [null, []], - showMin: [null, []], - showMax: [null, []], - showAvg: [null, []], - showTotal: [null, []], - showLatest: [null, []] - }); - this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges - .subscribe((direction: LegendDirection) => { - this.onDirectionChanged(direction); - }); - this.legendSettingsFormChanges$ = this.legendConfigForm.valueChanges.subscribe( - () => this.legendConfigUpdated() - ); - } - - private onDirectionChanged(direction: LegendDirection) { - if (direction === LegendDirection.row) { - let position: LegendPosition = this.legendConfigForm.get('position').value; - if (position !== LegendPosition.bottom && position !== LegendPosition.top) { - position = LegendPosition.bottom; - } - this.legendConfigForm.patchValue({position}, {emitEvent: false} - ); - } - } - - ngOnDestroy(): void { - if (this.legendSettingsFormDirectionChanges$) { - this.legendSettingsFormDirectionChanges$.unsubscribe(); - this.legendSettingsFormDirectionChanges$ = null; - } - if (this.legendSettingsFormChanges$) { - this.legendSettingsFormChanges$.unsubscribe(); - this.legendSettingsFormChanges$ = null; - } - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { - } - - setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - if (this.disabled) { - this.legendConfigForm.disable({emitEvent: false}); - } else { - this.legendConfigForm.enable({emitEvent: false}); - } - } - - writeValue(legendConfig: LegendConfig): void { - if (legendConfig) { - this.legendConfigForm.patchValue({ - direction: legendConfig.direction, - position: legendConfig.position, - sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false, - showMin: isDefined(legendConfig.showMin) ? legendConfig.showMin : false, - showMax: isDefined(legendConfig.showMax) ? legendConfig.showMax : false, - showAvg: isDefined(legendConfig.showAvg) ? legendConfig.showAvg : false, - showTotal: isDefined(legendConfig.showTotal) ? legendConfig.showTotal : false, - showLatest: isDefined(legendConfig.showLatest) ? legendConfig.showLatest : false - }, {emitEvent: false}); - } - this.onDirectionChanged(legendConfig.direction); - } - - private legendConfigUpdated() { - this.propagateChange(this.legendConfigForm.value); - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.html b/ui-ngx/src/app/modules/home/components/widget/legend.component.html deleted file mode 100644 index e92c5c4baf..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/legend.component.html +++ /dev/null @@ -1,87 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'legend.min' | translate }}{{ 'legend.max' | translate }}{{ 'legend.avg' | translate }}{{ 'legend.total' | translate }}{{ 'legend.latest' | translate }}
- {{ legendKey.dataKey.label }} - {{ legendData.data[legendKey.dataIndex].min }}{{ legendData.data[legendKey.dataIndex].max }}{{ legendData.data[legendKey.dataIndex].avg }}{{ legendData.data[legendKey.dataIndex].total }}{{ legendData.data[legendKey.dataIndex].latest }}
- - - {{ legendKey.dataKey.label }} - -
{{ 'legend.min' | translate }} - {{ legendData.data[legendKey.dataIndex].min }} -
{{ 'legend.max' | translate }} - {{ legendData.data[legendKey.dataIndex].max }} -
{{ 'legend.avg' | translate }} - {{ legendData.data[legendKey.dataIndex].avg }} -
{{ 'legend.total' | translate }} - {{ legendData.data[legendKey.dataIndex].total }} -
{{ 'legend.latest' | translate }} - {{ legendData.data[legendKey.dataIndex].latest }} -
diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.scss b/ui-ngx/src/app/modules/home/components/widget/legend.component.scss deleted file mode 100644 index 9aefd0c88d..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/legend.component.scss +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host { - table.tb-legend { - font-size: 12px; - - &.tb-legend-column { - width: 100%; - } - - &.tb-legend-row { - width: auto; - margin-left: auto; - margin-right: auto; - } - - .tb-legend-header, - .tb-legend-value { - text-align: right; - } - - .tb-legend-header { - th { - padding: 0 10px 1px 0; - color: rgb(255, 110, 64); - white-space: nowrap; - font-size: 12px; - } - } - - .tb-legend-keys { - td { - font-size: 12px; - } - td.tb-legend-label, - td.tb-legend-value { - padding: 2px 10px; - white-space: nowrap; - } - - .tb-legend-type { - min-width: 50px; - padding: 0 10px 1px 0; - font-weight: 700; - color: rgb(255, 110, 64); - text-align: right; - white-space: nowrap; - } - - .tb-legend-line { - display: inline-block; - width: 15px; - height: 3px; - vertical-align: middle; - } - - .tb-legend-label { - padding: 2px 10px; - text-align: left; - white-space: nowrap; - outline: none; - - &.tb-horizontal { - width: 95%; - } - - &.tb-hidden-label { - text-decoration: line-through; - opacity: .6; - } - } - - &.tb-row-direction { - display: inline-block; - } - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend.component.ts deleted file mode 100644 index 4548621517..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { LegendConfig, LegendData, LegendDirection, LegendKey, LegendPosition } from '@shared/models/widget.models'; - -@Component({ - selector: 'tb-legend', - templateUrl: './legend.component.html', - styleUrls: ['./legend.component.scss'] -}) -export class LegendComponent implements OnInit { - - @Input() - legendConfig: LegendConfig; - - @Input() - legendData: LegendData; - - @Output() - legendKeyHiddenChange = new EventEmitter(); - - displayHeader: boolean; - - isHorizontal: boolean; - - isRowDirection: boolean; - - ngOnInit(): void { - this.displayHeader = this.legendConfig.showMin === true || - this.legendConfig.showMax === true || - this.legendConfig.showAvg === true || - this.legendConfig.showTotal === true || - this.legendConfig.showLatest === true; - - this.isHorizontal = this.legendConfig.position === LegendPosition.bottom || - this.legendConfig.position === LegendPosition.top; - - this.isRowDirection = this.legendConfig.direction === LegendDirection.row; - } - - toggleHideData(index: number) { - const dataKey = this.legendData.keys.find(key => key.dataIndex === index).dataKey; - if (!dataKey.settings.disableDataHiding) { - dataKey.hidden = !dataKey.hidden; - this.legendKeyHiddenChange.emit(index); - } - } - - legendKeys(): LegendKey[] { - try { - let keys = this.legendData.keys; - if (this.legendConfig.sortDataKeys) { - keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); - } - return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend); - } catch (e) {} - } - -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html deleted file mode 100644 index 9741440574..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html +++ /dev/null @@ -1,65 +0,0 @@ - -
- - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - - - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - - - - - alarm.alarm-type-list - - - {{type}} - cancel - - - - -
- - -
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss deleted file mode 100644 index 45ba061843..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host { - width: 100%; - height: 100%; - min-width: 300px; - overflow: hidden; - background: #fff; - border-radius: 4px; - box-shadow: - 0 7px 8px -4px rgba(0, 0, 0, .2), - 0 13px 19px 2px rgba(0, 0, 0, .14), - 0 5px 24px 4px rgba(0, 0, 0, .12); - - .mat-content { - overflow: hidden; - background-color: #fff; - } - - .mat-padding { - padding: 16px; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts deleted file mode 100644 index 04a2740ce9..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts +++ /dev/null @@ -1,119 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { Component, Inject, InjectionToken } from '@angular/core'; -import { - AlarmSearchStatus, - alarmSearchStatusTranslations, - AlarmSeverity, - alarmSeverityTranslations -} from '@shared/models/alarm.models'; -import { FormBuilder, FormGroup } from '@angular/forms'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; -import { OverlayRef } from '@angular/cdk/overlay'; - -export const ALARM_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); - -export interface AlarmFilterPanelData { - statusList: AlarmSearchStatus[]; - severityList: AlarmSeverity[]; - typeList: string[]; -} - -@Component({ - selector: 'tb-alarm-filter-panel', - templateUrl: './alarm-filter-panel.component.html', - styleUrls: ['./alarm-filter-panel.component.scss'] -}) -export class AlarmFilterPanelComponent { - - readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; - - alarmFilterFormGroup: FormGroup; - - result: AlarmFilterPanelData; - - alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, - AlarmSearchStatus.CLEARED, - AlarmSearchStatus.ACK, - AlarmSearchStatus.UNACK]; - - alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; - - alarmSeverities = Object.keys(AlarmSeverity); - alarmSeverityEnum = AlarmSeverity; - - alarmSeverityTranslationMap = alarmSeverityTranslations; - - constructor(@Inject(ALARM_FILTER_PANEL_DATA) - public data: AlarmFilterPanelData, - public overlayRef: OverlayRef, - private fb: FormBuilder) { - this.alarmFilterFormGroup = this.fb.group( - { - alarmStatusList: [this.data.statusList], - alarmSeverityList: [this.data.severityList], - alarmTypeList: [this.data.typeList] - } - ); - } - - public alarmTypeList(): string[] { - return this.alarmFilterFormGroup.get('alarmTypeList').value; - } - - public removeAlarmType(type: string): void { - const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value; - const index = types.indexOf(type); - if (index >= 0) { - types.splice(index, 1); - this.alarmFilterFormGroup.get('alarmTypeList').setValue(types); - this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty(); - } - } - - public addAlarmType(event: MatChipInputEvent): void { - const input = event.input; - const value = event.value; - - const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value; - - if ((value || '').trim()) { - types.push(value.trim()); - this.alarmFilterFormGroup.get('alarmTypeList').setValue(types); - this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty(); - } - - if (input) { - input.value = ''; - } - } - - update() { - this.result = { - statusList: this.alarmFilterFormGroup.get('alarmStatusList').value, - severityList: this.alarmFilterFormGroup.get('alarmSeverityList').value, - typeList: this.alarmFilterFormGroup.get('alarmTypeList').value - }; - this.overlayRef.dispose(); - } - - cancel() { - this.overlayRef.dispose(); - } -} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 6a0a4fc3e1..9a23e86bf8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -1,6 +1,6 @@
- +
-
- +
{{ translate.get('alarm.selected-alarms', @@ -45,13 +45,13 @@ - + + - + - +
-
-
- + +
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.scss new file mode 100644 index 0000000000..da03a5e40b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-add-doc-link-dialog { + width: 480px; + .mat-toolbar-single-row { + padding: 0 24px; + } + h2 { + color: rgba(0, 0, 0, 0.76); + } + .mat-icon { + color: rgba(0, 0, 0, 0.54); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts new file mode 100644 index 0000000000..8a7413b7dd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { DocumentationLink } from '@shared/models/user-settings.models'; + +@Component({ + selector: 'tb-add-doc-link-dialog', + templateUrl: './add-doc-link-dialog.component.html', + styleUrls: ['./add-doc-link-dialog.component.scss'] +}) +export class AddDocLinkDialogComponent extends + DialogComponent implements OnInit { + + addDocLinkFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.addDocLinkFormGroup = this.fb.group({ + docLink: [{ icon: 'notifications' }, [Validators.required]] + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(docLink: DocumentationLink): void { + this.dialogRef.close(docLink); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html new file mode 100644 index 0000000000..6eaf4ca205 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html @@ -0,0 +1,33 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.scss new file mode 100644 index 0000000000..0a7cf8b418 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-add-quick-link-dialog { + width: 480px; + .mat-toolbar-single-row { + padding: 0 24px; + } + h2 { + color: rgba(0, 0, 0, 0.76); + } + .mat-icon { + color: rgba(0, 0, 0, 0.54); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.ts new file mode 100644 index 0000000000..ac9cb381d6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.ts @@ -0,0 +1,57 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-add-quick-link-dialog', + templateUrl: './add-quick-link-dialog.component.html', + styleUrls: ['./add-quick-link-dialog.component.scss'] +}) +export class AddQuickLinkDialogComponent extends + DialogComponent implements OnInit { + + addQuickLinkFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.addQuickLinkFormGroup = this.fb.group({ + link: [null, [Validators.required]] + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(link: string): void { + this.dialogRef.close(link); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html new file mode 100644 index 0000000000..01c02757a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html @@ -0,0 +1,77 @@ + + + + + {{ 'widgets.cluster-info.service-id' | translate }} + + + {{ serviceData.serviceId }} + + + + + {{ 'widgets.cluster-info.service-type' | translate }} + + + {{ serviceData.serviceType }} + + + + + {{ 'widgets.system-info.cpu' | translate }} + + + {{ serviceData.cpuUsage }}% + + + + + {{ 'widgets.system-info.ram' | translate }} + + + {{ serviceData.memoryUsage }}% + + + + + {{ 'widgets.system-info.disk' | translate }} + + + {{ serviceData.discUsage }}% + + + + +
+widgets.cluster-info.no-data diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss new file mode 100644 index 0000000000..0d76956dfc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + span.no-data-found { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + } + + .tb-resource-usage { + cursor: default; + &.tb-status-warn, &.tb-status-critical { + position: relative; + overflow: visible; + &:before { + position: absolute; + top: 15px; + left: -24px; + font-family: 'Material Icons Round'; + font-size: 20px; + line-height: 1; + } + } + &.tb-status-warn { + color: #FAA405; + &:before { + content: "warning"; + color: #FAA405; + } + } + &.tb-status-critical { + color: #D12730; + &:before { + content: "warning"; + color: #D12730; + } + } + } +} + +:host ::ng-deep { + .mdc-data-table__header-cell { + font-weight: 500; + font-size: 11px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + } + .mdc-data-table__cell { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts new file mode 100644 index 0000000000..7940d2c21a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts @@ -0,0 +1,237 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, NgZone, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; +import { catchError, map, publishReplay, refCount, share } from 'rxjs/operators'; +import { MatSort } from '@angular/material/sort'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { EntityDataUpdate, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { EntityKeyType } from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; + +export interface SystemInfoData { + serviceId: string; + serviceType: string; + cpuUsage: number; + cpuCount: number; + memoryUsage: number; + totalMemory: number; + discUsage: number; + totalDiscSpace: number; +} + +@Component({ + selector: 'tb-cluster-info-table', + templateUrl: './cluster-info-table.component.html', + styleUrls: ['./cluster-info-table.component.scss'] +}) +export class ClusterInfoTableComponent extends PageComponent implements OnInit, AfterViewInit { + + @ViewChild(MatSort) sort: MatSort; + + displayedColumns = ['serviceId', 'serviceType', 'cpuUsage', 'memoryUsage', 'discUsage']; + + dataSource: SystemInfoDataSource; + + pageLink: PageLink; + + constructor(protected store: Store, + private telemetryWsService: TelemetryWebsocketService, + private translate: TranslateService, + private zone: NgZone) { + super(store); + } + + ngOnInit() { + this.dataSource = new SystemInfoDataSource(this.telemetryWsService, this.zone); + const sortOrder: SortOrder = { + property: 'serviceId', + direction: Direction.ASC + }; + this.pageLink = new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, sortOrder); + this.dataSource.loadData(this.pageLink); + } + + ngAfterViewInit() { + this.sort.sortChange.subscribe(() => this.updateData()); + } + + updateData() { + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.dataSource.loadData(this.pageLink); + } + + statusClass(value: number): string { + let status = ''; + if (value > 85) { + status = 'tb-status-critical'; + } else if (value > 75) { + status = 'tb-status-warn'; + } + return status; + } + + infoTooltip(serviceData: SystemInfoData, type: 'cpu' | 'ram' | 'disc'): string { + let tooltip = ''; + let value: number; + switch (type) { + case 'cpu': + value = serviceData.cpuUsage; + tooltip += value + ' / 100 %'; + break; + case 'ram': + value = serviceData.memoryUsage; + const memoryUsage = serviceData.memoryUsage; + const totalMemory = serviceData.totalMemory / 1073741824; + const usedMemory = (totalMemory * (memoryUsage / 100)).toFixed(0); + tooltip += usedMemory + ' / ' + totalMemory.toFixed(0) + ' Gb'; + break; + case 'disc': + value = serviceData.discUsage; + const discUsage = serviceData.discUsage; + const totalDiscSpace = serviceData.totalDiscSpace / 1073741824; + const usedDisc = (totalDiscSpace * (discUsage / 100)).toFixed(0); + tooltip += usedDisc + ' / ' + totalDiscSpace.toFixed(0) + ' Gb'; + break; + } + if (value > 85) { + switch (type) { + case 'cpu': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.cpu-critical-text'); + break; + case 'ram': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.ram-critical-text'); + break; + case 'disc': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.disk-critical-text'); + break; + } + } else if (value > 75) { + switch (type) { + case 'cpu': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.cpu-warning-text'); + break; + case 'ram': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.ram-warning-text'); + break; + case 'disc': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.disk-warning-text'); + break; + } + } + return tooltip; + } +} + +export class SystemInfoDataSource implements DataSource { + + private systemDataSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + private allSystemData: Observable>; + + private telemetrySubscriber: TelemetrySubscriber; + + constructor(private telemetryWsService: TelemetryWebsocketService, + private zone: NgZone) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.systemDataSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.systemDataSubject.complete(); + this.pageDataSubject.complete(); + if (this.telemetrySubscriber) { + this.telemetrySubscriber.unsubscribe(); + this.telemetrySubscriber = null; + } + } + + loadData(pageLink: PageLink): void { + this.getAllSystemData().pipe( + map((data) => pageLink.filterData(data)), + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.onSystemData(pageData.data); + this.pageDataSubject.next(pageData); + } + ); + } + + getAllSystemData(): Observable> { + if (!this.allSystemData) { + this.telemetrySubscriber = TelemetrySubscriber.createEntityFilterLatestSubscription( + this.telemetryWsService, { type: AliasFilterType.apiUsageState }, this.zone, + [ { key: 'clusterSystemData', type: EntityKeyType.TIME_SERIES } ]); + this.telemetrySubscriber.subscribe(); + this.allSystemData = this.telemetrySubscriber.entityData$.pipe( + map((update) => this.toSystemInfoData(update)), + publishReplay(1), + refCount() + ); + } + return this.allSystemData; + } + + private toSystemInfoData(entityDataUpdate: EntityDataUpdate): Array { + const entityData = entityDataUpdate.data?.data ? entityDataUpdate.data?.data : entityDataUpdate.update; + if (entityData && entityData.length) { + const latest = entityData[0].latest; + if (latest && latest[EntityKeyType.TIME_SERIES]) { + const latestTs = latest[EntityKeyType.TIME_SERIES]; + const tsValue = latestTs.clusterSystemData; + if (tsValue && tsValue.value) { + return JSON.parse(tsValue.value); + } + } + } + return []; + } + + protected onSystemData(data: SystemInfoData[]) { + this.systemDataSubject.next(data); + } + + isEmpty(): Observable { + return this.systemDataSubject.pipe( + map((entities) => !entities.length), + share() + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements), + share() + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.html new file mode 100644 index 0000000000..9c028856be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.html @@ -0,0 +1,72 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss new file mode 100644 index 0000000000..12b7bce17d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss @@ -0,0 +1,104 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../scss/constants"; + +:host { + + .tb-title { + margin-top: 8px; + } + + .tb-info-icon { + color: rgba(0, 0, 0, 0.12); + font-size: 16px; + width: 16px; + height: 16px; + line-height: 15px; + vertical-align: middle; + } + + .tb-features-list { + overflow: auto; + } + + .mat-grid-tile.tb-feature-tile { + overflow: visible; + } + + .tb-feature-button { + height: 50px; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + padding: 12px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + @media #{$mat-md-lg} { + height: 22px; + padding: 0 8px; + } + &:hover { + border: 1px solid rgba(0, 0, 0, 0.12); + box-shadow: 0 4px 10px rgba(23, 33, 90, 0.08); + } + .tb-feature-text { + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: rgba(0, 0, 0, 0.87); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + } + .tb-feature-status { + display: inline-block; + font-family: 'Material Icons Round'; + font-size: 20px; + line-height: 20px; + color: rgba(0, 0, 0, 0.12); + font-weight: 600; + position: relative; + background: #F4F4F4; + border-radius: 4px; + width: 24px; + height: 24px; + padding: 2px; + margin-right: 8px; + vertical-align: middle; + @media #{$mat-md-lg} { + font-size: 14px; + line-height: 14px; + width: 18px; + height: 18px; + margin-right: 4px; + } + } + &.configured { + .tb-feature-status { + color: #198038; + background: #F3F6FA; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts new file mode 100644 index 0000000000..d9c2609dc7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AdminService } from '@core/http/admin.service'; +import { FeaturesInfo } from '@shared/models/settings.models'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { of, Subscription } from 'rxjs'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { TranslateService } from '@ngx-translate/core'; +import { MediaBreakpoints } from '@shared/models/constants'; + +@Component({ + selector: 'tb-configured-features', + templateUrl: './configured-features.component.html', + styleUrls: ['./home-page-widget.scss', './configured-features.component.scss'] +}) +export class ConfiguredFeaturesComponent extends PageComponent implements OnInit, OnDestroy { + + authUser = getCurrentAuthUser(this.store); + featuresInfo: FeaturesInfo; + rowHeight = '50px'; + gutterSize = '12px'; + bottomColspan = 2; + lastColspan = 2; + + private observeBreakpointSubscription: Subscription; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private adminService: AdminService, + private translate: TranslateService, + private breakpointObserver: BreakpointObserver) { + super(store); + } + + ngOnInit() { + const isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); + const isLtMd = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']); + this.rowHeight = isMdLg ? '22px' : '50px'; + this.gutterSize = isMdLg ? '8px' : '12px'; + this.bottomColspan = isLtMd ? 3 : 2; + this.lastColspan = isLtMd ? 6 : 2; + this.observeBreakpointSubscription = this.breakpointObserver + .observe([MediaBreakpoints['md-lg'], MediaBreakpoints['lt-md']]) + .subscribe((state: BreakpointState) => { + if (state.breakpoints[MediaBreakpoints['md-lg']]) { + this.rowHeight = '22px'; + this.gutterSize = '8px'; + } else { + this.rowHeight = '50px'; + this.gutterSize = '12px'; + } + if (state.breakpoints[MediaBreakpoints['lt-md']]) { + this.bottomColspan = 3; + this.lastColspan = 6; + } else { + this.bottomColspan = 2; + this.lastColspan = 2; + } + this.cd.markForCheck(); + } + ); + (this.authUser.authority === Authority.SYS_ADMIN ? + this.adminService.getFeaturesInfo() : of(null)).subscribe( + (featuresInfo) => { + this.featuresInfo = featuresInfo; + this.cd.markForCheck(); + } + ); + } + + ngOnDestroy() { + if (this.observeBreakpointSubscription) { + this.observeBreakpointSubscription.unsubscribe(); + } + super.ngOnDestroy(); + } + + featureTooltip(configured: boolean): string { + if (configured) { + return this.translate.instant('widgets.configured-features.feature-configured'); + } else { + return this.translate.instant('widgets.configured-features.feature-not-configured'); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html new file mode 100644 index 0000000000..2d6ea9b3fa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html @@ -0,0 +1,70 @@ + +
+ +
+ + +
+
+ +
+ +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts new file mode 100644 index 0000000000..323d56ced4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts @@ -0,0 +1,190 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output, SkipSelf } from '@angular/core'; +import { + ControlValueAccessor, FormGroupDirective, + NG_VALUE_ACCESSOR, NgForm, + UntypedFormBuilder, UntypedFormControl, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DocumentationLink } from '@shared/models/user-settings.models'; +import { ErrorStateMatcher } from '@angular/material/core'; + +@Component({ + selector: 'tb-doc-link', + templateUrl: './doc-link.component.html', + styleUrls: ['./link.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DocLinkComponent), + multi: true + }, + {provide: ErrorStateMatcher, useExisting: DocLinkComponent} + ] +}) +export class DocLinkComponent extends PageComponent implements OnInit, ControlValueAccessor, ErrorStateMatcher { + + @Input() + disabled: boolean; + + @Input() + addOnly = false; + + @Input() + disableEdit = false; + + @Output() + docLinkAdded = new EventEmitter(); + + @Output() + docLinkAddCanceled = new EventEmitter(); + + @Output() + docLinkUpdated = new EventEmitter(); + + @Output() + docLinkDeleted = new EventEmitter(); + + @Output() + editModeChanged = new EventEmitter(); + + editMode = false; + addMode = false; + + docLink: DocumentationLink; + + private propagateChange = null; + + public editDocLinkFormGroup: UntypedFormGroup; + + private submitted = false; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { + super(store); + } + + ngOnInit(): void { + this.addMode = this.addOnly; + this.editDocLinkFormGroup = this.fb.group({ + icon: [null, [Validators.required]], + name: [null, [Validators.required]], + link: [null, [Validators.required]] + }); + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.editDocLinkFormGroup.disable({emitEvent: false}); + } else { + this.editDocLinkFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DocumentationLink): void { + this.docLink = value; + this.editDocLinkFormGroup.patchValue( + value, {emitEvent: false} + ); + if (!this.editDocLinkFormGroup.valid) { + this.addMode = true; + this.editModeChanged.emit(true); + } + } + + switchToEditMode() { + if (!this.disableEdit && !this.editMode) { + this.submitted = false; + this.editDocLinkFormGroup.patchValue( + this.docLink, {emitEvent: false} + ); + this.editMode = true; + this.editModeChanged.emit(true); + } + } + + apply() { + this.submitted = true; + this.updateModel(); + if (this.editDocLinkFormGroup.valid) { + this.editMode = false; + this.editModeChanged.emit(false); + this.docLinkUpdated.next(this.editDocLinkFormGroup.value); + } + } + + cancelEdit() { + this.submitted = false; + this.editMode = false; + this.editModeChanged.emit(false); + } + + add() { + this.submitted = true; + this.updateModel(); + if (this.editDocLinkFormGroup.valid) { + if (!this.addOnly) { + this.addMode = false; + this.editModeChanged.emit(false); + } + this.docLinkAdded.next(this.editDocLinkFormGroup.value); + } + } + + cancelAdd() { + this.editModeChanged.emit(false); + this.docLinkAddCanceled.emit(); + } + + delete() { + this.docLinkDeleted.emit(); + } + + isEditing() { + return this.editMode || this.addMode; + } + + private updateModel() { + if (this.editDocLinkFormGroup.valid) { + this.docLink = this.editDocLinkFormGroup.value; + this.propagateChange(this.editDocLinkFormGroup.value); + } else { + this.propagateChange(null); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html new file mode 100644 index 0000000000..1ccc2e8b24 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html @@ -0,0 +1,50 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts new file mode 100644 index 0000000000..55e33a8948 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts @@ -0,0 +1,209 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Authority } from '@shared/models/authority.enum'; +import { map, Subscription } from 'rxjs'; +import { DocumentationLink, DocumentationLinks } from '@shared/models/user-settings.models'; +import { UserSettingsService } from '@core/http/user-settings.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { MatDialog } from '@angular/material/dialog'; +import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component'; +import { + EditLinksDialogComponent, + EditLinksDialogData +} from '@home/components/widget/lib/home-page/edit-links-dialog.component'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; + +const defaultDocLinksMap = new Map( + [ + [Authority.SYS_ADMIN, { + links: [ + { + icon: 'rocket', + name: 'Getting started', + link: 'https://thingsboard.io/docs/getting-started-guides/helloworld/' + }, + { + icon: 'title', + name: 'Tenant profiles', + link: 'https://thingsboard.io/docs/user-guide/tenant-profiles/' + }, + { + icon: 'insert_chart', + name: 'API', + link: 'https://thingsboard.io/docs/api/' + }, + { + icon: 'now_widgets', + name: 'Widgets Library', + link: 'https://thingsboard.io/docs/user-guide/ui/widget-library/' + } + ] + }], + [Authority.TENANT_ADMIN, { + links: [ + { + icon: 'rocket', + name: 'Getting started', + link: 'https://thingsboard.io/docs/getting-started-guides/helloworld/' + }, + { + icon: 'settings_ethernet', + name: 'Rule engine', + link: 'https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/' + }, + { + icon: 'insert_chart', + name: 'API', + link: 'https://thingsboard.io/docs/api/' + }, + { + icon: 'devices', + name: 'Device profiles', + link: 'https://thingsboard.io/docs/user-guide/device-profiles/' + } + ] + }], + [Authority.CUSTOMER_USER, { + links: [ + { + icon: 'rocket', + name: 'Getting started', + link: 'https://thingsboard.io/docs/getting-started-guides/helloworld/' + } + ] + }] + ] +); + +interface DocLinksWidgetSettings { + columns: number; +} + +@Component({ + selector: 'tb-doc-links-widget', + templateUrl: './doc-links-widget.component.html', + styleUrls: ['./home-page-widget.scss', './links-widget.component.scss'] +}) +export class DocLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + @Input() + ctx: WidgetContext; + + settings: DocLinksWidgetSettings; + columns: number; + rowHeight = '55px'; + gutterSize = '12px'; + + documentationLinks: DocumentationLinks; + authUser = getCurrentAuthUser(this.store); + + private observeBreakpointSubscription: Subscription; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private userSettingsService: UserSettingsService, + private dialog: MatDialog, + private breakpointObserver: BreakpointObserver) { + super(store); + } + + ngOnInit() { + this.settings = this.ctx.settings; + this.columns = this.settings.columns || 3; + const isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); + this.rowHeight = isMdLg ? '18px' : '55px'; + this.gutterSize = isMdLg ? '8px' : '12px'; + this.observeBreakpointSubscription = this.breakpointObserver + .observe(MediaBreakpoints['md-lg']) + .subscribe((state: BreakpointState) => { + if (state.matches) { + this.rowHeight = '18px'; + this.gutterSize = '8px'; + } else { + this.rowHeight = '55px'; + this.gutterSize = '12px'; + } + this.cd.markForCheck(); + } + ); + this.loadDocLinks(); + } + + ngOnDestroy() { + if (this.observeBreakpointSubscription) { + this.observeBreakpointSubscription.unsubscribe(); + } + super.ngOnDestroy(); + } + + private loadDocLinks() { + this.userSettingsService.getDocumentationLinks().pipe( + map((documentationLinks) => { + if (!documentationLinks || !documentationLinks.links) { + return defaultDocLinksMap.get(this.authUser.authority); + } else { + return documentationLinks; + } + }) + ).subscribe( + (documentationLinks) => { + this.documentationLinks = documentationLinks; + this.cd.markForCheck(); + } + ); + } + + edit() { + this.dialog.open(EditLinksDialogComponent, { + disableClose: true, + autoFocus: false, + data: { + mode: 'docs', + links: this.documentationLinks + }, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (result) => { + if (result) { + this.loadDocLinks(); + } + }); + } + + addLink() { + this.dialog.open(AddDocLinkDialogComponent, { + disableClose: true, + autoFocus: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (docLink) => { + if (docLink) { + this.documentationLinks.links.push(docLink); + this.cd.markForCheck(); + this.userSettingsService.updateDocumentationLinks(this.documentationLinks).subscribe(); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html new file mode 100644 index 0000000000..e70e8cd5c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html @@ -0,0 +1,95 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.scss new file mode 100644 index 0000000000..0e914e93a6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.scss @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-edit-links-dialog { + width: 480px; + .mat-mdc-dialog-content { + max-height: 50vh; + } + .mat-toolbar-single-row { + padding: 0 24px; + } + h2 { + color: rgba(0, 0, 0, 0.76); + } + .mat-icon { + color: rgba(0, 0, 0, 0.54); + } + .mat-mdc-dialog-content { + padding: 24px 24px 8px 24px; + } + .mdc-dialog__actions { + padding: 0 24px 24px; + } + } + .tb-drag-handle { + height: 24px; + margin-left: 12px; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + .tb-add-link-button { + height: 55px; + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: #FFFFFF; + padding: 8px; + border: 2px dashed rgba(0, 0, 0, 0.08); + border-radius: 10px; + .tb-add-icon { + color: rgba(0, 0, 0, 0.12); + } + &:hover { + .tb-add-icon { + color: rgba(0, 0, 0, 0.38); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.ts new file mode 100644 index 0000000000..32415c6669 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { DocumentationLink, DocumentationLinks, QuickLinks } from '@shared/models/user-settings.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { UserSettingsService } from '@core/http/user-settings.service'; +import { Observable } from 'rxjs'; + +export interface EditLinksDialogData { + mode: 'docs' | 'quickLinks'; + links: DocumentationLinks | QuickLinks; +} + +@Component({ + selector: 'tb-edit-links-dialog', + templateUrl: './edit-links-dialog.component.html', + styleUrls: ['./edit-links-dialog.component.scss'] +}) +export class EditLinksDialogComponent extends + DialogComponent implements OnInit { + + updated = false; + addMode = false; + editMode = false; + + links = this.data.links; + mode = this.data.mode; + addingLink: Partial | string; + + editLinksFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EditLinksDialogData, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder, + private userSettingsService: UserSettingsService) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + const linksControls: Array = []; + for (const link of this.links.links) { + linksControls.push(this.fb.control(link, [Validators.required])); + } + this.editLinksFormGroup = this.fb.group({ + links: this.fb.array(linksControls) + }); + } + + linksFormArray(): UntypedFormArray { + return this.editLinksFormGroup.get('links') as UntypedFormArray; + } + + trackByLink(index: number, linkControl: AbstractControl): any { + return linkControl; + } + + linkDrop(event: CdkDragDrop) { + const linksArray = this.editLinksFormGroup.get('links') as UntypedFormArray; + const link = linksArray.at(event.previousIndex); + linksArray.removeAt(event.previousIndex); + linksArray.insert(event.currentIndex, link); + this.update(); + } + + addLink() { + this.addingLink = this.mode === 'docs' ? { icon: 'notifications' } : null; + this.addMode = true; + } + + linkAdded(link: DocumentationLink | string) { + this.addMode = false; + const linksArray = this.editLinksFormGroup.get('links') as UntypedFormArray; + const linkControl = this.fb.control(link, [Validators.required]); + linksArray.push(linkControl); + this.update(); + } + + deleteLink(index: number) { + (this.editLinksFormGroup.get('links') as UntypedFormArray).removeAt(index); + this.update(); + } + + update() { + if (this.editLinksFormGroup.valid) { + let updateObservable: Observable; + if (this.mode === 'docs') { + updateObservable = this.userSettingsService.updateDocumentationLinks(this.editLinksFormGroup.value); + } else { + updateObservable = this.userSettingsService.updateQuickLinks(this.editLinksFormGroup.value); + } + updateObservable.subscribe(() => { + this.updated = true; + }); + } + } + + close(): void { + this.dialogRef.close(this.updated); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.html new file mode 100644 index 0000000000..ac57d77c3a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.html @@ -0,0 +1,34 @@ + +
+
+ + +
+
+
+ done +
+
+
+
widgets.getting-started.done-welcome-title
+
widgets.getting-started.done-welcome-text
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.scss new file mode 100644 index 0000000000..e440fa89a0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.scss @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-getting-started-completed-dialog { + .mat-mdc-dialog-container .mdc-dialog__surface { + border-radius: 8px; + background-image: url(/assets/home/greetings_bg.svg); + background-repeat: no-repeat; + } + + .tb-getting-started-completed-dialog-container { + width: 500px; + height: 290px; + } + + .tb-done-container { + width: 70px; + height: 70px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.05); + } + + .tb-done-icon { + width: 62px; + height: 62px; + line-height: 56px; + font-size: 36px; + text-align: center; + border-radius: 50%; + border: 2px solid #305680; + background: #FFFFFF; + } + + .tb-done-welcome-title { + font-weight: 600; + font-size: 20px; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.87); + padding-bottom: 8px; + } + + .tb-done-welcome-text { + font-weight: 400; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.54); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts new file mode 100644 index 0000000000..087b70f3d8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ViewEncapsulation } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UntypedFormGroup } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-getting-started-completed-dialog', + templateUrl: './getting-started-completed-dialog.component.html', + styleUrls: ['./getting-started-completed-dialog.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class GettingStartedCompletedDialogComponent extends + DialogComponent { + + addDocLinkFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + dialogRef.addPanelClass('tb-getting-started-completed-dialog'); + } + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html new file mode 100644 index 0000000000..17ede099dc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html @@ -0,0 +1,223 @@ + +
widgets.getting-started.get-started
+
+ + + {{ index + 1 }} + + + + + +
+
widgets.getting-started.sys-admin.step1.title
+ {{ 'tenant.tenants' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step1.how-to-create-tenant' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step2.title
+ {{ 'admin.settings' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step2.how-to-configure-mail-server' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step3.title
+ {{ 'admin.settings' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step3.how-to-configure-sms-provider' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step4.title
+ {{ 'admin.settings' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step4.how-to-configure-2fa' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step5.title
+ {{ 'admin.settings' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step5.how-to-configure-oauth2' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step6.title
+ {{ 'admin.settings' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }} +
+
+ + + +
+
widgets.getting-started.tenant-admin.step1.title
+ {{ 'device.devices' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step1.how-to-create-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step2.title
+
+
+
+
+ + + + +

+ +
+ +

+ +
+ +

+
+
+

+ +
+
+ + description{{ 'widgets.getting-started.tenant-admin.step2.how-to-connect-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step3.title
+ {{ 'dashboard.dashboards' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step3.how-to-create-dashboard' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step4.title
+ {{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step5.title
+
+
+
+

+ +
+ + description{{ 'widgets.getting-started.tenant-admin.step5.how-to-create-alarm' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step6.title
+ {{ 'customer.customers' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step6.how-to-create-customer-and-assign-dashboard' | translate }} +
+
+
+
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss new file mode 100644 index 0000000000..20345a97f4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss @@ -0,0 +1,214 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../scss/constants"; + +:host { + display: flex; + flex-direction: column; + height: 100%; + + .tb-title { + font-weight: 600; + font-size: 20px; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.76); + padding-bottom: 24px; + @media #{$mat-md-lg} { + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + padding-bottom: 20px; + } + } + .tb-get-started-container { + flex: 1; + overflow: auto; + } +} + +:host ::ng-deep { + + .tb-get-started .mat-vertical-stepper-header { + border-radius: 12px; + padding: 12px; + height: auto; + } + + .tb-get-started .mat-vertical-stepper-header[aria-selected="true"] { + background: rgba(0, 0, 0, 0.03); + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + .tb-get-started .mat-vertical-content-container { + margin-left: 0; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + background: rgba(0, 0, 0, 0.03); + } + + .tb-get-started .mat-stepper-vertical-line::before { + content: none; + } + + .tb-get-started .mat-step-label { + width: 100%; + white-space: inherit; + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: rgba(0, 0, 0, 0.76); + } + + .tb-get-started .mat-step-label.mat-step-label-selected { + font-weight: 500; + font-size: 16px; + line-height: 24px; + color: rgba(0, 0, 0, 0.87); + } + + .tb-get-started .mat-vertical-content { + p, li { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.54); + em { + font-style: normal; + font-weight: 500; + font-size: 13px; + color: rgba(0, 0, 0, 0.38); + } + } + ul { + padding-inline-start: 20px; + } + .tb-bordered-content { + display: flex; + flex-direction: column; + align-items: stretch; + border: 1px solid rgba(0, 0, 0, 0.05); + border-radius: 8px; + padding: 16px; + p { + margin-top: 16px; + margin-bottom: 8px; + } + } + .tb-markdown-view { + .tb-getting-started-code { + .code-wrapper { + padding: 0; + pre[class*=language-] { + margin: 0; + padding: 9px 38px 9px 16px; + background: rgba(0, 0, 0, 0.03); + border-radius: 6px; + border: none; + } + code[class*="language-"], pre[class*="language-"] { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.38); + overflow: hidden; + white-space: break-spaces; + word-break: break-all; + & * { + color: rgba(0, 0, 0, 0.38); + cursor: inherit; + background: transparent; + } + } + button.clipboard-btn { + right: 0; + height: 34px; + p, div { + background: transparent; + } + p { + margin: 0; + padding: 7px; + color: #305680; + } + div { + top: 0; + padding: 8px; + height: 34px; + width: 34px; + img { + display: none; + } + &:after { + content: ""; + position: initial; + display: block; + width: 18px; + height: 18px; + background: #305680; + -webkit-mask-image: url(/assets/copy-code-icon.svg); + -webkit-mask-repeat: no-repeat; + mask-image: url(/assets/copy-code-icon.svg); + mask-repeat: no-repeat; + } + } + } + } + } + } + @media #{$mat-md-lg} { + .tb-bordered-content { + padding: 4px; + } + } + } + + @media #{$mat-md-lg} { + .tb-get-started .mat-step-label { + font-size: 12px; + line-height: 16px; + } + .tb-get-started .mat-step-label.mat-step-label-selected { + font-size: 13px; + line-height: 16px; + } + + .tb-get-started .mdc-button { + font-size: 12px; + line-height: 16px; + } + + .tb-get-started .mat-vertical-content { + p, li { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.25px; + } + } + + .tb-get-started .mat-vertical-content { + padding: 0 16px 16px 16px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts new file mode 100644 index 0000000000..7566ae8579 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts @@ -0,0 +1,136 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UserSettingsService } from '@core/http/user-settings.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { MatDialog } from '@angular/material/dialog'; +import { + GettingStartedCompletedDialogComponent +} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; +import { GettingStarted } from '@shared/models/user-settings.models'; +import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper'; +import { baseUrl, isUndefined } from '@core/utils'; +import { MatStepper } from '@angular/material/stepper'; +import { first } from 'rxjs/operators'; +import { Authority } from '@shared/models/authority.enum'; + +@Component({ + selector: 'tb-getting-started-widget', + templateUrl: './getting-started-widget.component.html', + styleUrls: ['./getting-started-widget.component.scss'] +}) +export class GettingStartedWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + @ViewChild('matStepper') + matStepper: MatStepper; + + @Input() + ctx: WidgetContext; + + authority = Authority; + + authUser = getCurrentAuthUser(this.store); + gettingStarted: GettingStarted = { + lastSelectedIndex: 0, + maxSelectedIndex: 0 + }; + allCompleted = false; + + baseUrl = baseUrl(); + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private userSettingsService: UserSettingsService, + private dialog: MatDialog) { + super(store); + } + + ngOnInit() { + this.userSettingsService.getGettingStarted().subscribe( + (gettingStarted) => { + if (gettingStarted) { + this.gettingStarted = gettingStarted; + if (isUndefined(this.gettingStarted.lastSelectedIndex)) { + this.gettingStarted.lastSelectedIndex = 0; + } + if (isUndefined(this.gettingStarted.maxSelectedIndex)) { + this.gettingStarted.maxSelectedIndex = 0; + } + if (this.gettingStarted.lastSelectedIndex > 0 && this.gettingStarted.lastSelectedIndex < this.matStepper.steps.length) { + const animationDuration = this.matStepper.animationDuration; + this.matStepper.animationDuration = '0'; + this.matStepper.selectedIndex = this.gettingStarted.lastSelectedIndex; + this.matStepper.animationDone.pipe(first()).subscribe(() => { + setTimeout(() => {this.matStepper.animationDuration = animationDuration;}, 0); + }); + } + if (this.matStepper.steps.length) { + this.updateCompletedSteps(); + this.cd.markForCheck(); + } else { + this.matStepper.steps.changes.subscribe( + () => { + this.updateCompletedSteps(); + this.cd.markForCheck(); + } + ); + } + } + } + ); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + isSelected(step: CdkStep) { + return this.matStepper?.selected === step; + } + + gettingStartedCompleted() { + this.dialog.open(GettingStartedCompletedDialogComponent, { + disableClose: true, + autoFocus: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe(); + } + + updateSelectedIndex(event: StepperSelectionEvent) { + if (this.gettingStarted.lastSelectedIndex !== event.selectedIndex) { + this.gettingStarted.lastSelectedIndex = event.selectedIndex; + if (event.selectedIndex > this.gettingStarted.maxSelectedIndex) { + this.gettingStarted.maxSelectedIndex = event.selectedIndex; + this.updateCompletedSteps(); + } + this.userSettingsService + .updateGettingStarted(this.gettingStarted, {ignoreLoading: true}).subscribe(); + } + } + + updateCompletedSteps() { + if (this.gettingStarted.maxSelectedIndex >= this.matStepper.steps.length-1) { + this.allCompleted = true; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss new file mode 100644 index 0000000000..4cad7a6971 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../../scss/constants"; + +:host { + + .tb-card-content { + width: 100%; + height: 100%; + } + + .tb-title { + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + @media #{$mat-md-lg} { + font-size: 12px; + line-height: 16px; + } + } + + .tb-title-link { + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + position: relative; + border-bottom: none; + @media #{$mat-md-lg} { + font-size: 12px; + line-height: 16px; + } + &:hover, &:focus { + border-bottom: none; + } + &::after { + content: 'arrow_forward'; + display: inline-block; + transform: rotate(315deg); + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 18px; + color: rgba(0, 0, 0, 0.12); + vertical-align: bottom; + margin-left: 6px; + } + &:hover::after { + color: inherit; + } + } + + .tb-no-data-available { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .tb-no-data-bg { + margin: 10px; + position: relative; + flex: 1; + width: 100%; + max-height: 100px; + &:before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #305680; + -webkit-mask-image: url(/assets/home/no_data_folder_bg.svg); + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: contain; + -webkit-mask-position: center; + mask-image: url(/assets/home/no_data_folder_bg.svg); + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .tb-no-data-text { + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + @media #{$mat-md-lg} { + font-size: 12px; + line-height: 16px; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts new file mode 100644 index 0000000000..1b69de8c45 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts @@ -0,0 +1,78 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { ClusterInfoTableComponent } from '@home/components/widget/lib/home-page/cluster-info-table.component'; +import { ConfiguredFeaturesComponent } from '@home/components/widget/lib/home-page/configured-features.component'; +import { VersionInfoComponent } from '@home/components/widget/lib/home-page/version-info.component'; +import { DocLinksWidgetComponent } from '@home/components/widget/lib/home-page/doc-links-widget.component'; +import { DocLinkComponent } from '@home/components/widget/lib/home-page/doc-link.component'; +import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component'; +import { EditLinksDialogComponent } from '@home/components/widget/lib/home-page/edit-links-dialog.component'; +import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home-page/getting-started-widget.component'; +import { + GettingStartedCompletedDialogComponent +} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; +import { UsageInfoWidgetComponent } from '@home/components/widget/lib/home-page/usage-info-widget.component'; +import { QuickLinksWidgetComponent } from '@home/components/widget/lib/home-page/quick-links-widget.component'; +import { QuickLinkComponent } from '@home/components/widget/lib/home-page/quick-link.component'; +import { AddQuickLinkDialogComponent } from '@home/components/widget/lib/home-page/add-quick-link-dialog.component'; +import { + RecentDashboardsWidgetComponent +} from '@home/components/widget/lib/home-page/recent-dashboards-widget.component'; + +@NgModule({ + declarations: + [ + ClusterInfoTableComponent, + ConfiguredFeaturesComponent, + VersionInfoComponent, + DocLinksWidgetComponent, + DocLinkComponent, + AddDocLinkDialogComponent, + EditLinksDialogComponent, + GettingStartedWidgetComponent, + GettingStartedCompletedDialogComponent, + UsageInfoWidgetComponent, + QuickLinksWidgetComponent, + QuickLinkComponent, + AddQuickLinkDialogComponent, + RecentDashboardsWidgetComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + ClusterInfoTableComponent, + ConfiguredFeaturesComponent, + VersionInfoComponent, + DocLinksWidgetComponent, + DocLinkComponent, + AddDocLinkDialogComponent, + EditLinksDialogComponent, + GettingStartedWidgetComponent, + GettingStartedCompletedDialogComponent, + UsageInfoWidgetComponent, + QuickLinksWidgetComponent, + QuickLinkComponent, + AddQuickLinkDialogComponent, + RecentDashboardsWidgetComponent + ] +}) +export class HomePageWidgetsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss new file mode 100644 index 0000000000..a488d77c6e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-autocomplete.tb-quick-links { + .mat-mdc-option { + border-bottom: none; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + .mat-icon { + margin-right: 10px; + color: rgba(0, 0, 0, 0.54); + font-size: 20px; + width: 20px; + height: 20px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/link.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/link.component.scss new file mode 100644 index 0000000000..3b71124d4f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/link.component.scss @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-edit-link { + padding: 16px; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + &.edit-mode { + border: 1px solid rgba(48, 86, 128, 0.32); + } + } + .tb-link { + height: 55px; + display: flex; + flex-direction: column; + justify-content: center; + padding: 8px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + .tb-link-container { + display: flex; + flex-direction: row; + align-items: center; + .tb-link-icon-container { + height: 40px; + padding: 8px; + background: #F3F6FA; + border-radius: 6px; + margin-right: 8px; + } + .tb-link-text { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.87); + } + } + } + .tb-edit-buttons { + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/links-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/links-widget.component.scss new file mode 100644 index 0000000000..3e52d6451d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/links-widget.component.scss @@ -0,0 +1,121 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../scss/constants"; + +:host { + .tb-card-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-end; + } + + .tb-title-icon { + color: rgba(0, 0, 0, 0.38); + width: 32px; + height: 32px; + padding: 4px; + } + + .tb-links-list { + overflow: auto; + } + + .mat-grid-tile.tb-links-tile { + overflow: visible; + } + + .tb-link-button { + height: 55px; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + padding: 8px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + @media #{$mat-md-lg} { + height: 18px; + padding: 0 8px; + } + &:hover { + border: 1px solid rgba(0, 0, 0, 0.12); + box-shadow: 0 4px 10px rgba(23, 33, 90, 0.08); + } + .tb-link-container { + display: flex; + flex-direction: row; + align-items: center; + .tb-link-icon-container { + height: 40px; + padding: 8px; + background: #F3F6FA; + border-radius: 6px; + margin-right: 8px; + @media #{$mat-md-lg} { + display: none; + } + } + .tb-link-text { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.87); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + } + } + } + + .tb-add-link-button { + height: 55px; + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: #FFFFFF; + padding: 8px; + border: 2px dashed rgba(0, 0, 0, 0.08); + border-radius: 10px; + @media #{$mat-md-lg} { + height: 18px; + padding: 0 8px; + .tb-add-icon { + height: 12px; + width: 12px; + font-size: 12px; + } + } + .tb-add-icon { + color: rgba(0, 0, 0, 0.12); + } + &:hover { + .tb-add-icon { + color: rgba(0, 0, 0, 0.38); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html new file mode 100644 index 0000000000..0dec41e71d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html @@ -0,0 +1,91 @@ + +
+ +
+ + +
+
+ +
+ +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts new file mode 100644 index 0000000000..359ae4468d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts @@ -0,0 +1,318 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + SkipSelf, + ViewChild +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormGroupDirective, + NG_VALUE_ACCESSOR, + NgForm, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MenuService } from '@core/services/menu.service'; +import { Observable, of } from 'rxjs'; +import { MenuSection } from '@core/services/menu.models'; +import { TranslateService } from '@ngx-translate/core'; +import { catchError, distinctUntilChanged, map, publishReplay, refCount, share, switchMap, tap } from 'rxjs/operators'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-quick-link', + templateUrl: './quick-link.component.html', + styleUrls: ['./link.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => QuickLinkComponent), + multi: true + }, + {provide: ErrorStateMatcher, useExisting: QuickLinkComponent} + ] +}) +export class QuickLinkComponent extends PageComponent implements OnInit, ControlValueAccessor, ErrorStateMatcher { + + @Input() + disabled: boolean; + + @Input() + addOnly = false; + + @Input() + disableEdit = false; + + @Output() + quickLinkAdded = new EventEmitter(); + + @Output() + quickLinkAddCanceled = new EventEmitter(); + + @Output() + quickLinkUpdated = new EventEmitter(); + + @Output() + quickLinkDeleted = new EventEmitter(); + + @Output() + editModeChanged = new EventEmitter(); + + @ViewChild('linkInput', {static: false}) linkInput: ElementRef; + + filteredLinks: Observable>; + + private allLinksObservable$: Observable> = null; + + searchText = ''; + + editMode = false; + addMode = false; + + quickLink: MenuSection; + + private propagateChange = null; + + public editQuickLinkFormGroup: UntypedFormGroup; + + private submitted = false; + + private dirty = false; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + private menuService: MenuService, + public translate: TranslateService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { + super(store); + } + + ngOnInit(): void { + this.addMode = this.addOnly; + this.editQuickLinkFormGroup = this.fb.group({ + link: [null, [this.requiredLinkValidator]] + }); + this.filteredLinks = this.editQuickLinkFormGroup.get('link').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + }), + map(value => value ? (typeof value === 'string' ? value : + ((value as any).translated ? value.name : this.translate.instant(value.name))) : ''), + distinctUntilChanged(), + switchMap(name => this.fetchLinks(name) ), + share() + ); + } + + requiredLinkValidator(control: AbstractControl): ValidationErrors | null { + const value = control.value; + if (!value || typeof value === 'string') { + return {required: true}; + } + return null; + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.editQuickLinkFormGroup.disable({emitEvent: false}); + } else { + this.editQuickLinkFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string): void { + if (value) { + this.menuService.menuLinkById(value).subscribe( + (link) => { + this.quickLink = link; + this.editQuickLinkFormGroup.get('link').patchValue( + link, {emitEvent: false} + ); + } + ); + } else { + this.quickLink = null; + this.editQuickLinkFormGroup.get('link').patchValue( + value, {emitEvent: false} + ); + if (!this.editQuickLinkFormGroup.valid) { + this.addMode = true; + this.editModeChanged.emit(true); + } + } + this.dirty = true; + } + + updateView(value: MenuSection | null) { + if (this.quickLink !== value) { + this.quickLink = value; + } + } + + displayLinkFn = (link?: MenuSection): string | undefined => + link ? ((link as any).translated ? link.name : this.translate.instant(link.fullName || link.name)) : undefined; + + fetchLinks(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(100, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.getLinks(pageLink).pipe( + catchError(() => of(emptyPageData())), + map(pageData => pageData.data) + ); + } + + getLinks(pageLink: PageLink): Observable> { + return this.allLinks().pipe( + map((links) => pageLink.filterData(links)) + ); + } + + allLinks(): Observable> { + if (this.allLinksObservable$ === null) { + this.allLinksObservable$ = this.menuService.availableMenuLinks().pipe( + map((links) => { + const result = deepClone(links); + for (const link of result) { + link.name = this.translate.instant(link.fullName || link.name); + (link as any).translated = true; + } + return result; + }), + publishReplay(1), + refCount() + ); + } + return this.allLinksObservable$; + } + + onFocus() { + if (this.dirty) { + this.editQuickLinkFormGroup.get('link').updateValueAndValidity({onlySelf: true}); + this.dirty = false; + } + } + + clear() { + this.editQuickLinkFormGroup.get('link').patchValue(''); + setTimeout(() => { + this.linkInput.nativeElement.blur(); + this.linkInput.nativeElement.focus(); + }, 0); + } + + switchToEditMode() { + if (!this.disableEdit && !this.editMode) { + this.submitted = false; + this.editQuickLinkFormGroup.get('link').patchValue( + this.quickLink, {emitEvent: false} + ); + this.editMode = true; + this.editModeChanged.emit(true); + } + } + + apply() { + this.submitted = true; + this.updateModel(); + if (this.quickLink) { + this.editMode = false; + this.editModeChanged.emit(false); + this.quickLinkUpdated.next(this.quickLink.id); + } + } + + cancelEdit() { + this.submitted = false; + this.editMode = false; + this.editModeChanged.emit(false); + } + + add() { + this.submitted = true; + this.updateModel(); + if (this.quickLink) { + if (!this.addOnly) { + this.addMode = false; + this.editModeChanged.emit(false); + } + this.quickLinkAdded.next(this.quickLink.id); + } + } + + cancelAdd() { + this.editModeChanged.emit(false); + this.quickLinkAddCanceled.emit(); + } + + delete() { + this.quickLinkDeleted.emit(); + } + + isEditing() { + return this.editMode || this.addMode; + } + + private updateModel() { + if (this.quickLink) { + this.propagateChange(this.quickLink.id); + } else { + this.propagateChange(null); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html new file mode 100644 index 0000000000..95c2818f1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html @@ -0,0 +1,51 @@ + +
+
+
{{ 'widgets.quick-links.title' | translate }}
+ +
+ + + + + + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts new file mode 100644 index 0000000000..1b16601a7c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts @@ -0,0 +1,168 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Authority } from '@shared/models/authority.enum'; +import { map, Observable, of, Subscription } from 'rxjs'; +import { QuickLinks } from '@shared/models/user-settings.models'; +import { UserSettingsService } from '@core/http/user-settings.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { MatDialog } from '@angular/material/dialog'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { MenuService } from '@core/services/menu.service'; +import { MenuSection } from '@core/services/menu.models'; +import { AddQuickLinkDialogComponent } from '@home/components/widget/lib/home-page/add-quick-link-dialog.component'; +import { + EditLinksDialogComponent, + EditLinksDialogData +} from '@home/components/widget/lib/home-page/edit-links-dialog.component'; + +const defaultQuickLinksMap = new Map( + [ + [Authority.SYS_ADMIN, { + links: ['tenants', 'tenant_profiles'] + }], + [Authority.TENANT_ADMIN, { + links: ['alarms', 'dashboards', 'devices'] + }], + [Authority.CUSTOMER_USER, { + links: ['alarms', 'dashboards', 'devices'] + }] + ] +); + +interface QuickLinksWidgetSettings { + columns: number; +} + +@Component({ + selector: 'tb-quick-links-widget', + templateUrl: './quick-links-widget.component.html', + styleUrls: ['./home-page-widget.scss', './links-widget.component.scss'] +}) +export class QuickLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + @Input() + ctx: WidgetContext; + + settings: QuickLinksWidgetSettings; + columns: number; + rowHeight = '55px'; + gutterSize = '12px'; + + quickLinks: QuickLinks; + authUser = getCurrentAuthUser(this.store); + + private observeBreakpointSubscription: Subscription; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private userSettingsService: UserSettingsService, + private dialog: MatDialog, + private menuService: MenuService, + private breakpointObserver: BreakpointObserver) { + super(store); + } + + ngOnInit() { + this.settings = this.ctx.settings; + this.columns = this.settings.columns || 3; + const isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); + this.rowHeight = isMdLg ? '18px' : '55px'; + this.gutterSize = isMdLg ? '8px' : '12px'; + this.observeBreakpointSubscription = this.breakpointObserver + .observe(MediaBreakpoints['md-lg']) + .subscribe((state: BreakpointState) => { + if (state.matches) { + this.rowHeight = '18px'; + this.gutterSize = '8px'; + } else { + this.rowHeight = '55px'; + this.gutterSize = '12px'; + } + this.cd.markForCheck(); + } + ); + this.loadQuickLinks(); + } + + ngOnDestroy() { + if (this.observeBreakpointSubscription) { + this.observeBreakpointSubscription.unsubscribe(); + } + super.ngOnDestroy(); + } + + menuLinks$(): Observable> { + return this.quickLinks ? this.menuService.menuLinksByIds(this.quickLinks.links) : of([]); + } + + private loadQuickLinks() { + this.userSettingsService.getQuickLinks().pipe( + map((quickLinks) => { + if (!quickLinks || !quickLinks.links) { + return defaultQuickLinksMap.get(this.authUser.authority); + } else { + return quickLinks; + } + }) + ).subscribe( + (quickLinks) => { + this.quickLinks = quickLinks; + this.cd.markForCheck(); + } + ); + } + + edit() { + this.dialog.open(EditLinksDialogComponent, { + disableClose: true, + autoFocus: false, + data: { + mode: 'quickLinks', + links: this.quickLinks + }, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (result) => { + if (result) { + this.loadQuickLinks(); + } + }); + } + + addLink() { + this.dialog.open(AddQuickLinkDialogComponent, { + disableClose: true, + autoFocus: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (link) => { + if (link) { + this.quickLinks.links.push(link); + this.cd.markForCheck(); + this.userSettingsService.updateQuickLinks(this.quickLinks).subscribe(); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html new file mode 100644 index 0000000000..ba7614934d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html @@ -0,0 +1,117 @@ + +
+ + + + +
+ + + + + + {{ lastVisitedDashboard.starred ? 'star' : 'star_border' }} + + + + + {{ 'widgets.recent-dashboards.name' | translate }} + + + {{ lastVisitedDashboard.title }} + + + + + {{ 'widgets.recent-dashboards.last-viewed' | translate }} + + + {{ lastVisitedDashboard.lastVisited | dateAgo:{applyAgo: true} }} + + + + +
+
+ +
+
+
{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}
+
+
+
+ +
+
+
+ {{ dashboard.starred ? 'star' : 'star_border' }} +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+
{{ 'widgets.recent-dashboards.title' | translate }}
+
+
+
+
widgets.home.no-data-available
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.scss new file mode 100644 index 0000000000..29235a0d1b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.scss @@ -0,0 +1,240 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../../scss/constants"; + +:host { + .tb-card-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + @media #{$mat-lt-md} { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + } + .tb-no-dashboards { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + .tb-no-last-visited-bg { + width: 60px; + height: 60px; + margin: 10px; + position: relative; + &:before { + content: ""; + border-radius: 8px; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #305680; + -webkit-mask-image: url(/assets/home/no_data_bg.svg); + -webkit-mask-repeat: no-repeat; + mask-image: url(/assets/home/no_data_bg.svg); + mask-repeat: no-repeat; + } + } + .tb-no-last-visited-dashboards-text { + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + } + + .tb-starred-dashboard-row { + height: 40px; + display: flex; + flex-direction: row; + align-items: center; + @media #{$mat-md-lg} { + height: 34px; + } + } + + .tb-cell { + box-sizing: content-box; + &:first-child { + padding: 0 12px; + } + &:nth-child(n+2):nth-last-child(n+2) { + padding: 0 28px 0 0; + } + } + + .tb-star-dashboard-autocomplete { + margin-left: 64px; + margin-right: 64px; + @media #{$mat-md-lg} { + margin-left: 48px; + margin-right: 48px; + } + } +} + +:host ::ng-deep { + .mat-mdc-cell, .mat-mdc-header-cell, .mdc-data-table__row:last-child .mdc-data-table__cell { + border-bottom: none; + } + + .mat-mdc-table { + .mdc-data-table__row, .mdc-data-table__header-row { + height: 40px; + @media #{$mat-md-lg} { + height: 34px; + } + + &:hover { + background-color: inherit; + } + } + } + + .tb-cell:nth-child(n+2):nth-last-child(n+2) { + padding: 0 28px 0 0; + } + + .mat-mdc-row:not(.mat-row-select) .mat-mdc-cell:nth-child(n+2):nth-last-child(n+2) { + @media #{$mat-md-lg} { + padding: 0 12px 0 0; + } + } + + .mat-mdc-header-cell, .mat-mdc-cell, .tb-cell { + &.star-cell { + width: 40px; + min-width: 40px; + @media #{$mat-md-lg} { + width: 24px; + min-width: 24px; + max-width: 24px; + } + } + + &.title { + width: 100%; + } + + &.last-visited { + width: 80px; + @media #{$mat-md-lg} { + width: 50px; + } + } + } + + .mat-mdc-header-cell { + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.38); + @media #{$mat-md-lg} { + font-size: 11px; + } + } + .mat-mdc-cell.title { + max-width: 0; + } + .mat-mdc-cell, .tb-cell { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + &.title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + a { + color: rgba(0, 0, 0, 0.87); + border-bottom: none; + &:hover, &:focus { + border-bottom: none; + } + } + } + &.last-visited { + cursor: default; + color: rgba(0, 0, 0, 0.54); + } + .star { + cursor: pointer; + vertical-align: middle; + color: rgba(0, 0, 0, 0.12); + &:hover { + color: rgba(0, 0, 0, 0.18); + } + &.starred { + color: #FAE205; + &:hover { + color: #CDBD2C; + } + } + } + } + .tb-star-dashboard-autocomplete { + .mat-mdc-form-field { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.54); + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + } + .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { + padding-top: 4px; + padding-bottom: 4px; + min-height: 28px; + @media #{$mat-md-lg} { + min-height: 24px; + } + } + .mat-mdc-form-field-hint-wrapper { + height: 0; + } + .mdc-text-field--outlined .mdc-notched-outline { + .mdc-notched-outline__leading, .mdc-notched-outline__trailing { + border-top-style: dashed; + border-bottom-style: dashed; + } + .mdc-notched-outline__leading { + border-left-style: dashed; + } + .mdc-notched-outline__trailing { + border-right-style: dashed; + } + } + .mat-mdc-icon-button.mat-mdc-button-base { + width: 28px; + height: 28px; + padding: 2px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts new file mode 100644 index 0000000000..4bc6ac9982 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts @@ -0,0 +1,226 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + QueryList, ViewChild, + ViewChildren +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Authority } from '@shared/models/authority.enum'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { + AbstractUserDashboardInfo, + LastVisitedDashboardInfo, StarredDashboardInfo, + UserDashboardAction, + UserDashboardsInfo +} from '@shared/models/user-settings.models'; +import { UserSettingsService } from '@core/http/user-settings.service'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; +import { map, share } from 'rxjs/operators'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { MatSort } from '@angular/material/sort'; +import { DashboardInfo } from '@shared/models/dashboard.models'; +import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; + +@Component({ + selector: 'tb-recent-dashboards-widget', + templateUrl: './recent-dashboards-widget.component.html', + styleUrls: ['./home-page-widget.scss', './recent-dashboards-widget.component.scss'] +}) +export class RecentDashboardsWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { + + @Input() + ctx: WidgetContext; + + @ViewChildren(MatSort) lastVisitedDashboardsSort: QueryList; + + @ViewChild('starDashboardAutocomplete', {static: false}) + starDashboardAutocomplete: DashboardAutocompleteComponent; + + authority = Authority; + + userDashboardsInfo: UserDashboardsInfo; + authUser = getCurrentAuthUser(this.store); + + toggleValue: 'last' | 'starred' = 'last'; + + lastVisitedDashboardsColumns = ['starred', 'title', 'lastVisited']; + lastVisitedDashboardsDataSource: LastVisitedDashboardsDataSource; + lastVisitedDashboardsPageLink: PageLink; + + starredDashboardValue = null; + hasDashboardsAccess = true; + + dirty = false; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private userSettingService: UserSettingsService) { + super(store); + } + + ngOnInit() { + this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority); + if (this.hasDashboardsAccess) { + this.reload(); + } + } + + reload() { + this.userDashboardsInfo = null; + this.cd.markForCheck(); + (this.authUser.authority !== Authority.SYS_ADMIN ? + this.userSettingService.getUserDashboardsInfo() : of({last: [], starred: []})).subscribe( + (userDashboardsInfo) => { + this.userDashboardsInfo = userDashboardsInfo; + for (const starredDashboard of this.userDashboardsInfo?.starred) { + starredDashboard.starred = true; + } + this.userDashboardsInfo?.starred.sort((a, b) => a.starredAt - b.starredAt); + if (this.hasLastVisitedDashboards()) { + this.initLastVisitedDashboardsDataSource(); + } + this.cd.markForCheck(); + } + ); + } + + toggleValueChange(value: 'last' | 'starred') { + this.toggleValue = value; + if (this.dirty) { + this.dirty = false; + this.reload(); + } else { + if (value === 'last' && this.hasLastVisitedDashboards()) { + this.initLastVisitedDashboardsDataSource(); + } + } + } + + private initLastVisitedDashboardsDataSource() { + this.lastVisitedDashboardsDataSource = new LastVisitedDashboardsDataSource(this.userDashboardsInfo.last); + const sortOrder: SortOrder = { + property: 'lastVisited', + direction: Direction.DESC + }; + this.lastVisitedDashboardsPageLink = new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, sortOrder); + this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink); + } + + ngAfterViewInit() { + this.lastVisitedDashboardsSort.changes.subscribe(() => { + if (this.lastVisitedDashboardsSort.length) { + this.lastVisitedDashboardsSort.get(0).sortChange.subscribe(() => + this.updateLastVisitedDashboardsData(this.lastVisitedDashboardsSort.get(0))); + } + }); + } + + updateLastVisitedDashboardsData(sort: MatSort) { + this.lastVisitedDashboardsPageLink.sortOrder.property = sort.active; + this.lastVisitedDashboardsPageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()]; + this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink); + } + + hasLastVisitedDashboards(): boolean { + return !!(this.userDashboardsInfo && this.userDashboardsInfo.last && this.userDashboardsInfo.last.length); + } + + toggleDashboardStar(dashboard: AbstractUserDashboardInfo): void { + const action: UserDashboardAction = dashboard.starred ? UserDashboardAction.UNSTAR : UserDashboardAction.STAR; + dashboard.starred = !dashboard.starred; + this.userSettingService.reportUserDashboardAction(dashboard.id, action, {ignoreLoading: true}).subscribe(); + this.dirty = true; + if (this.toggleValue === 'starred') { + const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id); + if (index > -1) { + this.userDashboardsInfo.starred.splice(index, 1); + this.cd.markForCheck(); + } + } + } + + onStarDashboard(dashboard: DashboardInfo) { + if (dashboard) { + this.starDashboardAutocomplete.clear(); + const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id.id); + if (index === -1) { + const starredDashboard: StarredDashboardInfo = { + starredAt: Date.now(), + id: dashboard.id.id, + starred: true, + title: dashboard.title + }; + this.userDashboardsInfo.starred.push(starredDashboard); + this.userSettingService.reportUserDashboardAction(dashboard.id.id, UserDashboardAction.STAR, {ignoreLoading: true}).subscribe(); + } + this.cd.markForCheck(); + } + } + +} + +export class LastVisitedDashboardsDataSource implements DataSource { + + private lastVisitedDashboardsSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + constructor(private lastVisitedDashboards: Array) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.lastVisitedDashboardsSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.lastVisitedDashboardsSubject.complete(); + this.pageDataSubject.complete(); + } + + loadData(pageLink: PageLink): void { + const result = pageLink.filterData(this.lastVisitedDashboards); + this.lastVisitedDashboardsSubject.next(result.data); + this.pageDataSubject.next(result); + } + + isEmpty(): Observable { + return this.lastVisitedDashboardsSubject.pipe( + map((entities) => !entities.length), + share() + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements), + share() + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html new file mode 100644 index 0000000000..a0beeacdb2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html @@ -0,0 +1,100 @@ + +
+ + + + +
+
+
device.devices
+
asset.assets
+
user.users
+
dashboard.dashboards
+
customer.customers
+
+
+
+
{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}
+
{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}
+
{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}
+
{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}
+
{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}
+
+
+ + + + + +
+
+
+
+ +
+
+
api-usage.transport-messages
+
api-usage.javascript
+
api-usage.alarms-created
+
api-usage.email
+
api-usage.sms
+
+
+
+
{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}
+
{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}
+
{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}
+
{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}
+
{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}
+
+
+ + + + + +
+
+
+
+
+
+ +
+
{{ 'widgets.usage-info.title' | translate }}
+
+
+
+
widgets.home.no-data-available
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss new file mode 100644 index 0000000000..8d85e176a8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../../scss/constants"; + +:host { + .tb-card-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + @media #{$mat-lt-md} { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + } + + .tb-usage-list { + display: flex; + flex-direction: row; + justify-content: space-between; + overflow-y: auto; + } + + .tb-usage-items, .tb-usage-items-counts, .tb-usage-items-progress { + display: flex; + flex-direction: column; + justify-content: space-evenly; + align-items: flex-start; + overflow: hidden; + } + + .tb-usage-items-progress { + width: 34px; + @media #{$mat-md} { + display: none; + } + .mdc-linear-progress { + height: 8px; + margin-top: 6px; + margin-bottom: 6px; + border-radius: 2px; + @media #{$mat-md-lg} { + margin-top: 4px; + margin-bottom: 4px; + } + } + } + + .tb-usage-items-values { + display: flex; + flex-direction: row; + align-items: stretch; + gap: 16px; + } + + .tb-usage-item, .tb-usage-item-counts { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + &.critical { + color: #D12730; + } + } + + .tb-usage-item { + color: rgba(0, 0, 0, 0.38); + } + + .tb-usage-item-counts { + color: rgba(0, 0, 0, 0.76); + } +} + +:host ::ng-deep { + .tb-usage-items-progress { + .mat-mdc-progress-bar { + .mdc-linear-progress__bar-inner { + border-top-width: 8px; + } + &.critical { + .mdc-linear-progress__buffer-bar { + background: rgba(209, 39, 48, 0.06); + } + .mdc-linear-progress__bar-inner { + border-top-color: #D12730; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts new file mode 100644 index 0000000000..03148d501a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts @@ -0,0 +1,115 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Authority } from '@shared/models/authority.enum'; +import { of } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { UsageInfo } from '@shared/models/usage.models'; +import { UsageInfoService } from '@core/http/usage-info.service'; +import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; + +@Component({ + selector: 'tb-usage-info-widget', + templateUrl: './usage-info-widget.component.html', + styleUrls: ['./home-page-widget.scss', './usage-info-widget.component.scss'] +}) +export class UsageInfoWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + @Input() + ctx: WidgetContext; + + usageInfo: UsageInfo; + authUser = getCurrentAuthUser(this.store); + + toggleValue: 'entities' | 'apiCalls' = 'entities'; + + entityItemCritical: {[key: string]: boolean} = {}; + apiCallItemCritical: {[key: string]: boolean} = {}; + + hasUsageInfoAccess = true; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private shortNumberPipe: ShortNumberPipe, + private usageInfoService: UsageInfoService) { + super(store); + } + + ngOnInit() { + this.hasUsageInfoAccess = this.authUser.authority === Authority.TENANT_ADMIN; + if (this.hasUsageInfoAccess) { + this.usageInfoService.getUsageInfo().subscribe( + (usageInfo) => { + this.usageInfo = usageInfo; + this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices); + this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets); + this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers); + this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards); + this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers); + this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages, + this.usageInfo?.maxTransportMessages); + this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions); + this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms); + this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails); + this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms); + let entitiesHasCriticalItem = false; + let apiCallsHasCriticalItem = false; + for (const key of Object.keys(this.entityItemCritical)) { + if (this.entityItemCritical[key]) { + entitiesHasCriticalItem = true; + break; + } + } + for (const key of Object.keys(this.apiCallItemCritical)) { + if (this.apiCallItemCritical[key]) { + apiCallsHasCriticalItem = true; + break; + } + } + if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) { + this.toggleValue = 'apiCalls'; + } + this.cd.markForCheck(); + } + ); + } + } + + maxValue(max: number): number | string { + return max ? this.shortNumberPipe.transform(max) : '∞'; + } + + progressValue(value: number, max: number): number { + if (max && value) { + return (value / max) * 100; + } + return 0; + } + + private isItemCritical(value: number, max: number): boolean { + if (max && value) { + return (value / max) >= 0.85; + } else { + return false; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.html new file mode 100644 index 0000000000..fb5dde16d2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.html @@ -0,0 +1,63 @@ + + + +
+
check
+
widgets.version-info.version-is-up-to-date
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss new file mode 100644 index 0000000000..2ad903afcf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss @@ -0,0 +1,132 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../scss/constants"; + +:host { + .tb-card-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + @media #{$mat-md-lg} { + margin-bottom: 4px; + } + } + + .tb-contact-us { + @media #{$mat-md-lg} { + height: 24px; + font-size: 12px; + } + } + + .tb-version-info-container { + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + padding: 8px 16px 4px 16px; + overflow: hidden; + white-space: nowrap; + @media #{$mat-lt-md} { + padding: 16px; + } + @media #{$mat-md-lg} { + padding: 4px; + } + &.up-to-date { + box-shadow: none; + border: none; + background: #F3F6FA; + white-space: initial; + text-align: center; + } + } + + .tb-version-link { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + position: relative; + border-bottom: none; + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + &:hover, &:focus { + border-bottom: none; + } + &::after { + content: 'arrow_forward'; + display: inline-block; + transform: rotate(315deg); + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 18px; + color: rgba(0, 0, 0, 0.12); + vertical-align: bottom; + margin-left: 6px; + } + &:hover::after { + color: inherit; + } + } + + .tb-version { + font-weight: 500; + font-size: 18px; + line-height: 24px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.87); + @media #{$mat-md-lg} { + font-size: 12px; + line-height: 14px; + } + } + + .tb-upgrade { + @media #{$mat-md-lg} { + height: 24px; + font-size: 12px; + padding: 0; + overflow: hidden; + } + } + + .check-icon { + color: #198038; + font-weight: 600; + @media #{$mat-md-lg} { + font-size: 16px; + } + } + + .up-to-date-text { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts new file mode 100644 index 0000000000..00265619f0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AdminService } from '@core/http/admin.service'; +import { UpdateMessage } from '@shared/models/settings.models'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { of } from 'rxjs'; + +@Component({ + selector: 'tb-version-info', + templateUrl: './version-info.component.html', + styleUrls: ['./home-page-widget.scss', './version-info.component.scss'] +}) +export class VersionInfoComponent extends PageComponent implements OnInit { + + authUser = getCurrentAuthUser(this.store); + updateMessage: UpdateMessage; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private adminService: AdminService) { + super(store); + } + + ngOnInit() { + (this.authUser.authority === Authority.SYS_ADMIN ? + this.adminService.checkUpdates() : of(null)).subscribe( + (updateMessage) => { + this.updateMessage = updateMessage; + this.cd.markForCheck(); + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html index 3bd3ece201..24fe07d92b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'legend.min' | translate }}{{ 'legend.max' | translate }}{{ 'legend.avg' | translate }}{{ 'legend.total' | translate }}{{ 'legend.latest' | translate }}
+ {{ legendKey.dataKey.label }} + {{ legendData.data[legendKey.dataIndex].min }}{{ legendData.data[legendKey.dataIndex].max }}{{ legendData.data[legendKey.dataIndex].avg }}{{ legendData.data[legendKey.dataIndex].total }}{{ legendData.data[legendKey.dataIndex].latest }}
+ + + {{ legendKey.dataKey.label }} + +
{{ 'legend.min' | translate }} + {{ legendData.data[legendKey.dataIndex].min }} +
{{ 'legend.max' | translate }} + {{ legendData.data[legendKey.dataIndex].max }} +
{{ 'legend.avg' | translate }} + {{ legendData.data[legendKey.dataIndex].avg }} +
{{ 'legend.total' | translate }} + {{ legendData.data[legendKey.dataIndex].total }} +
{{ 'legend.latest' | translate }} + {{ legendData.data[legendKey.dataIndex].latest }} +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss new file mode 100644 index 0000000000..ae61340873 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + table.tb-legend { + font-size: 12px; + + &.tb-legend-column { + width: 100%; + } + + &.tb-legend-row { + width: auto; + margin-left: auto; + margin-right: auto; + } + + .tb-legend-header, + .tb-legend-value { + text-align: right; + } + + .tb-legend-header { + th { + padding: 0 10px 1px 0; + color: rgb(255, 110, 64); + white-space: nowrap; + font-size: 12px; + } + } + + .tb-legend-keys { + td { + font-size: 12px; + } + td.tb-legend-label, + td.tb-legend-value { + padding: 2px 10px; + white-space: nowrap; + } + + .tb-legend-type { + min-width: 50px; + padding: 0 10px 1px 0; + font-weight: 700; + color: rgb(255, 110, 64); + text-align: right; + white-space: nowrap; + } + + .tb-legend-line { + display: inline-block; + width: 15px; + height: 3px; + vertical-align: middle; + } + + .tb-legend-label { + padding: 2px 10px; + text-align: left; + white-space: nowrap; + outline: none; + + &.tb-horizontal { + width: 95%; + } + + &.tb-hidden-label { + text-decoration: line-through; + opacity: .6; + } + } + + &.tb-row-direction { + display: inline-block; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts new file mode 100644 index 0000000000..e4dd8450fc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts @@ -0,0 +1,73 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { LegendConfig, LegendData, LegendDirection, LegendKey, LegendPosition } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-legend', + templateUrl: './legend.component.html', + styleUrls: ['./legend.component.scss'] +}) +export class LegendComponent implements OnInit { + + @Input() + legendConfig: LegendConfig; + + @Input() + legendData: LegendData; + + @Output() + legendKeyHiddenChange = new EventEmitter(); + + displayHeader: boolean; + + isHorizontal: boolean; + + isRowDirection: boolean; + + ngOnInit(): void { + this.displayHeader = this.legendConfig.showMin === true || + this.legendConfig.showMax === true || + this.legendConfig.showAvg === true || + this.legendConfig.showTotal === true || + this.legendConfig.showLatest === true; + + this.isHorizontal = this.legendConfig.position === LegendPosition.bottom || + this.legendConfig.position === LegendPosition.top; + + this.isRowDirection = this.legendConfig.direction === LegendDirection.row; + } + + toggleHideData(index: number) { + const dataKey = this.legendData.keys.find(key => key.dataIndex === index).dataKey; + if (!dataKey.settings.disableDataHiding) { + dataKey.hidden = !dataKey.hidden; + this.legendKeyHiddenChange.emit(index); + } + } + + legendKeys(): LegendKey[] { + try { + let keys = this.legendData.keys; + if (this.legendConfig.sortDataKeys) { + keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); + } + return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend); + } catch (e) {} + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts index d50a62bc9a..0eaf3ed5c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts index c9a12d68d9..934f9be6fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html index de34d1e01b..99740ac601 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html @@ -1,6 +1,6 @@ - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index d76e43d765..85265b8f4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, HostBinding, Inject, Input, OnInit, Type } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, Input, OnInit, Type } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { WidgetContext } from '@home/models/widget-component.models'; import { Store } from '@ngrx/store'; @@ -25,27 +25,29 @@ import { createLabelFromPattern, flatDataWithoutOverride, formattedDataFormDatasourceData, - hashCode, isDefinedAndNotNull, + hashCode, + isDefinedAndNotNull, isNotEmptyStr, parseFunction, safeExecute } from '@core/utils'; import cssjs from '@core/css/css'; import { UtilsService } from '@core/services/utils.service'; -import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; +import { HOME_COMPONENTS_MODULE_TOKEN, WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { EntityDataPageLink } from '@shared/models/query/query.models'; interface MarkdownWidgetSettings { markdownTextPattern: string; useMarkdownTextFunction: boolean; markdownTextFunction: string; + applyDefaultMarkdownStyle: boolean; markdownCss: string; } -type MarkdownTextFunction = (data: FormattedData[]) => string; +type MarkdownTextFunction = (data: FormattedData[], ctx: WidgetContext) => string; @Component({ - selector: 'tb-markdown-widget ', + selector: 'tb-markdown-widget', templateUrl: './markdown-widget.component.html' }) export class MarkdownWidgetComponent extends PageComponent implements OnInit { @@ -53,7 +55,6 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { settings: MarkdownWidgetSettings; markdownTextFunction: MarkdownTextFunction; - @HostBinding('class') markdownClass: string; @Input() @@ -61,10 +62,14 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { markdownText: string; + additionalStyles: string[]; + + applyDefaultMarkdownStyle = true; constructor(protected store: Store, private utils: UtilsService, @Inject(HOME_COMPONENTS_MODULE_TOKEN) public homeComponentsModule: Type, + @Inject(WIDGET_COMPONENTS_MODULE_TOKEN) public widgetComponentsModule: Type, private cd: ChangeDetectorRef) { super(store); } @@ -72,15 +77,20 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { ngOnInit(): void { this.ctx.$scope.markdownWidget = this; this.settings = this.ctx.settings; - this.markdownTextFunction = this.settings.useMarkdownTextFunction ? parseFunction(this.settings.markdownTextFunction, ['data']) : null; - this.markdownClass = 'markdown-widget'; - const cssString = this.settings.markdownCss; + this.markdownTextFunction = this.settings.useMarkdownTextFunction ? + parseFunction(this.settings.markdownTextFunction, ['data', 'ctx']) : null; + let cssString = this.settings.markdownCss; if (isNotEmptyStr(cssString)) { const cssParser = new cssjs(); - cssParser.testMode = false; - this.markdownClass += '-' + hashCode(cssString); + this.markdownClass = 'markdown-widget-' + hashCode(cssString); cssParser.cssPreviewNamespace = this.markdownClass; - cssParser.createStyleElement(this.markdownClass, cssString); + cssParser.testMode = false; + cssString = cssParser.applyNamespacing(cssString); + cssString = cssParser.getCSSForEditor(cssString); + this.additionalStyles = [cssString]; + } + if (isDefinedAndNotNull(this.settings.applyDefaultMarkdownStyle)) { + this.applyDefaultMarkdownStyle = this.settings.applyDefaultMarkdownStyle; } const pageSize = isDefinedAndNotNull(this.ctx.widgetConfig.pageSize) && this.ctx.widgetConfig.pageSize > 0 ? this.ctx.widgetConfig.pageSize : 16384; @@ -117,7 +127,7 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { } const data = formattedDataFormDatasourceData(initialData); let markdownText = this.settings.useMarkdownTextFunction ? - safeExecute(this.markdownTextFunction, [data]) : this.settings.markdownTextPattern; + safeExecute(this.markdownTextFunction, [data, this.ctx]) : this.settings.markdownTextPattern; const allData: FormattedData = flatDataWithoutOverride(data); markdownText = createLabelFromPattern(markdownText, allData); if (this.markdownText !== markdownText) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html index 550f7cde49..0a757fd34f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -1,6 +1,6 @@ - + - {{section.name}} + {{section.name}} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss index 08c26c6136..f3710fbc81 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,23 +22,23 @@ :host ::ng-deep { .tb-navigation-cards { - .mat-headline { + .mat-headline-5 { font-size: 20px; @media #{$mat-gt-xmd} { font-size: 24px; } } - mat-card { + .mat-mdc-card { padding: 0; margin: 8px; - mat-card-title { + .mat-mdc-card-title { margin: 0; padding: 24px 16px 16px; } - mat-card-title+mat-card-content { + .mat-mdc-card-title+.mat-mdc-card-content { padding-top: 0; } - mat-card-content { + .mat-mdc-card-content { padding: 16px; } } @@ -52,28 +52,25 @@ &:focus { border-bottom: none; } - .mat-button-wrapper { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - mat-icon { - margin: auto; - } - span { - height: 18px; - min-height: 36px; - max-height: 36px; - padding: 0 0 20px 0; - margin: auto; - font-size: 18px; - font-weight: 400; - line-height: 18px; - white-space: normal; - } + display: flex; + flex-direction: column; + align-items: center; + mat-icon { + margin: auto; + } + span.mdc-button__label { + height: 18px; + min-height: 36px; + max-height: 36px; + padding: 0 0 20px 0; + margin: auto; + font-size: 18px; + font-weight: 400; + line-height: 18px; + white-space: normal; + text-align: center; } - &.mat-raised-button.mat-primary { + &.mat-mdc-raised-button.mat-primary { .mat-ripple-element { opacity: 0.3; background-color: rgba(255, 255, 255, 0.3); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts index 03e0eda216..13a2231c15 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html index 78db5300b3..8b98e45bb8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html @@ -1,6 +1,6 @@ -
- - widgets.simple-card.label-position - - - {{ 'widgets.simple-card.label-position-left' | translate }} - - - {{ 'widgets.simple-card.label-position-top' | translate }} - - - -
+
+
widgets.simple-card.label
+
+
widgets.simple-card.label-position
+ + + + {{ 'widgets.simple-card.label-position-left' | translate }} + + + {{ 'widgets.simple-card.label-position-top' | translate }} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts index d690c4f434..4ecd21ad1d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -27,14 +27,14 @@ import { AppState } from '@core/core.state'; }) export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent { - simpleCardWidgetSettingsForm: FormGroup; + simpleCardWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.simpleCardWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html index f8a2b852d2..2dbedf96bd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html @@ -1,6 +1,6 @@ -
, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.flotBarKeySettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.html index b91b716ce3..095c80727e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.html @@ -1,6 +1,6 @@ -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.ts index 285c339edf..f60b37f490 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { flotDefaultSettings } from '@home/components/widget/lib/settings/chart/flot-widget-settings.component'; @@ -28,14 +28,14 @@ import { flotDefaultSettings } from '@home/components/widget/lib/settings/chart/ }) export class FlotBarWidgetSettingsComponent extends WidgetSettingsComponent { - flotBarWidgetSettingsForm: FormGroup; + flotBarWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.flotBarWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html index 5741bfae9f..2bebfeef8a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html @@ -1,6 +1,6 @@ -
, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.flotLineKeySettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.html index 299627f9e2..ca06d3987d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.html @@ -1,6 +1,6 @@ -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.ts index d2b0e76ca6..7cd2056683 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { flotDefaultSettings } from '@home/components/widget/lib/settings/chart/flot-widget-settings.component'; @@ -29,14 +29,14 @@ import { deepClone } from '@core/utils'; }) export class FlotLineWidgetSettingsComponent extends WidgetSettingsComponent { - flotLineWidgetSettingsForm: FormGroup; + flotLineWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.flotLineWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-key-settings.component.html index 8649e5e97d..0951ad6b8a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-key-settings.component.html @@ -1,6 +1,6 @@ +
+
+ + legend.direction + + + {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} + + + + + legend.position + + + {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} + + + +
+
+ + {{ 'legend.show-min' | translate }} + + + {{ 'legend.show-max' | translate }} + +
+
+ + {{ 'legend.show-avg' | translate }} + + + {{ 'legend.show-total' | translate }} + +
+
+ + {{ 'legend.show-latest' | translate }} + + + {{ 'legend.sort-legend' | translate }} + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts new file mode 100644 index 0000000000..c8de7872e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts @@ -0,0 +1,138 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { isDefined } from '@core/utils'; +import { + LegendConfig, + LegendDirection, + legendDirectionTranslationMap, + LegendPosition, + legendPositionTranslationMap +} from '@shared/models/widget.models'; +import { Subscription } from 'rxjs'; + +// @dynamic +@Component({ + selector: 'tb-legend-config', + templateUrl: './legend-config.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LegendConfigComponent), + multi: true + } + ] +}) +export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { + + @Input() disabled: boolean; + + legendConfigForm: UntypedFormGroup; + legendDirection = LegendDirection; + legendDirections = Object.keys(LegendDirection); + legendDirectionTranslations = legendDirectionTranslationMap; + legendPosition = LegendPosition; + legendPositions = Object.keys(LegendPosition); + legendPositionTranslations = legendPositionTranslationMap; + + private legendSettingsFormChanges$: Subscription; + private legendSettingsFormDirectionChanges$: Subscription; + private propagateChange = (_: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit(): void { + this.legendConfigForm = this.fb.group({ + direction: [null, []], + position: [null, []], + sortDataKeys: [null, []], + showMin: [null, []], + showMax: [null, []], + showAvg: [null, []], + showTotal: [null, []], + showLatest: [null, []] + }); + this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges + .subscribe((direction: LegendDirection) => { + this.onDirectionChanged(direction); + }); + this.legendSettingsFormChanges$ = this.legendConfigForm.valueChanges.subscribe( + () => this.legendConfigUpdated() + ); + } + + private onDirectionChanged(direction: LegendDirection) { + if (direction === LegendDirection.row) { + let position: LegendPosition = this.legendConfigForm.get('position').value; + if (position !== LegendPosition.bottom && position !== LegendPosition.top) { + position = LegendPosition.bottom; + } + this.legendConfigForm.patchValue({position}, {emitEvent: false} + ); + } + } + + ngOnDestroy(): void { + if (this.legendSettingsFormDirectionChanges$) { + this.legendSettingsFormDirectionChanges$.unsubscribe(); + this.legendSettingsFormDirectionChanges$ = null; + } + if (this.legendSettingsFormChanges$) { + this.legendSettingsFormChanges$.unsubscribe(); + this.legendSettingsFormChanges$ = null; + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.legendConfigForm.disable({emitEvent: false}); + } else { + this.legendConfigForm.enable({emitEvent: false}); + } + } + + writeValue(legendConfig: LegendConfig): void { + if (legendConfig) { + this.legendConfigForm.patchValue({ + direction: legendConfig.direction, + position: legendConfig.position, + sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false, + showMin: isDefined(legendConfig.showMin) ? legendConfig.showMin : false, + showMax: isDefined(legendConfig.showMax) ? legendConfig.showMax : false, + showAvg: isDefined(legendConfig.showAvg) ? legendConfig.showAvg : false, + showTotal: isDefined(legendConfig.showTotal) ? legendConfig.showTotal : false, + showLatest: isDefined(legendConfig.showLatest) ? legendConfig.showLatest : false + }, {emitEvent: false}); + } + this.onDirectionChanged(legendConfig.direction); + } + + private legendConfigUpdated() { + this.propagateChange(this.legendConfigForm.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html index c454ef1af0..46e3f87664 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html @@ -1,6 +1,6 @@
-
+
widgets.widget-font.font-family @@ -26,7 +26,7 @@
-
+
widgets.widget-font.font-style @@ -68,7 +68,7 @@
-
+
, private translate: TranslateService, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html index d60481fc8c..dc28e8f922 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html @@ -1,6 +1,6 @@ -
widgets.rpc.common-settings @@ -30,7 +29,7 @@ widgets.rpc.initial-value -
+
widgets.rpc.min-value diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts index dae3045110..59c1658e6b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -27,14 +27,14 @@ import { AppState } from '@core/core.state'; }) export class KnobControlWidgetSettingsComponent extends WidgetSettingsComponent { - knobControlWidgetSettingsForm: FormGroup; + knobControlWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.knobControlWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html index c4e934eabe..393acdb538 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html @@ -1,6 +1,6 @@ -
widgets.rpc.common-settings @@ -31,7 +30,7 @@
widgets.rpc.value-settings - + {{ 'widgets.rpc.initial-value' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts index 8ee8ebe127..a50db04103 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetService } from '@core/http/widget.service'; @@ -41,15 +41,15 @@ export class LedIndicatorWidgetSettingsComponent extends WidgetSettingsComponent dataKeyType = DataKeyType; - ledIndicatorWidgetSettingsForm: FormGroup; + ledIndicatorWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, private widgetService: WidgetService, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.ledIndicatorWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html index 47b3ef545d..34105fa0e9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html @@ -1,6 +1,6 @@ -
widgets.rpc.common-settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts index cf7ee3bbb9..5b44d1b7f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { switchRpcDefaultSettings } from '@home/components/widget/lib/settings/control/switch-rpc-settings.component'; @@ -29,10 +29,10 @@ import { deepClone } from '@core/utils'; }) export class RoundSwitchWidgetSettingsComponent extends WidgetSettingsComponent { - roundSwitchWidgetSettingsForm: FormGroup; + roundSwitchWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } @@ -44,7 +44,7 @@ export class RoundSwitchWidgetSettingsComponent extends WidgetSettingsComponent return null; } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.roundSwitchWidgetSettingsForm; } @@ -72,8 +72,8 @@ export class RoundSwitchWidgetSettingsComponent extends WidgetSettingsComponent protected prepareOutputSettings(settings: any): WidgetSettings { return { - title: settings.title, - ...settings.switchRpcSettings + ...settings.switchRpcSettings, + title: settings.title }; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html index a8153c68fe..74e782c901 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html @@ -1,6 +1,6 @@ -
widgets.rpc.rpc-settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts index d901ad2636..849c8d415a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -27,14 +27,14 @@ import { AppState } from '@core/core.state'; }) export class RpcShellWidgetSettingsComponent extends WidgetSettingsComponent { - rpcShellWidgetSettingsForm: FormGroup; + rpcShellWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.rpcShellWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html index 79c34bfdea..445e69394f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html @@ -1,6 +1,6 @@ -
widgets.rpc.rpc-settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts index 906c0c71b3..60fa562558 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -27,14 +27,14 @@ import { AppState } from '@core/core.state'; }) export class RpcTerminalWidgetSettingsComponent extends WidgetSettingsComponent { - rpcTerminalWidgetSettingsForm: FormGroup; + rpcTerminalWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.rpcTerminalWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html index c36a21c349..aa67e3d834 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html @@ -1,6 +1,6 @@ -
widgets.rpc.common-settings @@ -30,7 +29,7 @@
widgets.rpc.rpc-settings - + {{ 'widgets.rpc.is-one-way-command' | translate }} @@ -47,7 +46,7 @@ widgets.rpc.request-timeout - + {{ 'widgets.rpc.show-rpc-error' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts index 051b780ca9..16bb854edd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ContentType } from '@shared/models/constants'; @@ -28,16 +28,16 @@ import { ContentType } from '@shared/models/constants'; }) export class SendRpcWidgetSettingsComponent extends WidgetSettingsComponent { - sendRpcWidgetSettingsForm: FormGroup; + sendRpcWidgetSettingsForm: UntypedFormGroup; contentTypes = ContentType; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.sendRpcWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html index 37a15b90f4..8d7f4243b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html @@ -1,6 +1,6 @@ -
widgets.rpc.common-settings @@ -23,7 +22,7 @@ widgets.rpc.slide-toggle-label -
+
widgets.rpc.label-position diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts index f95473dbb9..e89d6a9d6b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { switchRpcDefaultSettings } from '@home/components/widget/lib/settings/control/switch-rpc-settings.component'; @@ -29,10 +29,10 @@ import { deepClone } from '@core/utils'; }) export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent { - slideToggleWidgetSettingsForm: FormGroup; + slideToggleWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } @@ -44,7 +44,7 @@ export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent return null; } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.slideToggleWidgetSettingsForm; } @@ -78,10 +78,10 @@ export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent protected prepareOutputSettings(settings: any): WidgetSettings { return { + ...settings.switchRpcSettings, title: settings.title, labelPosition: settings.labelPosition, - sliderColor: settings.sliderColor, - ...settings.switchRpcSettings + sliderColor: settings.sliderColor }; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html index da0b4237db..5556540f4d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html @@ -1,6 +1,6 @@ -
widgets.rpc.common-settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts index 65f09f2dd8..fd6ebec6cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { switchRpcDefaultSettings } from '@home/components/widget/lib/settings/control/switch-rpc-settings.component'; @@ -29,10 +29,10 @@ import { deepClone } from '@core/utils'; }) export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponent { - switchControlWidgetSettingsForm: FormGroup; + switchControlWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } @@ -44,7 +44,7 @@ export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponen return null; } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.switchControlWidgetSettingsForm; } @@ -75,9 +75,9 @@ export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponen protected prepareOutputSettings(settings: any): WidgetSettings { return { + ...settings.switchRpcSettings, title: settings.title, - showOnOffLabels: settings.showOnOffLabels, - ...settings.switchRpcSettings + showOnOffLabels: settings.showOnOffLabels }; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html index ec2228cc78..225ec4399e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html @@ -1,6 +1,6 @@ -
widgets.rpc.common-settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts index 6689112bf7..0397ca2bf5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ContentType } from '@shared/models/constants'; @@ -28,16 +28,16 @@ import { ContentType } from '@shared/models/constants'; }) export class UpdateDeviceAttributeWidgetSettingsComponent extends WidgetSettingsComponent { - updateDeviceAttributeWidgetSettingsForm: FormGroup; + updateDeviceAttributeWidgetSettingsForm: UntypedFormGroup; contentTypes = ContentType; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.updateDeviceAttributeWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html index 519ef6bafa..2acd3ad1f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html @@ -1,6 +1,6 @@ +
+ + widgets.documentation.columns + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts new file mode 100644 index 0000000000..7e0205a1e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-doc-links-widget-settings', + templateUrl: './doc-links-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class DocLinksWidgetSettingsComponent extends WidgetSettingsComponent { + + docLinksWidgetSettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.docLinksWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + columns: 3 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.docLinksWidgetSettingsForm = this.fb.group({ + columns: [settings.columns, [Validators.required, Validators.min(1), Validators.max(20)]] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html new file mode 100644 index 0000000000..f2936d7bba --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html @@ -0,0 +1,23 @@ + +
+ + widgets.quick-links.columns + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts new file mode 100644 index 0000000000..af22e4e06b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-quick-links-widget-settings', + templateUrl: './quick-links-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class QuickLinksWidgetSettingsComponent extends WidgetSettingsComponent { + + quickLinksWidgetSettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.quickLinksWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + columns: 3 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.quickLinksWidgetSettingsForm = this.fb.group({ + columns: [settings.columns, [Validators.required, Validators.min(1), Validators.max(20)]] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.html index aa440982ab..3ef9a55e78 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.html @@ -1,6 +1,6 @@ -
, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.updateDateAttributeWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html index be0c4a2b3b..512ccc9f8f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html @@ -1,6 +1,6 @@ -
widgets.input-widgets.double-field-settings -
+
widgets.input-widgets.min-value diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts index ef71992aef..20e747b52a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { deepClone } from '@core/utils'; @@ -31,14 +31,14 @@ import { }) export class UpdateDoubleAttributeWidgetSettingsComponent extends WidgetSettingsComponent { - updateDoubleAttributeWidgetSettingsForm: FormGroup; + updateDoubleAttributeWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.updateDoubleAttributeWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html index ee86c62870..1e570ae27e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html @@ -1,6 +1,6 @@ -
widgets.input-widgets.integer-field-settings -
+
widgets.input-widgets.min-value diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts index d06601b468..46ce869e5f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { deepClone } from '@core/utils'; @@ -31,14 +31,14 @@ import { }) export class UpdateIntegerAttributeWidgetSettingsComponent extends WidgetSettingsComponent { - updateIntegerAttributeWidgetSettingsForm: FormGroup; + updateIntegerAttributeWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.updateIntegerAttributeWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html index fd76c980ac..732b98dc05 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html @@ -1,6 +1,6 @@ -
widgets.input-widgets.text-field-settings -
+
widgets.input-widgets.min-length diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts index 13dd1ec453..1328d4b0eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { deepClone } from '@core/utils'; @@ -31,14 +31,14 @@ import { }) export class UpdateStringAttributeWidgetSettingsComponent extends WidgetSettingsComponent { - updateStringAttributeWidgetSettingsForm: FormGroup; + updateStringAttributeWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } - protected settingsForm(): FormGroup { + protected settingsForm(): UntypedFormGroup { return this.updateStringAttributeWidgetSettingsForm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html index 827fb44129..d7ddd5da42 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html @@ -1,6 +1,6 @@
- +
-
@@ -59,18 +60,18 @@ - + - +
- +
+
+
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.scss new file mode 100644 index 0000000000..d379c9ff8a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.scss @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.trip-animation-widget { + position: relative; + width: 100%; + height: 100%; + font-size: 16px; + line-height: 24px; + display: flex; + flex-direction: column; + + .trip-animation-label-container { + height: 24px; + } + + .trip-animation-container { + position: relative; + z-index: 1; + flex: 1; + width: 100%; + + .map { + width: 100%; + height: 100%; + } + + .trip-animation-info-panel { + position: absolute; + top: 0; + right: 0; + pointer-events: none; + + .tooltip-button { + top: 0; + left: 0; + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; + margin: 2px; + line-height: 24px; + z-index: 999; + + mat-icon { + width: 24px; + height: 24px; + + svg { + width: inherit; + height: inherit; + } + } + } + } + + .trip-animation-tooltip { + display: flex; + overflow: auto; + max-height: 90%; + position: absolute; + top: 30px; + right: 0; + z-index: 1000; + padding: 5px; + background-color: #fff; + transition: 0.3s ease-in-out; + + &-hidden { + transform: translateX(110%); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts new file mode 100644 index 0000000000..c2770c6479 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts @@ -0,0 +1,339 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import _ from 'lodash'; +import tinycolor from 'tinycolor2'; + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + SecurityContext, + ViewChild +} from '@angular/core'; +import { + defaultTripAnimationSettings, + MapProviders, + WidgetUnitedTripAnimationSettings +} from '@home/components/widget/lib/maps/map-models'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { WidgetContext } from '@app/modules/home/models/widget-component.models'; +import { + findAngle, + getRatio, + interpolateOnLineSegment, + parseWithTranslation +} from '@home/components/widget/lib/maps/common-maps-utils'; +import { FormattedData, WidgetConfig } from '@shared/models/widget.models'; +import moment from 'moment'; +import { + formattedDataArrayFromDatasourceData, + formattedDataFormDatasourceData, + isDefined, + isUndefined, + mergeFormattedData, + parseFunction, + safeExecute +} from '@core/utils'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { MapWidgetInterface } from '@home/components/widget/lib/maps/map-widget.interface'; + +interface DataMap { + [key: string]: FormattedData; +} + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'trip-animation', + templateUrl: './trip-animation.component.html', + styleUrls: ['./trip-animation.component.scss'] +}) +export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy { + + private mapResize$: ResizeObserver; + + constructor(private cd: ChangeDetectorRef, private sanitizer: DomSanitizer) { } + + @Input() ctx: WidgetContext; + + @ViewChild('map') mapContainer; + + mapWidget: MapWidgetInterface; + historicalData: FormattedData[][]; + normalizationStep: number; + interpolatedTimeData: {[time: number]: FormattedData}[] = []; + formattedInterpolatedTimeData: FormattedData[][] = []; + formattedCurrentPosition: FormattedData[] = []; + formattedLatestData: FormattedData[] = []; + widgetConfig: WidgetConfig; + settings: WidgetUnitedTripAnimationSettings; + mainTooltips = []; + visibleTooltip = false; + activeTrip: FormattedData; + label: SafeHtml; + minTime: number; + maxTime: number; + anchors: number[] = []; + useAnchors: boolean; + currentTime: number; + + ngOnInit(): void { + this.widgetConfig = this.ctx.widgetConfig; + this.settings = { + buttonColor: tinycolor(this.widgetConfig.color).setAlpha(0.54).toRgbString(), + ...defaultTripAnimationSettings, + ...this.ctx.settings + }; + this.useAnchors = this.settings.showPoints && this.settings.usePointAsAnchor; + this.settings.parsedPointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']); + this.settings.parsedTooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']); + this.settings.parsedLabelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']); + this.settings.parsedColorPointFunction = parseFunction(this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex']); + this.normalizationStep = this.settings.normalizationStep; + const subscription = this.ctx.defaultSubscription; + subscription.callbacks.onDataUpdated = () => { + this.historicalData = formattedDataArrayFromDatasourceData(this.ctx.data).map( + item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length); + this.interpolatedTimeData.length = 0; + this.formattedInterpolatedTimeData.length = 0; + const prevMinTime = this.minTime; + const prevMaxTime = this.maxTime; + this.calculateIntervals(); + const currentTime = this.calculateCurrentTime(prevMinTime, prevMaxTime); + if (currentTime !== this.currentTime) { + this.timeUpdated(currentTime); + } + this.mapWidget.map.map?.invalidateSize(); + this.mapWidget.map.setLoading(false); + this.cd.detectChanges(); + }; + subscription.callbacks.onLatestDataUpdated = () => { + this.formattedLatestData = formattedDataFormDatasourceData(this.ctx.latestData); + this.updateCurrentData(); + }; + } + + ngAfterViewInit() { + import('@home/components/widget/lib/maps/map-widget2').then( + (mod) => { + this.mapWidget = new mod.MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement); + this.mapResize$ = new ResizeObserver(() => { + this.mapWidget.resize(); + }); + this.mapResize$.observe(this.mapContainer.nativeElement); + } + ); + } + + ngOnDestroy() { + if (this.mapResize$) { + this.mapResize$.disconnect(); + } + } + + timeUpdated(time: number) { + this.currentTime = time; + // get point for each datasource associated with time + this.formattedCurrentPosition = this.interpolatedTimeData + .map(dataSource => dataSource[time]); + for (let j = 0; j < this.interpolatedTimeData.length; j++) { + if (isUndefined(this.formattedCurrentPosition[j])) { + const timePoints = Object.keys(this.interpolatedTimeData[j]).map(item => parseInt(item, 10)); + for (let i = 1; i < timePoints.length; i++) { + if (timePoints[i - 1] < time && timePoints[i] > time) { + const beforePosition = this.interpolatedTimeData[j][timePoints[i - 1]]; + const afterPosition = this.interpolatedTimeData[j][timePoints[i]]; + const ratio = getRatio(timePoints[i - 1], timePoints[i], time); + this.formattedCurrentPosition[j] = { + ...beforePosition, + time, + ...interpolateOnLineSegment(beforePosition, afterPosition, this.settings.latKeyName, this.settings.lngKeyName, ratio) + }; + break; + } + } + } + } + for (let j = 0; j < this.interpolatedTimeData.length; j++) { + if (isUndefined(this.formattedCurrentPosition[j])) { + this.formattedCurrentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time); + } + } + this.updateCurrentData(); + } + + private updateCurrentData() { + let currentPosition = this.formattedCurrentPosition; + if (this.formattedLatestData.length) { + currentPosition = mergeFormattedData(this.formattedCurrentPosition, this.formattedLatestData); + } + this.calcLabel(currentPosition); + this.calcMainTooltip(currentPosition); + if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) { + this.mapWidget.map.updateFromData(true, currentPosition, this.formattedInterpolatedTimeData, (trip) => { + this.activeTrip = trip; + this.timeUpdated(this.currentTime); + this.cd.markForCheck(); + }); + if (this.settings.showPoints) { + this.mapWidget.map.updatePoints(this.formattedInterpolatedTimeData, this.calcTooltip); + } + } + } + + setActiveTrip() { + } + + private calculateLastPoints(dataSource: DataMap, time: number): FormattedData { + const timeArr = Object.keys(dataSource); + let index = timeArr.findIndex((dtime) => { + return Number(dtime) >= time; + }); + + if (index !== -1) { + if (Number(timeArr[index]) !== time && index !== 0) { + index--; + } + } else { + index = timeArr.length - 1; + } + + return dataSource[timeArr[index]]; + } + + calculateIntervals() { + let minTime = Infinity; + let maxTime = -Infinity; + this.historicalData.forEach((dataSource) => { + minTime = Math.min(dataSource[0].time, minTime); + maxTime = Math.max(dataSource[dataSource.length - 1].time, maxTime); + }); + this.minTime = minTime; + this.maxTime = maxTime; + this.historicalData.forEach((dataSource, index) => { + this.interpolatedTimeData[index] = this.interpolateArray(dataSource); + }); + this.formattedInterpolatedTimeData = this.interpolatedTimeData.map(ds => _.values(ds)); + if (!this.activeTrip) { + this.activeTrip = this.interpolatedTimeData.map(dataSource => dataSource[this.minTime]).filter(ds => ds)[0]; + } + if (this.useAnchors) { + const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]); + this.anchors = anchorDate + .filter((data: [string, FormattedData], tsIndex) => safeExecute(this.settings.parsedPointAsAnchorFunction, [data[1], + this.formattedInterpolatedTimeData.map(ds => ds[tsIndex]), data[1].dsIndex])) + .map(data => parseInt(data[0], 10)); + } + } + + calcTooltip = (point: FormattedData, points: FormattedData[]): string => { + const data = point ? point : this.activeTrip; + const tooltipPattern: string = this.settings.useTooltipFunction ? + safeExecute(this.settings.parsedTooltipFunction, + [data, points, point.dsIndex]) : this.settings.tooltipPattern; + return parseWithTranslation.parseTemplate(tooltipPattern, data, true); + } + + private calcMainTooltip(points: FormattedData[]): void { + const tooltips = []; + for (const point of points) { + tooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, this.calcTooltip(point, points))); + } + this.mainTooltips = tooltips; + } + + calcLabel(points: FormattedData[]) { + if (this.activeTrip) { + const data = points[this.activeTrip.dsIndex]; + const labelText: string = this.settings.useLabelFunction ? + safeExecute(this.settings.parsedLabelFunction, [data, points, data.dsIndex]) : this.settings.label; + this.label = this.sanitizer.bypassSecurityTrustHtml(parseWithTranslation.parseTemplate(labelText, data, true)); + } + } + + private interpolateArray(originData: FormattedData[]): {[time: number]: FormattedData} { + const result: {[time: number]: FormattedData} = {}; + const latKeyName = this.settings.latKeyName; + const lngKeyName = this.settings.lngKeyName; + for (const data of originData) { + const currentTime = data.time; + const normalizeTime = this.minTime + Math.ceil((currentTime - this.minTime) / this.normalizationStep) * this.normalizationStep; + result[normalizeTime] = { + ...data, + minTime: this.minTime !== Infinity ? moment(this.minTime).format('YYYY-MM-DD HH:mm:ss') : '', + maxTime: this.maxTime !== -Infinity ? moment(this.maxTime).format('YYYY-MM-DD HH:mm:ss') : '', + rotationAngle: this.settings.rotationAngle + }; + } + const timeStamp = Object.keys(result); + for (let i = 0; i < timeStamp.length - 1; i++) { + if (isUndefined(result[timeStamp[i + 1]][latKeyName]) || isUndefined(result[timeStamp[i + 1]][lngKeyName])) { + for (let j = i + 2; j < timeStamp.length - 1; j++) { + if (isDefined(result[timeStamp[j]][latKeyName]) || isDefined(result[timeStamp[j]][lngKeyName])) { + const ratio = getRatio(Number(timeStamp[i]), Number(timeStamp[j]), Number(timeStamp[i + 1])); + result[timeStamp[i + 1]] = { + ...interpolateOnLineSegment(result[timeStamp[i]], result[timeStamp[j]], latKeyName, lngKeyName, ratio), + ...result[timeStamp[i + 1]], + }; + break; + } + } + } + result[timeStamp[i]].rotationAngle += findAngle(result[timeStamp[i]], result[timeStamp[i + 1]], latKeyName, lngKeyName); + } + return result; + } + + private calculateCurrentTime(minTime: number, maxTime: number): number { + if (minTime !== this.minTime || maxTime !== this.maxTime) { + if (this.minTime >= this.currentTime || isUndefined(this.currentTime)) { + return this.minTime; + } else if (this.maxTime <= this.currentTime) { + return this.maxTime; + } else { + return this.minTime + Math.ceil((this.currentTime - this.minTime) / this.normalizationStep) * this.normalizationStep; + } + } + return this.currentTime; + } + + private clearIncorrectFirsLastDatapoint(dataSource: FormattedData[]): FormattedData[] { + const firstHistoricalDataIndexCoordinate = dataSource.findIndex(this.findFirstHistoricalDataIndexCoordinate); + if (firstHistoricalDataIndexCoordinate === -1) { + return []; + } + let lastIndex = dataSource.length - 1; + for (lastIndex; lastIndex > 0; lastIndex--) { + if (this.findFirstHistoricalDataIndexCoordinate(dataSource[lastIndex])) { + lastIndex++; + break; + } + } + if (firstHistoricalDataIndexCoordinate > 0 || lastIndex < dataSource.length) { + return dataSource.slice(firstHistoricalDataIndexCoordinate, lastIndex); + } + return dataSource; + } + + private findFirstHistoricalDataIndexCoordinate = (item: FormattedData): boolean => { + return isDefined(item[this.settings.latKeyName]) && isDefined(item[this.settings.lngKeyName]); + } +} + +export let TbTripAnimationWidget = TripAnimationComponent; diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html deleted file mode 100644 index e7a56691d8..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html +++ /dev/null @@ -1,45 +0,0 @@ - -
-
-
-
-
- -
-
-
-
-
-
- -
diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss deleted file mode 100644 index cdd1adbcf5..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.trip-animation-widget { - position: relative; - width: 100%; - height: 100%; - font-size: 16px; - line-height: 24px; - display: flex; - flex-direction: column; - - .trip-animation-label-container { - height: 24px; - } - - .trip-animation-container { - position: relative; - z-index: 1; - flex: 1; - width: 100%; - - .map { - width: 100%; - height: 100%; - } - - .trip-animation-info-panel { - position: absolute; - top: 0; - right: 0; - pointer-events: none; - - .tooltip-button { - top: 0; - left: 0; - width: 32px; - min-width: 32px; - height: 32px; - min-height: 32px; - padding: 0 0 2px; - margin: 2px; - line-height: 24px; - z-index: 999; - - &::ng-deep .mat-button-wrapper { - padding: 0; - } - - mat-icon { - width: 24px; - height: 24px; - - svg { - width: inherit; - height: inherit; - } - } - } - } - - .trip-animation-tooltip { - display: flex; - overflow: auto; - max-height: 90%; - position: absolute; - top: 30px; - right: 0; - z-index: 1000; - padding: 5px; - background-color: #fff; - transition: 0.3s ease-in-out; - - &-hidden { - transform: translateX(110%); - } - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts deleted file mode 100644 index f0ba0a93d0..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts +++ /dev/null @@ -1,339 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import _ from 'lodash'; -import tinycolor from 'tinycolor2'; - -import { - AfterViewInit, - ChangeDetectorRef, - Component, - Input, - OnDestroy, - OnInit, - SecurityContext, - ViewChild -} from '@angular/core'; -import { - defaultTripAnimationSettings, - MapProviders, - WidgetUnitedTripAnimationSettings -} from '@home/components/widget/lib/maps/map-models'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -import { WidgetContext } from '@app/modules/home/models/widget-component.models'; -import { - findAngle, - getRatio, - interpolateOnLineSegment, - parseWithTranslation -} from '@home/components/widget/lib/maps/common-maps-utils'; -import { FormattedData, WidgetConfig } from '@shared/models/widget.models'; -import moment from 'moment'; -import { - formattedDataArrayFromDatasourceData, - formattedDataFormDatasourceData, - isDefined, - isUndefined, - mergeFormattedData, - parseFunction, - safeExecute -} from '@core/utils'; -import { ResizeObserver } from '@juggle/resize-observer'; -import { MapWidgetInterface } from '@home/components/widget/lib/maps/map-widget.interface'; - -interface DataMap { - [key: string]: FormattedData; -} - -@Component({ - // tslint:disable-next-line:component-selector - selector: 'trip-animation', - templateUrl: './trip-animation.component.html', - styleUrls: ['./trip-animation.component.scss'] -}) -export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy { - - private mapResize$: ResizeObserver; - - constructor(private cd: ChangeDetectorRef, private sanitizer: DomSanitizer) { } - - @Input() ctx: WidgetContext; - - @ViewChild('map') mapContainer; - - mapWidget: MapWidgetInterface; - historicalData: FormattedData[][]; - normalizationStep: number; - interpolatedTimeData: {[time: number]: FormattedData}[] = []; - formattedInterpolatedTimeData: FormattedData[][] = []; - formattedCurrentPosition: FormattedData[] = []; - formattedLatestData: FormattedData[] = []; - widgetConfig: WidgetConfig; - settings: WidgetUnitedTripAnimationSettings; - mainTooltips = []; - visibleTooltip = false; - activeTrip: FormattedData; - label: SafeHtml; - minTime: number; - maxTime: number; - anchors: number[] = []; - useAnchors: boolean; - currentTime: number; - - ngOnInit(): void { - this.widgetConfig = this.ctx.widgetConfig; - this.settings = { - buttonColor: tinycolor(this.widgetConfig.color).setAlpha(0.54).toRgbString(), - ...defaultTripAnimationSettings, - ...this.ctx.settings - }; - this.useAnchors = this.settings.showPoints && this.settings.usePointAsAnchor; - this.settings.parsedPointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']); - this.settings.parsedTooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']); - this.settings.parsedLabelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']); - this.settings.parsedColorPointFunction = parseFunction(this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex']); - this.normalizationStep = this.settings.normalizationStep; - const subscription = this.ctx.defaultSubscription; - subscription.callbacks.onDataUpdated = () => { - this.historicalData = formattedDataArrayFromDatasourceData(this.ctx.data).map( - item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length); - this.interpolatedTimeData.length = 0; - this.formattedInterpolatedTimeData.length = 0; - const prevMinTime = this.minTime; - const prevMaxTime = this.maxTime; - this.calculateIntervals(); - const currentTime = this.calculateCurrentTime(prevMinTime, prevMaxTime); - if (currentTime !== this.currentTime) { - this.timeUpdated(currentTime); - } - this.mapWidget.map.map?.invalidateSize(); - this.mapWidget.map.setLoading(false); - this.cd.detectChanges(); - }; - subscription.callbacks.onLatestDataUpdated = () => { - this.formattedLatestData = formattedDataFormDatasourceData(this.ctx.latestData); - this.updateCurrentData(); - }; - } - - ngAfterViewInit() { - import('@home/components/widget/lib/maps/map-widget2').then( - (mod) => { - this.mapWidget = new mod.MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement); - this.mapResize$ = new ResizeObserver(() => { - this.mapWidget.resize(); - }); - this.mapResize$.observe(this.mapContainer.nativeElement); - } - ); - } - - ngOnDestroy() { - if (this.mapResize$) { - this.mapResize$.disconnect(); - } - } - - timeUpdated(time: number) { - this.currentTime = time; - // get point for each datasource associated with time - this.formattedCurrentPosition = this.interpolatedTimeData - .map(dataSource => dataSource[time]); - for (let j = 0; j < this.interpolatedTimeData.length; j++) { - if (isUndefined(this.formattedCurrentPosition[j])) { - const timePoints = Object.keys(this.interpolatedTimeData[j]).map(item => parseInt(item, 10)); - for (let i = 1; i < timePoints.length; i++) { - if (timePoints[i - 1] < time && timePoints[i] > time) { - const beforePosition = this.interpolatedTimeData[j][timePoints[i - 1]]; - const afterPosition = this.interpolatedTimeData[j][timePoints[i]]; - const ratio = getRatio(timePoints[i - 1], timePoints[i], time); - this.formattedCurrentPosition[j] = { - ...beforePosition, - time, - ...interpolateOnLineSegment(beforePosition, afterPosition, this.settings.latKeyName, this.settings.lngKeyName, ratio) - }; - break; - } - } - } - } - for (let j = 0; j < this.interpolatedTimeData.length; j++) { - if (isUndefined(this.formattedCurrentPosition[j])) { - this.formattedCurrentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time); - } - } - this.updateCurrentData(); - } - - private updateCurrentData() { - let currentPosition = this.formattedCurrentPosition; - if (this.formattedLatestData.length) { - currentPosition = mergeFormattedData(this.formattedCurrentPosition, this.formattedLatestData); - } - this.calcLabel(currentPosition); - this.calcMainTooltip(currentPosition); - if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) { - this.mapWidget.map.updateFromData(true, currentPosition, this.formattedInterpolatedTimeData, (trip) => { - this.activeTrip = trip; - this.timeUpdated(this.currentTime); - this.cd.markForCheck(); - }); - if (this.settings.showPoints) { - this.mapWidget.map.updatePoints(this.formattedInterpolatedTimeData, this.calcTooltip); - } - } - } - - setActiveTrip() { - } - - private calculateLastPoints(dataSource: DataMap, time: number): FormattedData { - const timeArr = Object.keys(dataSource); - let index = timeArr.findIndex((dtime) => { - return Number(dtime) >= time; - }); - - if (index !== -1) { - if (Number(timeArr[index]) !== time && index !== 0) { - index--; - } - } else { - index = timeArr.length - 1; - } - - return dataSource[timeArr[index]]; - } - - calculateIntervals() { - let minTime = Infinity; - let maxTime = -Infinity; - this.historicalData.forEach((dataSource) => { - minTime = Math.min(dataSource[0].time, minTime); - maxTime = Math.max(dataSource[dataSource.length - 1].time, maxTime); - }); - this.minTime = minTime; - this.maxTime = maxTime; - this.historicalData.forEach((dataSource, index) => { - this.interpolatedTimeData[index] = this.interpolateArray(dataSource); - }); - this.formattedInterpolatedTimeData = this.interpolatedTimeData.map(ds => _.values(ds)); - if (!this.activeTrip) { - this.activeTrip = this.interpolatedTimeData.map(dataSource => dataSource[this.minTime]).filter(ds => ds)[0]; - } - if (this.useAnchors) { - const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]); - this.anchors = anchorDate - .filter((data: [string, FormattedData], tsIndex) => safeExecute(this.settings.parsedPointAsAnchorFunction, [data[1], - this.formattedInterpolatedTimeData.map(ds => ds[tsIndex]), data[1].dsIndex])) - .map(data => parseInt(data[0], 10)); - } - } - - calcTooltip = (point: FormattedData, points: FormattedData[]): string => { - const data = point ? point : this.activeTrip; - const tooltipPattern: string = this.settings.useTooltipFunction ? - safeExecute(this.settings.parsedTooltipFunction, - [data, points, point.dsIndex]) : this.settings.tooltipPattern; - return parseWithTranslation.parseTemplate(tooltipPattern, data, true); - } - - private calcMainTooltip(points: FormattedData[]): void { - const tooltips = []; - for (const point of points) { - tooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, this.calcTooltip(point, points))); - } - this.mainTooltips = tooltips; - } - - calcLabel(points: FormattedData[]) { - if (this.activeTrip) { - const data = points[this.activeTrip.dsIndex]; - const labelText: string = this.settings.useLabelFunction ? - safeExecute(this.settings.parsedLabelFunction, [data, points, data.dsIndex]) : this.settings.label; - this.label = this.sanitizer.bypassSecurityTrustHtml(parseWithTranslation.parseTemplate(labelText, data, true)); - } - } - - private interpolateArray(originData: FormattedData[]): {[time: number]: FormattedData} { - const result: {[time: number]: FormattedData} = {}; - const latKeyName = this.settings.latKeyName; - const lngKeyName = this.settings.lngKeyName; - for (const data of originData) { - const currentTime = data.time; - const normalizeTime = this.minTime + Math.ceil((currentTime - this.minTime) / this.normalizationStep) * this.normalizationStep; - result[normalizeTime] = { - ...data, - minTime: this.minTime !== Infinity ? moment(this.minTime).format('YYYY-MM-DD HH:mm:ss') : '', - maxTime: this.maxTime !== -Infinity ? moment(this.maxTime).format('YYYY-MM-DD HH:mm:ss') : '', - rotationAngle: this.settings.rotationAngle - }; - } - const timeStamp = Object.keys(result); - for (let i = 0; i < timeStamp.length - 1; i++) { - if (isUndefined(result[timeStamp[i + 1]][latKeyName]) || isUndefined(result[timeStamp[i + 1]][lngKeyName])) { - for (let j = i + 2; j < timeStamp.length - 1; j++) { - if (isDefined(result[timeStamp[j]][latKeyName]) || isDefined(result[timeStamp[j]][lngKeyName])) { - const ratio = getRatio(Number(timeStamp[i]), Number(timeStamp[j]), Number(timeStamp[i + 1])); - result[timeStamp[i + 1]] = { - ...interpolateOnLineSegment(result[timeStamp[i]], result[timeStamp[j]], latKeyName, lngKeyName, ratio), - ...result[timeStamp[i + 1]], - }; - break; - } - } - } - result[timeStamp[i]].rotationAngle += findAngle(result[timeStamp[i]], result[timeStamp[i + 1]], latKeyName, lngKeyName); - } - return result; - } - - private calculateCurrentTime(minTime: number, maxTime: number): number { - if (minTime !== this.minTime || maxTime !== this.maxTime) { - if (this.minTime >= this.currentTime || isUndefined(this.currentTime)) { - return this.minTime; - } else if (this.maxTime <= this.currentTime) { - return this.maxTime; - } else { - return this.minTime + Math.ceil((this.currentTime - this.minTime) / this.normalizationStep) * this.normalizationStep; - } - } - return this.currentTime; - } - - private clearIncorrectFirsLastDatapoint(dataSource: FormattedData[]): FormattedData[] { - const firstHistoricalDataIndexCoordinate = dataSource.findIndex(this.findFirstHistoricalDataIndexCoordinate); - if (firstHistoricalDataIndexCoordinate === -1) { - return []; - } - let lastIndex = dataSource.length - 1; - for (lastIndex; lastIndex > 0; lastIndex--) { - if (this.findFirstHistoricalDataIndexCoordinate(dataSource[lastIndex])) { - lastIndex++; - break; - } - } - if (firstHistoricalDataIndexCoordinate > 0 || lastIndex < dataSource.length) { - return dataSource.slice(firstHistoricalDataIndexCoordinate, lastIndex); - } - return dataSource; - } - - private findFirstHistoricalDataIndexCoordinate = (item: FormattedData): boolean => { - return isDefined(item[this.settings.latKeyName]) && isDefined(item[this.settings.lngKeyName]); - } -} - -export let TbTripAnimationWidget = TripAnimationComponent; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 38b14fe485..e3ff270197 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -42,22 +42,17 @@ import { WidgetTypeId } from '@app/shared/models/id/widget-type-id'; import { TenantId } from '@app/shared/models/id/tenant-id'; import { SharedModule } from '@shared/shared.module'; import { MODULES_MAP } from '@shared/public-api'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import moment from 'moment'; import { IModulesMap } from '@modules/common/modules-map.models'; import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; -const tinycolor = tinycolor_; - -// @dynamic @Injectable() export class WidgetComponentService { private cssParser = new cssjs(); - private widgetsInfoInMemoryCache = new Map(); - private widgetsInfoFetchQueue = new Map>>(); private init$: Observable; @@ -77,14 +72,6 @@ export class WidgetComponentService { this.cssParser.testMode = false; - this.widgetService.onWidgetTypeUpdated().subscribe((widgetType) => { - this.deleteWidgetInfoFromCache(widgetType.bundleAlias, widgetType.alias, widgetType.tenantId.id === NULL_UUID); - }); - - this.widgetService.onWidgetBundleDeleted().subscribe((widgetsBundle) => { - this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID); - }); - this.init(); } @@ -112,11 +99,13 @@ export class WidgetComponentService { settingsDirective: this.utils.editWidgetInfo.settingsDirective, dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective, latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective, + hasBasicMode: this.utils.editWidgetInfo.hasBasicMode, + basicModeDirective: this.utils.editWidgetInfo.basicModeDirective, defaultConfig: this.utils.editWidgetInfo.defaultConfig }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined ); } - const initSubject = new ReplaySubject(); + const initSubject = new ReplaySubject(); this.init$ = initSubject.asObservable(); const w = (this.window as any); @@ -175,7 +164,7 @@ export class WidgetComponentService { (window as any).TbMapWidgetV2 = mod.TbMapWidgetV2; })) ); - widgetModulesTasks.push(from(import('@home/components/widget/trip-animation/trip-animation.component')).pipe( + widgetModulesTasks.push(from(import('@home/components/widget/lib/trip-animation/trip-animation.component')).pipe( tap((mod) => { (window as any).TbTripAnimationWidget = mod.TbTripAnimationWidget; })) @@ -223,7 +212,7 @@ export class WidgetComponentService { } public getInstantWidgetInfo(widget: Widget): WidgetInfo { - const widgetInfo = this.getWidgetInfoFromCache(widget.bundleAlias, widget.typeAlias, widget.isSystemType); + const widgetInfo = this.widgetService.getWidgetInfoFromCache(widget.bundleAlias, widget.typeAlias, widget.isSystemType); if (widgetInfo) { return widgetInfo; } else { @@ -237,9 +226,14 @@ export class WidgetComponentService { ); } + public clearWidgetInfo(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): void { + this.dynamicComponentFactoryService.destroyDynamicComponentFactory(widgetInfo.componentFactory); + this.widgetService.deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); + } + private getWidgetInfoInternal(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable { const widgetInfoSubject = new ReplaySubject(); - const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); + const widgetInfo = this.widgetService.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); if (widgetInfo) { widgetInfoSubject.next(widgetInfo); widgetInfoSubject.complete(); @@ -247,7 +241,7 @@ export class WidgetComponentService { if (this.utils.widgetEditMode) { this.loadWidget(this.editingWidgetType, bundleAlias, isSystem, widgetInfoSubject); } else { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); + const key = this.widgetService.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); let fetchQueue = this.widgetsInfoFetchQueue.get(key); if (fetchQueue) { fetchQueue.push(widgetInfoSubject); @@ -272,7 +266,7 @@ export class WidgetComponentService { private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject) { const widgetInfo = toWidgetInfo(widgetType); - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem); + const key = this.widgetService.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem); let widgetControllerDescriptor: WidgetControllerDescriptor = null; try { widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key); @@ -297,7 +291,7 @@ export class WidgetComponentService { widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters; widgetInfo.actionSources = widgetControllerDescriptor.actionSources; widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction; - this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); + this.widgetService.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); if (widgetInfoSubject) { widgetInfoSubject.next(widgetInfo); widgetInfoSubject.complete(); @@ -331,7 +325,7 @@ export class WidgetComponentService { (resource) => { resourceTasks.push( this.resources.loadResource(resource.url).pipe( - catchError(e => of(`Failed to load widget resource: '${resource.url}'`)) + catchError(() => of(`Failed to load widget resource: '${resource.url}'`)) ) ); } @@ -351,7 +345,7 @@ export class WidgetComponentService { factories: modulesWithFactoriesList.map(mf => mf.factories).flat() }; if (modules && modules.length) { - resModulesWithFactories.modules.concat(modules); + resModulesWithFactories.modules = resModulesWithFactories.modules.concat(modules); } return resModulesWithFactories; } @@ -412,6 +406,9 @@ export class WidgetComponentService { if (widgetInfo.latestDataKeySettingsDirective && widgetInfo.latestDataKeySettingsDirective.length) { directives.push(widgetInfo.latestDataKeySettingsDirective); } + if (widgetInfo.basicModeDirective && widgetInfo.basicModeDirective.length) { + directives.push(widgetInfo.basicModeDirective); + } if (directives.length) { factories.filter((factory) => directives.includes(factory.selector)) .forEach((foundFactory) => { @@ -586,34 +583,4 @@ export class WidgetComponentService { this.widgetsInfoFetchQueue.delete(key); } } - - // Cache functions - - private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string { - return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`; - } - - private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - return this.widgetsInfoInMemoryCache.get(key); - } - - private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - this.widgetsInfoInMemoryCache.set(key, widgetInfo); - } - - private deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - this.widgetsInfoInMemoryCache.delete(key); - } - - private deleteWidgetsBundleFromCache(bundleAlias: string, isSystem: boolean) { - const key = (isSystem ? 'sys_' : '') + bundleAlias; - this.widgetsInfoInMemoryCache.forEach((widgetInfo, cacheKey) => { - if (cacheKey.startsWith(key)) { - this.widgetsInfoInMemoryCache.delete(cacheKey); - } - }); - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index c81b5f01f5..ce2e7055cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -20,21 +20,18 @@ import { SharedModule } from '@app/shared/shared.module'; import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component'; import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component'; import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component'; -import { AlarmFilterPanelComponent } from '@home/components/widget/lib/alarm-filter-panel.component'; import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component'; import { EntitiesHierarchyWidgetComponent } from '@home/components/widget/lib/entities-hierarchy-widget.component'; -import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; import { RpcWidgetsModule } from '@home/components/widget/lib/rpc/rpc-widgets.module'; import { DateRangeNavigatorPanelComponent, DateRangeNavigatorWidgetComponent } from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component'; import { MultipleInputWidgetComponent } from '@home/components/widget/lib/multiple-input-widget.component'; -import { TripAnimationComponent } from '@home/components/widget/trip-animation/trip-animation.component'; +import { TripAnimationComponent } from '@home/components/widget/lib/trip-animation/trip-animation.component'; import { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.component'; import { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.component'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component'; @@ -42,12 +39,15 @@ import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget.component'; import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-widget.component'; import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; +import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/home-page-widgets.module'; +import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; +import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component'; +import { LegendComponent } from '@home/components/widget/lib/legend.component'; @NgModule({ declarations: [ DisplayColumnsPanelComponent, - AlarmFilterPanelComponent, EntitiesTableWidgetComponent, AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, @@ -64,35 +64,40 @@ import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/di NavigationCardWidgetComponent, QrCodeWidgetComponent, MarkdownWidgetComponent, - SelectEntityDialogComponent + SelectEntityDialogComponent, + LegendComponent, + FlotWidgetComponent ], imports: [ CommonModule, SharedModule, RpcWidgetsModule, + HomePageWidgetsModule, SharedHomeComponentsModule ], - exports: [ - EntitiesTableWidgetComponent, - AlarmsTableWidgetComponent, - TimeseriesTableWidgetComponent, - EntitiesHierarchyWidgetComponent, - EdgesOverviewWidgetComponent, - RpcWidgetsModule, - DateRangeNavigatorWidgetComponent, - JsonInputWidgetComponent, - MultipleInputWidgetComponent, - TripAnimationComponent, - PhotoCameraInputWidgetComponent, - GatewayFormComponent, - NavigationCardsWidgetComponent, - NavigationCardWidgetComponent, - QrCodeWidgetComponent, - MarkdownWidgetComponent - ], + exports: [ + EntitiesTableWidgetComponent, + AlarmsTableWidgetComponent, + TimeseriesTableWidgetComponent, + EntitiesHierarchyWidgetComponent, + EdgesOverviewWidgetComponent, + RpcWidgetsModule, + HomePageWidgetsModule, + DateRangeNavigatorWidgetComponent, + JsonInputWidgetComponent, + MultipleInputWidgetComponent, + TripAnimationComponent, + PhotoCameraInputWidgetComponent, + GatewayFormComponent, + NavigationCardsWidgetComponent, + NavigationCardWidgetComponent, + QrCodeWidgetComponent, + MarkdownWidgetComponent, + LegendComponent, + FlotWidgetComponent + ], providers: [ - CustomDialogService, - ImportExportService + {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } ] }) export class WidgetComponentsModule { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 8ba8cda461..1766b79009 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -1,6 +1,6 @@ - - -
-
-
- - {{ 'widget-config.use-dashboard-timewindow' | translate }} - - - {{ 'widget-config.display-timewindow' | translate }} - -
-
- widget-config.timewindow - -
-
-
-
- - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - +
+
+ + + + +
+
+
+ +
+
+ +
+
+
+
widget-config.card-title
+ + {{ 'widget-config.display-title' | translate }} + +
+ + widget-config.title + - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - - + + widget-config.title-tooltip +
-
- - alarm.alarm-type-list - - - {{type}} - cancel - - - - - - {{ 'alarm.search-propagated-alarms' | translate }} - +
+ + {{ 'widget-config.display-icon' | translate }} + +
+ + + + + + + + +
-
- - - - -
-
widget-config.datasources
-
{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}
-
- - report_gmailerrorred - -
- - {{ 'widget-config.timeseries-key-error' | translate }} + + + + widget-config.advanced-title-style -
- datasource.add-datasource-prompt -
- -
- -
- widget-config.datasource-type - widget-config.datasource-parameters - -
-
-
- - -
-
- - {{$index + 1}}. -
-
-
-
- - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - datasource.label - - - - - - - - - - - - - -
-
- - - - -
-
- -
- -
-
-
-
- -
+ + -
- -
- - - - {{ 'widget-config.target-device' | translate }} - - -
- - +
+
+
widget-config.card-style
+
+
{{ 'widget-config.text' | translate }}
+
+ + +
- - - - - {{ 'widget-config.alarm-source' | translate }} - - -
-
- - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - - - - -
- - -
+
+
+
{{ 'widget-config.background' | translate }}
+
+ + +
- - +
+
+
{{ 'widget-config.padding' | translate }}
+ + + +
+
+
{{ 'widget-config.margin' | translate }}
+ + + +
+ + {{ 'widget-config.drop-shadow' | translate }} + + - widget-config.data-settings + + widget-config.advanced-widget-style + -
- - widget-config.data-page-size - - -
-
- - widget-config.units - - - - widget-config.decimals - - -
-
- - widget-config.no-data-display-message - - -
+ +
- +
+
+
widget-config.card-buttons
+ + {{ 'widget-config.enable-fullscreen' | translate }} + +
- - -
-
-
- widget-config.title - - {{ 'widget-config.display-title' | translate }} - -
- - widget-config.title - - - - widget-config.title-tooltip - - -
-
- widget-config.title-icon - - {{ 'widget-config.display-icon' | translate }} - -
- - - - - - widget-config.icon-size - - -
-
- - - - widget-config.advanced-settings - - - - - - -
-
- widget-config.widget-style -
-
- - - - -
-
- - widget-config.padding - - - - widget-config.margin - - -
-
- - {{ 'widget-config.drop-shadow' | translate }} - - - {{ 'widget-config.enable-fullscreen' | translate }} - - - - - widget-config.advanced-settings - - - - - - - -
-
- widget-config.legend - - - - - {{ 'widget-config.display-legend' | translate }} - - - - widget-config.advanced-settings - - - - - - -
-
- widget-config.mobile-mode-settings - - - - - {{ 'widget-config.mobile-hide' | translate }} - - - {{ 'widget-config.desktop-hide' | translate }} - - - - widget-config.advanced-settings - - - -
- - widget-config.order - - - - widget-config.height - - -
-
-
-
+
+
+ +
- - -
- - +
+
+ + {{ 'widget-config.mobile-hide' | translate }} + +
+
+ + {{ 'widget-config.desktop-hide' | translate }} + +
+
+
+
widget-config.order
+ + + +
+
+
widget-config.height
+ + + +
+
- - - - - - +
+ +
+
+ +
{{basicModeDirectiveError}}
+
+ +
+ + + + + + +
+
+
+
+ + + + + +
+ +
+
+ + +
+
+
widget-config.target-device
+
+ + +
+
+
+
widget-config.alarm-source
+ + +
+
+
widget-config.limits
+
+
widget-config.data-page-size
+ + + +
+
+
+ +
+
widget-config.data-settings
+
+
widget-config.units
+ + +
+
+
widget-config.decimals
+ + + +
+
+
widget-config.no-data-display-message
+ + + +
+
+
+ + +
+
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts deleted file mode 100644 index 89a1409b5a..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts +++ /dev/null @@ -1,22 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models'; -import { DataKeysCallbacks } from './data-keys.component.models'; -import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models'; -import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; - -export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & FilterSelectCallbacks & DataKeysCallbacks & WidgetActionCallbacks; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index d8f18a6fc5..91b3368ad0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,216 +13,54 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +@import "../../../../../theme"; @import '../../../../../scss/constants'; -.tb-datasource-list-item.tb-draggable { - &.cdk-drag-preview { - background: #fff; +.tb-widget-config { + display: flex; + flex-direction: column; + gap: 16px; + .tb-widget-config-header { + padding: 24px 24px 8px; + height: 56px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; } -} - -:host { - .tb-widget-config { - .tb-advanced-widget-config { - height: 100%; - } - .tb-datasources { - - .handle { - cursor: move; - } - - .mat-list { - min-height: 68px; - padding-left: 0; - } - - .mat-list-item { - height: auto; - min-height: 68px; - display: block; - } - } - .tb-datasource-type { - min-width: 120px; - @media #{$mat-gt-sm} { - max-width: 120px; - } - } - .tb-datasource { - @media #{$mat-gt-sm} { - padding-left: 8px; - } - } - .tb-datasource-name { - @media #{$mat-gt-sm} { - width: 200px; - max-width: 200px; - } - } - .tb-datasource-params { - position: relative; - padding: 0 0 0 10px; - margin: 5px; - tb-error.tb-datasource-error { - position: absolute; - bottom: 4px; - left: 8px; - } - } - .tb-data-keys { - @media #{$mat-gt-sm} { - padding-left: 8px; - } - } - .fields-group { - padding: 0 16px 8px; - margin-bottom: 10px; - border: 1px groove rgba(0, 0, 0, .25); - border-radius: 4px; - legend { - color: rgba(0, 0, 0, .7); - width: fit-content; - } - } - .fields-group-slider { - padding: 0; - legend { - margin-left: 16px; - } - .tb-settings { - padding: 0 16px 8px; - } + .tb-widget-config-content { + flex: 1; + overflow: auto; + & > div { + padding: 16px; } - .tb-widget-style { - margin-top: 16px; + .mat-content { + display: flex; + flex-direction: column; + gap: 16px; } } + .tb-basic-mode-directive-error { + font-size: 13px; + font-weight: 400; + color: rgb(221, 44, 0); + } } :host ::ng-deep { .tb-widget-config { - .mat-tab-body-wrapper { + .tb-widget-config-header { + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + padding: 0 20px; + } + } + .mat-mdc-tab-body-wrapper { position: absolute; top: 49px; left: 0; right: 0; bottom: 0; } - .tb-panel-hint { - font-size: 12px; - color: #808080; - } - //.mat-tab-body.mat-tab-body-active { - // .mat-tab-body-content > div { - // height: 100%; - // } - //} - .mat-checkbox-label { - white-space: normal; - } - .mat-expansion-panel { - &.tb-settings { - box-shadow: none; - .mat-content { - overflow: visible; - } - .mat-expansion-panel-header { - padding: 0; - &:hover { - background: none; - } - .mat-expansion-indicator { - padding: 2px; - } - } - .mat-expansion-panel-header-description { - align-items: center; - } - > .mat-expansion-panel-content { - > .mat-expansion-panel-body { - padding: 0; - } - } - .tb-json-object-panel, .tb-css-content-panel { - margin: 0 0 8px; - } - .mat-checkbox-layout { - margin: 5px 0; - } - .mat-checkbox-inner-container { - margin-right: 12px; - } - } - &.tb-datasources { - &.mat-expanded { - overflow: visible; - } - .mat-expansion-panel-body{ - padding: 0 12px 16px; - } - .mat-list-base .mat-list-item { - .mat-list-item-content { - padding: 0; - } - } - } - .mat-expansion-panel-content { - font: inherit; - } - } - .tb-datasource-type { - .mat-form-field-infix { - width: 100%; - } - .mat-select-value { - min-width: 92px; - } - } - .tb-datasource { - tb-entity-alias-select { - .mat-form-field { - margin-top: 20px; - } - @media #{$mat-gt-sm} { - .mat-form-field { - width: 200px; - max-width: 200px; - } - } - } - tb-filter-select { - @media #{$mat-gt-sm} { - .mat-form-field { - width: 200px; - max-width: 200px; - } - } - } - .tb-datasource-name.no-border-top { - .mat-form-field-infix { - border-top: 0; - } - } - } - .tb-data-keys { - @media #{$mat-gt-sm} { - .mat-input-element { - min-height: 32px; - } - } - } - .mat-slide { - margin: 8px 0; - } - .slide-block { - display: block; - &:not(:last-child) { - margin-bottom: 8px; - } - } - .mat-slide-toggle-content { - white-space: normal; - } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index b8683741e3..999e7e55ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,42 +14,47 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + ComponentRef, + forwardRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DataKey, - Datasource, datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, - DatasourceType, - datasourceTypeTranslationMap, - defaultLegendConfig, + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation, GroupInfo, JsonSchema, JsonSettingsSchema, Widget, + WidgetConfigMode, widgetType } from '@shared/models/widget.models'; import { ControlValueAccessor, - FormArray, - FormBuilder, - FormControl, - FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, Validator, Validators } from '@angular/forms'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { deepClone, isDefined, isObject } from '@app/core/utils'; -import { - alarmFields, - AlarmSearchStatus, - alarmSearchStatusTranslations, - AlarmSeverity, - alarmSeverityTranslations -} from '@shared/models/alarm.models'; +import { deepClone, genNextLabel, isDefined, isObject } from '@app/core/utils'; +import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models'; import { IAliasController } from '@core/api/widget-api.models'; import { EntityAlias } from '@shared/models/alias.models'; import { UtilsService } from '@core/services/utils.service'; @@ -57,7 +62,10 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; import { Observable, of, Subscription } from 'rxjs'; -import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models'; +import { + IBasicWidgetConfigComponent, + WidgetConfigCallbacks +} from '@home/components/widget/config/widget-config.component.models'; import { EntityAliasDialogComponent, EntityAliasDialogData @@ -66,15 +74,15 @@ import { catchError, mergeMap, tap } from 'rxjs/operators'; import { MatDialog } from '@angular/material/dialog'; import { EntityService } from '@core/http/entity.service'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; -import { WidgetActionsData } from './action/manage-widget-actions.component.models'; import { Dashboard } from '@shared/models/dashboard.models'; import { entityFields } from '@shared/models/entity.models'; -import { Filter } from '@shared/models/query/query.models'; +import { Filter, singleEntityFilterFromDeviceId } from '@shared/models/query/query.models'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { AggregationType } from '@shared/models/time/time.models'; +import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module'; +import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; +import Timeout = NodeJS.Timeout; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -102,25 +110,15 @@ const defaultSettingsForm = [ } ] }) -export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { +export class WidgetConfigComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator, OnChanges { - readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + @ViewChild('basicModeContainer', {read: ViewContainerRef, static: false}) basicModeContainer: ViewContainerRef; widgetTypes = widgetType; - entityTypes = EntityType; - - alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, - AlarmSearchStatus.CLEARED, - AlarmSearchStatus.ACK, - AlarmSearchStatus.UNACK]; - - alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; + widgetConfigModes = WidgetConfigMode; - alarmSeverities = Object.keys(AlarmSeverity); - alarmSeverityEnum = AlarmSeverity; - - alarmSeverityTranslationMap = alarmSeverityTranslations; + entityTypes = EntityType; @Input() forceExpandDatasources: boolean; @@ -137,65 +135,80 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @Input() functionsOnly: boolean; + @Input() + @coerceBoolean() + hideHeader = false; + + @Input() + @coerceBoolean() + hideToggleHeader = false; + + @Input() + @coerceBoolean() + isAdd = false; + @Input() disabled: boolean; - widgetType: widgetType; + @Input() + widgetConfigMode = WidgetConfigMode.advanced; - datasourceType = DatasourceType; - datasourceTypes: Array = []; - datasourceTypesTranslations = datasourceTypeTranslationMap; + widgetType: widgetType; widgetConfigCallbacks: WidgetConfigCallbacks = { createEntityAlias: this.createEntityAlias.bind(this), createFilter: this.createFilter.bind(this), generateDataKey: this.generateDataKey.bind(this), + fetchEntityKeysForDevice: this.fetchEntityKeysForDevice.bind(this), fetchEntityKeys: this.fetchEntityKeys.bind(this), fetchDashboardStates: this.fetchDashboardStates.bind(this) }; widgetEditMode = this.utils.widgetEditMode; - selectedTab: number; + basicModeDirectiveError: string; modelValue: WidgetConfigComponentData; - showLegendFieldset = true; - private propagateChange = null; - public dataSettings: FormGroup; - public targetDeviceSettings: FormGroup; - public alarmSourceSettings: FormGroup; - public widgetSettings: FormGroup; - public layoutSettings: FormGroup; - public advancedSettings: FormGroup; - public actionsSettings: FormGroup; - public openExtensionPanel = true; - public timeseriesKeyError = false; + headerOptions: ToggleHeaderOption[] = []; + selectedOption: string; - public datasourceError: string[] = []; + public dataSettings: UntypedFormGroup; + public targetDeviceSettings: UntypedFormGroup; + public widgetSettings: UntypedFormGroup; + public layoutSettings: UntypedFormGroup; + public advancedSettings: UntypedFormGroup; + public actionsSettings: UntypedFormGroup; + + private createBasicModeComponentTimeout: Timeout; + private basicModeComponentRef: ComponentRef; + private basicModeComponent: IBasicWidgetConfigComponent; + private basicModeComponentChangeSubscription: Subscription; private dataSettingsChangesSubscription: Subscription; private targetDeviceSettingsSubscription: Subscription; - private alarmSourceSettingsSubscription: Subscription; private widgetSettingsSubscription: Subscription; private layoutSettingsSubscription: Subscription; private advancedSettingsSubscription: Subscription; private actionsSettingsSubscription: Subscription; + private defaultConfigFormsType: widgetType; + constructor(protected store: Store, private utils: UtilsService, private entityService: EntityService, private dialog: MatDialog, - private translate: TranslateService, - private fb: FormBuilder) { + public translate: TranslateService, + private cfr: ComponentFactoryResolver, + private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { super(store); } ngOnInit(): void { this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); - this.alarmSourceSettings = this.fb.group({}); this.advancedSettings = this.fb.group({}); this.widgetSettings = this.fb.group({ title: [null, []], @@ -217,53 +230,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont pageSize: [1024, [Validators.min(1), Validators.pattern(/^\d*$/)]], units: [null, []], decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], - noDataDisplayMessage: [null, []], - showLegend: [null, []], - legendConfig: [null, []] - }); - this.widgetSettings.get('showTitle').valueChanges.subscribe((value: boolean) => { - if (value) { - this.widgetSettings.get('titleStyle').enable({emitEvent: false}); - this.widgetSettings.get('titleTooltip').enable({emitEvent: false}); - this.widgetSettings.get('showTitleIcon').enable({emitEvent: false}); - } else { - this.widgetSettings.get('titleStyle').disable({emitEvent: false}); - this.widgetSettings.get('titleTooltip').disable({emitEvent: false}); - this.widgetSettings.get('showTitleIcon').patchValue(false); - this.widgetSettings.get('showTitleIcon').disable({emitEvent: false}); - } + noDataDisplayMessage: [null, []] }); - this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => { - if (value) { - this.widgetSettings.get('titleIcon').enable({emitEvent: false}); - this.widgetSettings.get('iconColor').enable({emitEvent: false}); - this.widgetSettings.get('iconSize').enable({emitEvent: false}); - } else { - this.widgetSettings.get('titleIcon').disable({emitEvent: false}); - this.widgetSettings.get('iconColor').disable({emitEvent: false}); - this.widgetSettings.get('iconSize').disable({emitEvent: false}); - } + this.widgetSettings.get('showTitle').valueChanges.subscribe(() => { + this.updateWidgetSettingsEnabledState(); }); - this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => { - if (value) { - this.widgetSettings.get('legendConfig').enable({emitEvent: false}); - } else { - this.widgetSettings.get('legendConfig').disable({emitEvent: false}); - } + this.widgetSettings.get('showTitleIcon').valueChanges.subscribe(() => { + this.updateWidgetSettingsEnabledState(); }); + this.layoutSettings = this.fb.group({ mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]], - mobileHeight: [null, [Validators.min(1), Validators.max(10), Validators.pattern(/^\d*$/)]], + mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]], mobileHide: [false], desktopHide: [false] }); this.actionsSettings = this.fb.group({ - actionsData: [null, []] + actions: [null, []] }); } + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'widgetConfigMode') { + if (this.hasBasicModeDirective) { + this.setupConfig(); + } + } + } + } + } + ngOnDestroy(): void { + this.destroyBasicModeComponent(); this.removeChangeSubscriptions(); } @@ -276,10 +278,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont this.targetDeviceSettingsSubscription.unsubscribe(); this.targetDeviceSettingsSubscription = null; } - if (this.alarmSourceSettingsSubscription) { - this.alarmSourceSettingsSubscription.unsubscribe(); - this.alarmSourceSettingsSubscription = null; - } if (this.widgetSettingsSubscription) { this.widgetSettingsSubscription.unsubscribe(); this.widgetSettingsSubscription = null; @@ -305,9 +303,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( () => this.updateTargetDeviceSettings() ); - this.alarmSourceSettingsSubscription = this.alarmSourceSettings.valueChanges.subscribe( - () => this.updateAlarmSourceSettings() - ); this.widgetSettingsSubscription = this.widgetSettings.valueChanges.subscribe( () => this.updateWidgetSettings() ); @@ -322,62 +317,78 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont ); } - private buildForms() { - if (this.functionsOnly) { - this.datasourceTypes = [DatasourceType.function]; - } else { - this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; - if (this.widgetType === widgetType.latest) { - this.datasourceTypes.push(DatasourceType.entityCount); + private buildHeader() { + this.headerOptions.length = 0; + if (this.widgetType !== widgetType.static) { + this.headerOptions.push( + { + name: this.translate.instant('widget-config.data'), + value: 'data' + } + ); + } + if (this.displayAppearance) { + this.headerOptions.push( + { + name: this.translate.instant('widget-config.appearance'), + value: 'appearance' + } + ); + } + this.headerOptions.push( + { + name: this.translate.instant('widget-config.widget-card'), + value: 'card' } + ); + this.headerOptions.push( + { + name: this.translate.instant('widget-config.actions'), + value: 'actions' + } + ); + this.headerOptions.push( + { + name: this.translate.instant('widget-config.mobile'), + value: 'mobile' + } + ); + if (!this.selectedOption || !this.headerOptions.find(o => o.value === this.selectedOption)) { + this.selectedOption = this.headerOptions[0].value; } + } + private buildForms() { this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); - this.alarmSourceSettings = this.fb.group({}); this.advancedSettings = this.fb.group({}); if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { - this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(true)); - this.dataSettings.addControl('displayTimewindow', this.fb.control({value: true, disabled: true})); - this.dataSettings.addControl('timewindow', this.fb.control({value: null, disabled: true})); - this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => { - if (value) { - this.dataSettings.get('displayTimewindow').disable({emitEvent: false}); - this.dataSettings.get('timewindow').disable({emitEvent: false}); - } else { - this.dataSettings.get('displayTimewindow').enable({emitEvent: false}); - this.dataSettings.get('timewindow').enable({emitEvent: false}); - } - }); + this.dataSettings.addControl('timewindowConfig', this.fb.control({ + useDashboardTimewindow: true, + displayTimewindow: true, + timewindow: null + })); if (this.widgetType === widgetType.alarm) { - this.dataSettings.addControl('alarmStatusList', this.fb.control(null)); - this.dataSettings.addControl('alarmSeverityList', this.fb.control(null)); - this.dataSettings.addControl('alarmTypeList', this.fb.control(null)); - this.dataSettings.addControl('searchPropagatedAlarms', this.fb.control(null)); + this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null)); } } if (this.modelValue.isDataEnabled) { if (this.widgetType !== widgetType.rpc && this.widgetType !== widgetType.alarm && this.widgetType !== widgetType.static) { - this.dataSettings.addControl('datasources', - this.fb.array([])); + this.dataSettings.addControl('datasources', this.fb.control(null)); } else if (this.widgetType === widgetType.rpc) { this.targetDeviceSettings.addControl('targetDeviceAliasId', this.fb.control(null, this.widgetEditMode ? [] : [Validators.required])); } else if (this.widgetType === widgetType.alarm) { - this.alarmSourceSettings = this.buildDatasourceForm(); + this.dataSettings.addControl('alarmSource', this.fb.control(null)); } } this.advancedSettings.addControl('settings', this.fb.control(null, [])); } - datasourcesFormArray(): FormArray { - return this.dataSettings.get('datasources') as FormArray; - } - registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -391,226 +402,200 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont writeValue(value: WidgetConfigComponentData): void { this.modelValue = value; - this.removeChangeSubscriptions(); + this.widgetType = this.modelValue?.widgetType; + this.setupConfig(this.isAdd); + } + + private setupConfig(isAdd = false) { if (this.modelValue) { - if (this.widgetType !== this.modelValue.widgetType) { - this.widgetType = this.modelValue.widgetType; - this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest); - this.buildForms(); + this.destroyBasicModeComponent(); + this.removeChangeSubscriptions(); + if (this.hasBasicModeDirective && this.widgetConfigMode === WidgetConfigMode.basic) { + this.setupBasicModeConfig(isAdd); + } else { + this.setupDefaultConfig(); } - const config = this.modelValue.config; - const layout = this.modelValue.layout; - if (config) { - this.selectedTab = 0; - const displayWidgetTitle = isDefined(config.showTitle) ? config.showTitle : false; - this.widgetSettings.patchValue({ - title: config.title, - showTitleIcon: isDefined(config.showTitleIcon) && displayWidgetTitle ? config.showTitleIcon : false, - titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '', - iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', - iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', - titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '', - showTitle: displayWidgetTitle, - dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, - enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, - backgroundColor: config.backgroundColor, - color: config.color, - padding: config.padding, - margin: config.margin, - widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {}, - widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '', - titleStyle: isDefined(config.titleStyle) ? config.titleStyle : { - fontSize: '16px', - fontWeight: 400 - }, - pageSize: isDefined(config.pageSize) ? config.pageSize : 1024, - units: config.units, - decimals: config.decimals, - noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : '', - showLegend: isDefined(config.showLegend) ? config.showLegend : - this.widgetType === widgetType.timeseries, - legendConfig: config.legendConfig || defaultLegendConfig(this.widgetType) - }, - {emitEvent: false} - ); - const showTitle: boolean = this.widgetSettings.get('showTitle').value; - if (showTitle) { - this.widgetSettings.get('titleTooltip').enable({emitEvent: false}); - this.widgetSettings.get('titleStyle').enable({emitEvent: false}); - this.widgetSettings.get('showTitleIcon').enable({emitEvent: false}); - } else { - this.widgetSettings.get('titleTooltip').disable({emitEvent: false}); - this.widgetSettings.get('titleStyle').disable({emitEvent: false}); - this.widgetSettings.get('showTitleIcon').disable({emitEvent: false}); - } - const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; - if (showTitleIcon) { - this.widgetSettings.get('titleIcon').enable({emitEvent: false}); - this.widgetSettings.get('iconColor').enable({emitEvent: false}); - this.widgetSettings.get('iconSize').enable({emitEvent: false}); - } else { - this.widgetSettings.get('titleIcon').disable({emitEvent: false}); - this.widgetSettings.get('iconColor').disable({emitEvent: false}); - this.widgetSettings.get('iconSize').disable({emitEvent: false}); - } - const showLegend: boolean = this.widgetSettings.get('showLegend').value; - if (showLegend) { - this.widgetSettings.get('legendConfig').enable({emitEvent: false}); - } else { - this.widgetSettings.get('legendConfig').disable({emitEvent: false}); - } - const actionsData: WidgetActionsData = { - actionsMap: config.actions || {}, - actionSources: this.modelValue.actionSources || {} - }; - this.actionsSettings.patchValue( - { - actionsData + } + } + + private setupBasicModeConfig(isAdd = false) { + const componentType = basicWidgetConfigComponentsMap[this.modelValue.basicModeDirective]; + if (!componentType) { + this.basicModeDirectiveError = this.translate.instant('widget-config.settings-component-not-found', + {selector: this.modelValue.basicModeDirective}); + } else { + const factory = this.cfr.resolveComponentFactory(componentType); + this.createBasicModeComponentTimeout = setTimeout(() => { + this.createBasicModeComponentTimeout = null; + this.basicModeComponentRef = this.basicModeContainer.createComponent(factory); + this.basicModeComponent = this.basicModeComponentRef.instance; + this.basicModeComponent.isAdd = isAdd; + this.basicModeComponent.widgetConfig = this.modelValue; + this.basicModeComponentChangeSubscription = this.basicModeComponent.widgetConfigChanged.subscribe((data) => { + this.modelValue = data; + this.propagateChange(this.modelValue); + }); + this.cd.markForCheck(); + }, 0); + } + } + + private destroyBasicModeComponent() { + this.basicModeDirectiveError = null; + if (this.basicModeComponentChangeSubscription) { + this.basicModeComponentChangeSubscription.unsubscribe(); + this.basicModeComponentChangeSubscription = null; + } + if (this.createBasicModeComponentTimeout) { + clearTimeout(this.createBasicModeComponentTimeout); + this.createBasicModeComponentTimeout = null; + } + if (this.basicModeComponentRef) { + this.basicModeComponentRef.destroy(); + this.basicModeComponentRef = null; + this.basicModeComponent = null; + } + if (this.basicModeContainer) { + this.basicModeContainer.clear(); + } + } + + private setupDefaultConfig() { + if (this.defaultConfigFormsType !== this.widgetType) { + this.defaultConfigFormsType = this.widgetType; + this.buildForms(); + } + this.buildHeader(); + const config = this.modelValue.config; + const layout = this.modelValue.layout; + if (config) { + const displayWidgetTitle = isDefined(config.showTitle) ? config.showTitle : false; + this.widgetSettings.patchValue({ + title: config.title, + showTitleIcon: isDefined(config.showTitleIcon) && displayWidgetTitle ? config.showTitleIcon : false, + titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '', + iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', + iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', + titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '', + showTitle: displayWidgetTitle, + dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, + enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, + backgroundColor: config.backgroundColor, + color: config.color, + padding: config.padding, + margin: config.margin, + widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {}, + widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '', + titleStyle: isDefined(config.titleStyle) ? config.titleStyle : { + fontSize: '16px', + fontWeight: 400 }, - {emitEvent: false} - ); - if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { - const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? - config.useDashboardTimewindow : true; - this.dataSettings.patchValue( - { useDashboardTimewindow }, {emitEvent: false} - ); - if (useDashboardTimewindow) { - this.dataSettings.get('displayTimewindow').disable({emitEvent: false}); - this.dataSettings.get('timewindow').disable({emitEvent: false}); - } else { - this.dataSettings.get('displayTimewindow').enable({emitEvent: false}); - this.dataSettings.get('timewindow').enable({emitEvent: false}); - } - this.dataSettings.patchValue( - { displayTimewindow: isDefined(config.displayTimewindow) ? - config.displayTimewindow : true }, {emitEvent: false} - ); - this.dataSettings.patchValue( - { timewindow: config.timewindow }, {emitEvent: false} - ); - } - if (this.modelValue.isDataEnabled) { - if (this.widgetType !== widgetType.rpc && - this.widgetType !== widgetType.alarm && - this.widgetType !== widgetType.static) { - const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray; - datasourcesFormArray.clear(); - if (config.datasources) { - config.datasources.forEach((datasource) => { - datasourcesFormArray.push(this.buildDatasourceForm(datasource)); - }); - } - } else if (this.widgetType === widgetType.rpc) { - let targetDeviceAliasId: string; - if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { - const aliasId = config.targetDeviceAliasIds[0]; - const entityAliases = this.aliasController.getEntityAliases(); - if (entityAliases[aliasId]) { - targetDeviceAliasId = entityAliases[aliasId].id; - } else { - targetDeviceAliasId = null; - } + pageSize: isDefined(config.pageSize) ? config.pageSize : 1024, + units: config.units, + decimals: config.decimals, + noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : '' + }, + {emitEvent: false} + ); + this.updateWidgetSettingsEnabledState(); + this.actionsSettings.patchValue( + { + actions: config.actions || {} + }, + {emitEvent: false} + ); + if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { + const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? + config.useDashboardTimewindow : true; + this.dataSettings.get('timewindowConfig').patchValue({ + useDashboardTimewindow, + displayTimewindow: isDefined(config.displayTimewindow) ? + config.displayTimewindow : true, + timewindow: config.timewindow + }, {emitEvent: false}); + } + if (this.modelValue.isDataEnabled) { + if (this.widgetType !== widgetType.rpc && + this.widgetType !== widgetType.alarm && + this.widgetType !== widgetType.static) { + this.dataSettings.patchValue({ datasources: config.datasources}, + {emitEvent: false}); + } else if (this.widgetType === widgetType.rpc) { + let targetDeviceAliasId: string; + if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { + const aliasId = config.targetDeviceAliasIds[0]; + const entityAliases = this.aliasController.getEntityAliases(); + if (entityAliases[aliasId]) { + targetDeviceAliasId = entityAliases[aliasId].id; } else { targetDeviceAliasId = null; } - this.targetDeviceSettings.patchValue({ - targetDeviceAliasId - }, {emitEvent: false}); - } else if (this.widgetType === widgetType.alarm) { - let alarmStatusList: AlarmSearchStatus[] = []; - if (isDefined(config.alarmStatusList) && config.alarmStatusList.length) { - alarmStatusList = config.alarmStatusList; - } else if (isDefined(config.alarmSearchStatus) && config.alarmSearchStatus !== AlarmSearchStatus.ANY) { - alarmStatusList = [config.alarmSearchStatus]; - } - this.dataSettings.patchValue( - { alarmStatusList }, {emitEvent: false} - ); - this.dataSettings.patchValue( - { alarmSeverityList: isDefined(config.alarmSeverityList) ? config.alarmSeverityList : [] }, {emitEvent: false} - ); - this.dataSettings.patchValue( - { alarmTypeList: isDefined(config.alarmTypeList) ? config.alarmTypeList : [] }, {emitEvent: false} - ); - this.dataSettings.patchValue( - { searchPropagatedAlarms: isDefined(config.searchPropagatedAlarms) ? - config.searchPropagatedAlarms : true }, {emitEvent: false} - ); - this.alarmSourceSettings.patchValue( - config.alarmSource, {emitEvent: false} - ); - const alarmSourceType: DatasourceType = this.alarmSourceSettings.get('type').value; - this.alarmSourceSettings.get('entityAliasId').setValidators( - alarmSourceType === DatasourceType.entity ? [Validators.required] : [] - ); - this.alarmSourceSettings.get('entityAliasId').updateValueAndValidity({emitEvent: false}); + } else { + targetDeviceAliasId = null; } - } - - this.updateSchemaForm(config.settings); - - if (layout) { - this.layoutSettings.patchValue( - { - mobileOrder: layout.mobileOrder, - mobileHeight: layout.mobileHeight, - mobileHide: layout.mobileHide, - desktopHide: layout.desktopHide - }, - {emitEvent: false} - ); - } else { - this.layoutSettings.patchValue( - { - mobileOrder: null, - mobileHeight: null, - mobileHide: false, - desktopHide: false - }, - {emitEvent: false} + this.targetDeviceSettings.patchValue({ + targetDeviceAliasId + }, {emitEvent: false}); + } else if (this.widgetType === widgetType.alarm) { + this.dataSettings.patchValue( + { alarmFilterConfig: isDefined(config.alarmFilterConfig) ? + config.alarmFilterConfig : + { statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true }, + alarmSource: config.alarmSource }, {emitEvent: false} ); } } - this.createChangeSubscriptions(); - } - } - public dataKeysOptional(datasource?: Datasource): boolean { - if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) { - return true; - } else { - return this.modelValue.typeParameters && this.modelValue.typeParameters.dataKeysOptional - && datasource?.type !== DatasourceType.entityCount; + this.updateSchemaForm(config.settings); + + if (layout) { + this.layoutSettings.patchValue( + { + mobileOrder: layout.mobileOrder, + mobileHeight: layout.mobileHeight, + mobileHide: layout.mobileHide, + desktopHide: layout.desktopHide + }, + {emitEvent: false} + ); + } else { + this.layoutSettings.patchValue( + { + mobileOrder: null, + mobileHeight: null, + mobileHide: false, + desktopHide: false + }, + {emitEvent: false} + ); + } } + this.createChangeSubscriptions(); } - private buildDatasourceForm(datasource?: Datasource): FormGroup { - const dataKeysRequired = !this.dataKeysOptional(datasource); - const datasourceFormGroup = this.fb.group( - { - type: [datasource ? datasource.type : null, [Validators.required]], - name: [datasource ? datasource.name : null, []], - entityAliasId: [datasource ? datasource.entityAliasId : null, - datasource && (datasource.type === DatasourceType.entity || - datasource.type === DatasourceType.entityCount) ? [Validators.required] : []], - filterId: [datasource ? datasource.filterId : null, []], - dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []] - } - ); - if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) { - datasourceFormGroup.addControl('latestDataKeys', this.fb.control(datasource ? datasource.latestDataKeys : null)); + private updateWidgetSettingsEnabledState() { + const showTitle: boolean = this.widgetSettings.get('showTitle').value; + const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; + if (showTitle) { + this.widgetSettings.get('title').enable({emitEvent: false}); + this.widgetSettings.get('titleTooltip').enable({emitEvent: false}); + this.widgetSettings.get('titleStyle').enable({emitEvent: false}); + this.widgetSettings.get('showTitleIcon').enable({emitEvent: false}); + } else { + this.widgetSettings.get('title').disable({emitEvent: false}); + this.widgetSettings.get('titleTooltip').disable({emitEvent: false}); + this.widgetSettings.get('titleStyle').disable({emitEvent: false}); + this.widgetSettings.get('showTitleIcon').disable({emitEvent: false}); + } + if (showTitle && showTitleIcon) { + this.widgetSettings.get('titleIcon').enable({emitEvent: false}); + this.widgetSettings.get('iconColor').enable({emitEvent: false}); + this.widgetSettings.get('iconSize').enable({emitEvent: false}); + } else { + this.widgetSettings.get('titleIcon').disable({emitEvent: false}); + this.widgetSettings.get('iconColor').disable({emitEvent: false}); + this.widgetSettings.get('iconSize').disable({emitEvent: false}); } - datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => { - datasourceFormGroup.get('entityAliasId').setValidators( - (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] - ); - const newDataKeysRequired = !this.dataKeysOptional(datasourceFormGroup.value); - datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []); - datasourceFormGroup.get('entityAliasId').updateValueAndValidity(); - datasourceFormGroup.get('dataKeys').updateValueAndValidity(); - }); - return datasourceFormGroup; } private updateSchemaForm(settings?: any) { @@ -633,7 +618,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont private updateDataSettings() { if (this.modelValue) { if (this.modelValue.config) { - Object.assign(this.modelValue.config, this.dataSettings.value); + let data = this.dataSettings.value; + if (data.timewindowConfig) { + const timewindowConfig: TimewindowConfigData = data.timewindowConfig; + data = {...data, ...timewindowConfig}; + delete data.timewindowConfig; + } + Object.assign(this.modelValue.config, data); } this.propagateChange(this.modelValue); } @@ -653,16 +644,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } } - private updateAlarmSourceSettings() { - if (this.modelValue) { - if (this.modelValue.config) { - const alarmSource: Datasource = this.alarmSourceSettings.value; - this.modelValue.config.alarmSource = alarmSource; - } - this.propagateChange(this.modelValue); - } - } - private updateWidgetSettings() { if (this.modelValue) { if (this.modelValue.config) { @@ -684,8 +665,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont private updateAdvancedSettings() { if (this.modelValue) { if (this.modelValue.config) { - const settings = this.advancedSettings.get('settings').value?.model; - this.modelValue.config.settings = settings; + this.modelValue.config.settings = this.advancedSettings.get('settings').value?.model; } this.propagateChange(this.modelValue); } @@ -694,50 +674,30 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont private updateActionSettings() { if (this.modelValue) { if (this.modelValue.config) { - const actions = (this.actionsSettings.get('actionsData').value as WidgetActionsData).actionsMap; - this.modelValue.config.actions = actions; + this.modelValue.config.actions = this.actionsSettings.get('actions').value; } this.propagateChange(this.modelValue); } } - public alarmTypeList(): string[] { - return this.dataSettings.get('alarmTypeList').value; + public get hasBasicModeDirective(): boolean { + return this.modelValue?.basicModeDirective?.length > 0; } - public removeAlarmType(type: string): void { - const types: string[] = this.dataSettings.get('alarmTypeList').value; - const index = types.indexOf(type); - if (index >= 0) { - types.splice(index, 1); - this.dataSettings.get('alarmTypeList').setValue(types); - this.dataSettings.get('alarmTypeList').markAsDirty(); - } + public get useDefinedBasicModeDirective(): boolean { + return this.modelValue?.basicModeDirective?.length && !this.basicModeDirectiveError; } - public addAlarmType(event: MatChipInputEvent): void { - const input = event.input; - const value = event.value; - - const types: string[] = this.dataSettings.get('alarmTypeList').value; - - if ((value || '').trim()) { - types.push(value.trim()); - this.dataSettings.get('alarmTypeList').setValue(types); - this.dataSettings.get('alarmTypeList').markAsDirty(); - } - - if (input) { - input.value = ''; - } + public get displayAppearance(): boolean { + return this.displayAppearanceDataSettings || this.displayAdvancedAppearance; } - public displayAdvanced(): boolean { + public get displayAdvancedAppearance(): boolean { return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema || !!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length); } - public displayTimewindowConfig(): boolean { + public get displayTimewindowConfig(): boolean { if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { return true; } else if (this.widgetType === widgetType.latest) { @@ -746,40 +706,30 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } } - public onlyHistoryTimewindow(): boolean { - if (this.widgetType === widgetType.latest) { - const datasources = this.dataSettings.get('datasources').value; - return datasourcesHasOnlyComparisonAggregation(datasources); - } else { - return false; - } + public get displayLimits(): boolean { + return this.widgetType !== widgetType.rpc && this.widgetType !== widgetType.alarm && + this.modelValue?.isDataEnabled && !this.modelValue?.typeParameters?.singleEntity; + } + + public get displayAppearanceDataSettings(): boolean { + return this.displayUnitsConfig || this.displayNoDataDisplayMessageConfig; } - public onDatasourceDrop(event: CdkDragDrop) { - const datasourcesFormArray = this.datasourcesFormArray(); - const datasourceForm = datasourcesFormArray.at(event.previousIndex); - datasourcesFormArray.removeAt(event.previousIndex); - datasourcesFormArray.insert(event.currentIndex, datasourceForm); + public get displayUnitsConfig(): boolean { + return this.widgetType === widgetType.latest || this.widgetType === widgetType.timeseries; } - public removeDatasource(index: number) { - this.datasourcesFormArray().removeAt(index); + public get displayNoDataDisplayMessageConfig(): boolean { + return this.widgetType !== widgetType.static && !this.modelValue?.typeParameters?.processNoDataByWidget; } - public addDatasource() { - let newDatasource: Datasource; - if (this.functionsOnly) { - newDatasource = deepClone(this.utils.getDefaultDatasource(this.modelValue.dataKeySettingsSchema.schema)); - newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function, this.modelValue.dataKeySettingsSchema)]; + public onlyHistoryTimewindow(): boolean { + if (this.widgetType === widgetType.latest) { + const datasources = this.dataSettings.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); } else { - newDatasource = { type: DatasourceType.entity, - dataKeys: [] - }; - } - if (this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) { - newDatasource.latestDataKeys = []; + return false; } - this.datasourcesFormArray().push(this.buildDatasourceForm(newDatasource)); } public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema): DataKey { @@ -794,7 +744,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont label = this.translate.instant(keyField.name); } } - label = this.genNextLabel(label); + const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources; + label = genNextLabel(label, datasources); const result: DataKey = { name: chip, type, @@ -819,41 +770,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } } - private genNextLabel(name: string): string { - let label = name; - let i = 1; - let matches = false; - const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources; - if (datasources) { - do { - matches = false; - datasources.forEach((datasource) => { - if (datasource) { - if (datasource.dataKeys) { - datasource.dataKeys.forEach((dataKey) => { - if (dataKey.label === label) { - i++; - label = name + ' ' + i; - matches = true; - } - }); - } - if (datasource.latestDataKeys) { - datasource.latestDataKeys.forEach((dataKey) => { - if (dataKey.label === label) { - i++; - label = name + ' ' + i; - matches = true; - } - }); - } - } - }); - } while (matches); - } - return label; - } - private genNextColor(): string { let i = 0; const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources; @@ -911,17 +827,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont ); } + private fetchEntityKeysForDevice(deviceId: string, dataKeyTypes: Array): Observable> { + const entityFilter = singleEntityFilterFromDeviceId(deviceId); + return this.entityService.getEntityKeysByEntityFilter( + entityFilter, + dataKeyTypes, [EntityType.DEVICE], + {ignoreLoading: true, ignoreErrors: true} + ).pipe( + catchError(() => of([])) + ); + } + private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { return this.aliasController.getAliasInfo(entityAliasId).pipe( - mergeMap((aliasInfo) => { - return this.entityService.getEntityKeysByEntityFilter( + mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, - dataKeyTypes, + dataKeyTypes, [], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) - ); - }), + )), catchError(() => of([] as Array)) ); } @@ -941,10 +866,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; } - public validate(c: FormControl) { - this.timeseriesKeyError = false; - this.datasourceError = []; - if (!this.dataSettings.valid) { + public validate(c: UntypedFormControl) { + if (this.basicModeComponent) { + if (!this.basicModeComponent.validateConfig()) { + return { + basicWidgetConfig: { + valid: false + } + }; + } + } else if (!this.dataSettings.valid) { return { dataSettings: { valid: false @@ -962,7 +893,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont valid: false } }; - } else if (!this.advancedSettings.valid || (this.displayAdvanced() && !this.modelValue.config.settings)) { + } else if (!this.advancedSettings.valid || (this.displayAdvancedAppearance && !this.modelValue.config.settings)) { return { advancedSettings: { valid: false @@ -978,54 +909,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } }; } - } else if (this.widgetType === widgetType.alarm && this.modelValue.isDataEnabled) { - if (!this.alarmSourceSettings.valid || !config.alarmSource) { - return { - alarmSource: { - valid: false - } - }; - } - } else if (this.widgetType !== widgetType.static && this.modelValue.isDataEnabled) { - if (!this.modelValue.typeParameters.datasourcesOptional && (!config.datasources || !config.datasources.length)) { - return { - datasources: { - valid: false - } - }; - } - if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) { - let valid = config.datasources.filter(datasource => datasource?.dataKeys?.length).length > 0; - if (!valid) { - this.timeseriesKeyError = true; - return { - timeseriesDataKeys: { - valid: false - } - }; - } else { - const emptyDatasources = config.datasources.filter(datasource => !datasource?.dataKeys?.length && - !datasource?.latestDataKeys?.length); - valid = emptyDatasources.length === 0; - if (!valid) { - for (const emptyDatasource of emptyDatasources) { - const i = config.datasources.indexOf(emptyDatasource); - this.datasourceError[i] = 'At least one data key should be specified'; - } - return { - dataKeys: { - valid: false - } - }; - } - } - } } } return null; } - - public extensionPanelIsOpen(value) { - this.openExtensionPanel = value; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index ad8f65a48b..23082a121b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -1,6 +1,6 @@ + + +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss new file mode 100644 index 0000000000..1782251f61 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + z-index: 10; + background: #F3F6FA; + .tb-preview-dashboard { + position: absolute; + top: 15%; + bottom: 15%; + left: 0; + right: 0; + } + .tb-preview-panel { + position: absolute; + top: 16px; + left: 24px; + } +} + +:host ::ng-deep { + tb-dashboard.tb-preview-dashboard { + .tb-dashboard-content { + background-color: transparent !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts new file mode 100644 index 0000000000..cd088e5744 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts @@ -0,0 +1,79 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { IAliasController, IStateController } from '@core/api/widget-api.models'; +import { Widget, WidgetConfig } from '@shared/models/widget.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-widget-preview', + templateUrl: './widget-preview.component.html', + styleUrls: ['./widget-preview.component.scss'] +}) +export class WidgetPreviewComponent extends PageComponent implements OnInit, OnChanges { + + @Input() + aliasController: IAliasController; + + @Input() + stateController: IStateController; + + @Input() + widget: Widget; + + @Input() + widgetConfig: WidgetConfig; + + widgets: Widget[]; + + constructor(protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.loadPreviewWidget(); + } + + ngOnChanges(changes: SimpleChanges): void { + let reloadPreviewWidget = false; + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['widget', 'widgetConfig'].includes(propName)) { + reloadPreviewWidget = true; + } + } + } + if (reloadPreviewWidget) { + this.loadPreviewWidget(); + } + } + + private loadPreviewWidget() { + const widget = deepClone(this.widget); + widget.sizeX = 24; + widget.sizeY = this.widget.sizeY * 2; + widget.row = 0; + widget.col = 0; + widget.config = this.widgetConfig; + this.widgets = [widget]; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html deleted file mode 100644 index a9742c0545..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html +++ /dev/null @@ -1,24 +0,0 @@ - -
- -
{{definedDirectiveError}}
- - -
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss deleted file mode 100644 index 2afac0944b..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host { - .tb-settings-directive-error { - font-size: 13px; - font-weight: 400; - color: rgb(221, 44, 0); - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts deleted file mode 100644 index 4337a5e07b..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts +++ /dev/null @@ -1,231 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { - AfterViewInit, - Component, ComponentFactoryResolver, - ComponentRef, - forwardRef, - Input, OnChanges, - OnDestroy, - OnInit, SimpleChanges, - ViewChild, - ViewContainerRef -} from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { - IRuleNodeConfigurationComponent, - RuleNodeConfiguration, - RuleNodeDefinition -} from '@shared/models/rule-node.models'; -import { Subscription } from 'rxjs'; -import { RuleChainService } from '@core/http/rule-chain.service'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { TranslateService } from '@ngx-translate/core'; -import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; -import { deepClone } from '@core/utils'; -import { RuleChainType } from '@shared/models/rule-chain.models'; -import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; -import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; -import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models'; -import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; -import { Dashboard } from '@shared/models/dashboard.models'; -import { WidgetService } from '@core/http/widget.service'; -import { IAliasController } from '@core/api/widget-api.models'; - -@Component({ - selector: 'tb-widget-settings', - templateUrl: './widget-settings.component.html', - styleUrls: ['./widget-settings.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetSettingsComponent), - multi: true - }] -}) -export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, OnDestroy, AfterViewInit, OnChanges { - - @ViewChild('definedSettingsContent', {read: ViewContainerRef, static: true}) definedSettingsContainer: ViewContainerRef; - - @ViewChild('jsonFormComponent') jsonFormComponent: JsonFormComponent; - - @Input() - disabled: boolean; - - @Input() - aliasController: IAliasController; - - @Input() - dashboard: Dashboard; - - @Input() - widget: Widget; - - private settingsDirective: string; - - definedDirectiveError: string; - - widgetSettingsFormGroup: FormGroup; - - changeSubscription: Subscription; - - private definedSettingsComponentRef: ComponentRef; - private definedSettingsComponent: IWidgetSettingsComponent; - - private widgetSettingsFormData: JsonFormComponentData; - - private propagateChange = (v: any) => { }; - - constructor(private translate: TranslateService, - private cfr: ComponentFactoryResolver, - private widgetService: WidgetService, - private fb: FormBuilder) { - this.widgetSettingsFormGroup = this.fb.group({ - settings: [null, Validators.required] - }); - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { - } - - ngOnInit(): void { - } - - ngOnChanges(changes: SimpleChanges): void { - for (const propName of Object.keys(changes)) { - const change = changes[propName]; - if (!change.firstChange && change.currentValue !== change.previousValue) { - if (propName === 'widget') { - if (this.definedSettingsComponent) { - this.definedSettingsComponent.widget = this.widget; - } - } - if (propName === 'dashboard') { - if (this.definedSettingsComponent) { - this.definedSettingsComponent.dashboard = this.dashboard; - } - } - if (propName === 'aliasController') { - if (this.definedSettingsComponent) { - this.definedSettingsComponent.aliasController = this.aliasController; - } - } - } - } - } - - ngOnDestroy(): void { - if (this.definedSettingsComponentRef) { - this.definedSettingsComponentRef.destroy(); - } - } - - ngAfterViewInit(): void { - } - - setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - if (this.disabled) { - this.widgetSettingsFormGroup.disable({emitEvent: false}); - } else { - this.widgetSettingsFormGroup.enable({emitEvent: false}); - } - } - - writeValue(value: JsonFormComponentData): void { - this.widgetSettingsFormData = value; - if (this.changeSubscription) { - this.changeSubscription.unsubscribe(); - this.changeSubscription = null; - } - if (this.settingsDirective !== this.widgetSettingsFormData.settingsDirective) { - this.settingsDirective = this.widgetSettingsFormData.settingsDirective; - this.validateDefinedDirective(); - } - if (this.definedSettingsComponent) { - this.definedSettingsComponent.settings = this.widgetSettingsFormData.model; - this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { - this.updateModel(settings); - }); - } else { - this.widgetSettingsFormGroup.get('settings').patchValue(this.widgetSettingsFormData, {emitEvent: false}); - this.changeSubscription = this.widgetSettingsFormGroup.get('settings').valueChanges.subscribe( - (widgetSettingsFormData: JsonFormComponentData) => { - this.updateModel(widgetSettingsFormData.model); - } - ); - } - } - - useDefinedDirective(): boolean { - return this.settingsDirective && - this.settingsDirective.length && !this.definedDirectiveError; - } - - useJsonForm(): boolean { - return !this.settingsDirective || !this.settingsDirective.length; - } - - private updateModel(settings: WidgetSettings) { - this.widgetSettingsFormData.model = settings; - if (this.definedSettingsComponent || this.widgetSettingsFormGroup.valid) { - this.propagateChange(this.widgetSettingsFormData); - } else { - this.propagateChange(null); - } - } - - private validateDefinedDirective() { - if (this.definedSettingsComponentRef) { - this.definedSettingsComponentRef.destroy(); - this.definedSettingsComponentRef = null; - this.definedSettingsComponent = null; - } - if (this.settingsDirective && this.settingsDirective.length) { - const componentType = widgetSettingsComponentsMap[this.settingsDirective]; - if (!componentType) { - this.definedDirectiveError = this.translate.instant('widget-config.settings-component-not-found', - {selector: this.settingsDirective}); - } else { - if (this.changeSubscription) { - this.changeSubscription.unsubscribe(); - this.changeSubscription = null; - } - this.definedSettingsContainer.clear(); - const factory = this.cfr.resolveComponentFactory(componentType); - this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory); - this.definedSettingsComponent = this.definedSettingsComponentRef.instance; - this.definedSettingsComponent.aliasController = this.aliasController; - this.definedSettingsComponent.dashboard = this.dashboard; - this.definedSettingsComponent.widget = this.widget; - this.definedSettingsComponent.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); - this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { - this.updateModel(settings); - }); - } - } - } - - validate() { - if (this.useDefinedDirective()) { - this.definedSettingsComponent.validate(); - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html index 0d20c1de2b..cf287790a0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -1,6 +1,6 @@ -
- -
- -
- Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}} +
{{ noDataDisplayMessageText }}
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget.component.scss index c7904be90b..ac942225c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index baa0664a31..7e9b9cb6e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -29,18 +29,16 @@ import { OnChanges, OnDestroy, OnInit, + Optional, Renderer2, SimpleChanges, + Type, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; import { DashboardWidget } from '@home/models/dashboard-component.models'; import { - defaultLegendConfig, - LegendConfig, - LegendData, - LegendPosition, Widget, WidgetActionDescriptor, widgetActionSources, @@ -89,7 +87,7 @@ import { import { EntityId } from '@shared/models/id/entity-id'; import { ActivatedRoute, Router } from '@angular/router'; import cssjs from '@core/css/css'; -import { ResourcesService } from '@core/services/resources.service'; +import { ModulesWithFactories, ResourcesService } from '@core/services/resources.service'; import { catchError, map, switchMap } from 'rxjs/operators'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { TimeService } from '@core/services/time.service'; @@ -115,6 +113,8 @@ import { DialogService } from '@core/services/dialog.service'; import { PopoverPlacement } from '@shared/components/popover.models'; import { TbPopoverService } from '@shared/components/popover.service'; import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens'; +import { MODULES_MAP } from '@shared/models/constants'; +import { IModulesMap } from '@modules/common/modules-map.models'; @Component({ selector: 'tb-widget', @@ -148,13 +148,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI displayNoData = false; noDataDisplayMessageText: string; - displayLegend: boolean; - legendConfig: LegendConfig; - legendData: LegendData; - isLegendFirst: boolean; - legendContainerLayoutType: string; - legendStyle: {[klass: string]: any}; - dynamicWidgetComponentRef: ComponentRef; dynamicWidgetComponent: IDynamicWidgetComponent; @@ -190,6 +183,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI private popoverService: TbPopoverService, @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType, @Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType, + @Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap, private widgetService: WidgetService, private resources: ResourcesService, private timeService: TimeService, @@ -215,57 +209,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.widget = this.dashboardWidget.widget; - this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend - : this.widget.type === widgetType.timeseries; - - this.legendContainerLayoutType = 'column'; - - if (this.displayLegend) { - this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type); - this.legendData = { - keys: [], - data: [] - }; - if (this.legendConfig.position === LegendPosition.top || - this.legendConfig.position === LegendPosition.bottom) { - this.legendContainerLayoutType = 'column'; - this.isLegendFirst = this.legendConfig.position === LegendPosition.top; - } else { - this.legendContainerLayoutType = 'row'; - this.isLegendFirst = this.legendConfig.position === LegendPosition.left; - } - switch (this.legendConfig.position) { - case LegendPosition.top: - this.legendStyle = { - paddingBottom: '8px', - maxHeight: '50%', - overflowY: 'auto' - }; - break; - case LegendPosition.bottom: - this.legendStyle = { - paddingTop: '8px', - maxHeight: '50%', - overflowY: 'auto' - }; - break; - case LegendPosition.left: - this.legendStyle = { - paddingRight: '0px', - maxWidth: '50%', - overflowY: 'auto' - }; - break; - case LegendPosition.right: - this.legendStyle = { - paddingLeft: '0px', - maxWidth: '50%', - overflowY: 'auto' - }; - break; - } - } - const actionDescriptorsBySourceId: {[actionSourceId: string]: Array} = {}; if (this.widget.config.actions) { for (const actionSourceId of Object.keys(this.widget.config.actions)) { @@ -458,15 +401,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } } - public onLegendKeyHiddenChange(index: number) { - for (const id of Object.keys(this.widgetContext.subscriptions)) { - const subscription = this.widgetContext.subscriptions[id]; - subscription.updateDataVisibility(index); - } - } - private loadFromWidgetInfo() { - this.widgetContext.widgetNamespace = `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`; + this.widgetContext.widgetNamespace = + `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`; const elem = this.elementRef.nativeElement; elem.classList.add('tb-widget'); elem.classList.add(this.widgetContext.widgetNamespace); @@ -510,6 +447,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.onInit(); }, (err) => { + this.widgetContext.inited = true; // console.log(err); } ); @@ -691,7 +629,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI private initialize(): Observable { - const initSubject = new ReplaySubject(); + const initSubject = new ReplaySubject(); this.rxSubscriptions.push(this.widgetContext.aliasController.entityAliasesChanged.subscribe( (aliasIds) => { @@ -733,9 +671,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.createDefaultSubscription().subscribe( () => { this.subscriptionInited = true; - this.configureDynamicWidgetComponent(); - initSubject.next(); - initSubject.complete(); + try { + this.configureDynamicWidgetComponent(); + initSubject.next(); + initSubject.complete(); + } catch (err) { + initSubject.error(err); + } }, (err) => { this.subscriptionInited = true; @@ -745,9 +687,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } else { this.loadingData = false; this.subscriptionInited = true; - this.configureDynamicWidgetComponent(); - initSubject.next(); - initSubject.complete(); + try { + this.configureDynamicWidgetComponent(); + initSubject.next(); + initSubject.complete(); + } catch (err) { + initSubject.error(err); + } } return initSubject.asObservable(); } @@ -801,12 +747,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector); this.cd.detectChanges(); } catch (e) { - console.error(e); if (this.dynamicWidgetComponentRef) { this.dynamicWidgetComponentRef.destroy(); this.dynamicWidgetComponentRef = null; } this.widgetContentContainer.clear(); + this.handleWidgetException(e); + this.widgetComponentService.clearWidgetInfo(this.widgetInfo, this.widget.bundleAlias, this.widget.typeAlias, + this.widget.isSystemType); + throw e; } if (this.dynamicWidgetComponentRef) { @@ -866,9 +815,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } this.createSubscription(options, subscribe).subscribe( (subscription) => { - if (useDefaultComponents) { - this.defaultSubscriptionOptions(subscription, options); - } createSubscriptionSubject.next(subscription); createSubscriptionSubject.complete(); }, @@ -886,8 +832,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI ? this.widget.config.displayTimewindow : !options.useDashboardTimewindow; options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow; options.legendConfig = null; - if (this.displayLegend) { - options.legendConfig = this.legendConfig; + if (this.widget.config.settings.showLegend === true) { + options.legendConfig = this.widget.config.settings.legendConfig; } options.decimals = this.widgetContext.decimals; options.units = this.widgetContext.units; @@ -956,17 +902,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI }); } }; - - } - - private defaultSubscriptionOptions(subscription: IWidgetSubscription, options: WidgetSubscriptionOptions) { - if (this.displayLegend) { - this.legendData = subscription.legendData; - } } private createDefaultSubscription(): Observable { - const createSubscriptionSubject = new ReplaySubject(); + const createSubscriptionSubject = new ReplaySubject(); let options: WidgetSubscriptionOptions; if (this.widget.type !== widgetType.rpc && this.widget.type !== widgetType.static) { const comparisonSettings: WidgetComparisonSettings = this.widgetContext.settings; @@ -993,7 +932,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.createSubscription(options).subscribe( (subscription) => { - this.defaultSubscriptionOptions(subscription, options); // backward compatibility this.widgetContext.datasources = subscription.datasources; @@ -1159,8 +1097,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI if (isDefined(customHtml) && customHtml.length > 0) { htmlTemplate = customHtml; } - this.loadCustomActionResources(actionNamespace, customCss, customResources).subscribe( - () => { + this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({ + next: () => { if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { try { if (!additionalParams) { @@ -1168,16 +1106,17 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction); + this.widgetContext.customDialog.setAdditionalModules(descriptor.customModules); customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); } catch (e) { console.error(e); } } }, - (errorMessages: string[]) => { + error: (errorMessages: string[]) => { this.processResourcesLoadErrors(errorMessages); } - ); + }); break; case WidgetActionType.mobileAction: const mobileAction = descriptor.mobileAction; @@ -1472,33 +1411,70 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } } - private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array): Observable { + private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array, + actionDescriptor: WidgetActionDescriptor): Observable { + const resourceTasks: Observable[] = []; + const modulesTasks: Observable[] = []; + if (isDefined(customCss) && customCss.length > 0) { this.cssParser.cssPreviewNamespace = actionNamespace; this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace'); } - const resourceTasks: Observable[] = []; + if (isDefined(customResources) && customResources.length > 0) { - customResources.forEach((resource) => { - resourceTasks.push( - this.resources.loadResource(resource.url).pipe( - catchError(e => of(`Failed to load custom action resource: '${resource.url}'`)) - ) - ); + customResources.forEach(resource => { + if (resource.isModule) { + modulesTasks.push( + this.resources.loadFactories(resource.url, this.modulesMap).pipe( + catchError((e: Error) => of(e?.message ? e.message : `Failed to load custom action resource module: '${resource.url}'`)) + ) + ); + } else { + resourceTasks.push( + this.resources.loadResource(resource.url).pipe( + catchError(() => of(`Failed to load custom action resource: '${resource.url}'`)) + ) + ); + } }); - return forkJoin(resourceTasks).pipe( - switchMap(msgs => { - let errors: string[]; - if (msgs && msgs.length) { - errors = msgs.filter(msg => msg && msg.length > 0); + + if (modulesTasks.length) { + const modulesObservable: Observable[]> = forkJoin(modulesTasks).pipe( + map(res => { + const msg = res.find(r => typeof r === 'string'); + if (msg) { + return msg as string; + } else { + const modulesWithFactoriesList = res as ModulesWithFactories[]; + const resModulesWithFactories: ModulesWithFactories = { + modules: modulesWithFactoriesList.map(mf => mf.modules).flat(), + factories: modulesWithFactoriesList.map(mf => mf.factories).flat() + }; + return resModulesWithFactories.modules; } - if (errors && errors.length) { - return throwError(errors); + }) + ); + + resourceTasks.push(modulesObservable.pipe( + map((resolvedModules) => { + if (typeof resolvedModules === 'string') { + return resolvedModules; } else { - return of(null); + actionDescriptor.customModules = resolvedModules; + return null; } - } - )); + }))); + } + + return forkJoin(resourceTasks).pipe( + switchMap(msgs => { + const errors = msgs.filter(msg => msg && msg.length > 0); + if (errors.length > 0) { + return throwError(() => errors); + } else { + return of(null); + }} + )); } else { return of(null); } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index a016560b8d..c1485cf450 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -1,6 +1,6 @@ -
- -

device.add-device-text

- -
- -
- - -
-
- - - check - - -
- {{ 'device.wizard.device-details' | translate}} -
- - device.name - - - {{ 'device.name-required' | translate }} - - - {{ 'device.name-max-length' | translate }} - - - - device.label - - - {{ 'device.label-max-length' | translate }} - - -
- - - device.wizard.existing-device-profile - - - device.wizard.new-device-profile - - -
- - - - device-profile.new-device-profile-name - - - {{ 'device-profile.new-device-profile-name-required' | translate }} - - -
-
- - -
-
- - -
+ +

device.add-device-text

+ +
+ +
+ + +
+
+ + + check + + + + {{ 'device.wizard.device-details' | translate}} +
+ + device.name + + + {{ 'device.name-required' | translate }} + + + {{ 'device.name-max-length' | translate }} + + + + device.label + + + {{ 'device.label-max-length' | translate }} + + +
+ + + device.wizard.existing-device-profile + + + device.wizard.new-device-profile + + +
+ + + + device-profile.new-device-profile-name + + + {{ 'device-profile.new-device-profile-name-required' | translate }} + +
-
- - {{ 'device.is-gateway' | translate }} - - - {{ 'device.overwrite-activity-time' | translate }} - +
+ +
- - device.description - - -
- -
- -
- {{ 'device-profile.transport-configuration' | translate }} - device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} - - - - {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} - - - - -
-
- -
- {{'device-profile.alarm-rules-with-count' | translate: - {count: alarmRulesFormGroup.get('alarms').value ? - alarmRulesFormGroup.get('alarms').value.length : 0} }} - - -
-
- -
- {{ 'device-profile.device-provisioning' | translate }} - - -
-
- - {{ 'device.credentials' | translate }} -
- {{ 'device.wizard.add-credentials' | translate }} - - -
-
- - {{ 'customer.customer' | translate }} -
- - -
-
-
-
-
+
+ + +
+
+
+ + {{ 'device.is-gateway' | translate }} + + + {{ 'device.overwrite-activity-time' | translate }} + +
+ + device.description + + +
+ +
+ +
+ {{ 'device-profile.transport-configuration' | translate }} + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} + + + {{ 'device-profile.transport-type-required' | translate }} + + + + +
+
+ +
+ {{'device-profile.alarm-rules-with-count' | translate: + {count: alarmRulesFormGroup.get('alarms').value ? + alarmRulesFormGroup.get('alarms').value.length : 0} }} + + +
+
+ +
+ {{ 'device-profile.device-provisioning' | translate }} + + +
+
+ + {{ 'device.credentials' | translate }} +
+ {{ 'device.wizard.add-credentials' | translate }} + + +
+
+ + {{ 'customer.customer' | translate }} +
+ + +
+
+
+
+
+
@@ -191,8 +191,8 @@ [disabled]="(isLoading$ | async)" (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}
- -
+ +
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss index ffeaad5acd..0fe18467fd 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,18 @@ */ @import "../../../../../scss/constants"; -:host-context(.tb-fullscreen-dialog .mat-dialog-container) { +:host { + height: 100%; + display: grid; + + .dialog-actions-row { + padding: 8px; + } +} + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { @media #{$mat-lt-sm} { - .mat-dialog-content { + .mat-mdc-dialog-content { max-height: 75vh; } } @@ -28,7 +37,7 @@ } :host ::ng-deep { - .mat-dialog-content { + .mat-mdc-dialog-content { display: flex; flex-direction: column; height: 100%; @@ -36,37 +45,21 @@ .mat-stepper-horizontal { display: flex; - flex-direction: column; height: 100%; overflow: hidden; - @media #{$mat-lt-sm} { - .mat-step-label { - white-space: normal; - overflow: visible; - .mat-step-text-label { - overflow: visible; - } - } + + .mat-horizontal-stepper-wrapper { + flex: 1 1 100%; } + .mat-horizontal-content-container { - height: 530px; + height: 680px; max-height: 100%; width: 100%;; overflow-y: auto; + scrollbar-gutter: stable; @media #{$mat-gt-sm} { - min-width: 800px; - } - } - .mat-horizontal-stepper-content[aria-expanded=true] { - height: 100%; - form { - height: 100%; - } - } - - tb-device-profile-autocomplete, tb-queue-autocomplete{ - .mat-form-field-wrapper{ - width: 180px !important; + min-width: 500px; } } } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 694deabd96..77916d379c 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { @@ -33,7 +33,7 @@ import { deviceTransportTypeHintMap, deviceTransportTypeTranslationMap } from '@shared/models/device.models'; -import { MatHorizontalStepper } from '@angular/material/stepper'; +import { MatStepper } from '@angular/material/stepper'; import { AddEntityDialogData } from '@home/models/entity/entity-component.models'; import { BaseData, HasId } from '@shared/models/base-data'; import { EntityType } from '@shared/models/entity-type.models'; @@ -59,7 +59,7 @@ import { deepTrim } from '@core/utils'; export class DeviceWizardDialogComponent extends DialogComponent implements OnDestroy, ErrorStateMatcher { - @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatHorizontalStepper; + @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper; selectedIndex = 0; @@ -75,19 +75,19 @@ export class DeviceWizardDialogComponent extends deviceTransportTypeHints = deviceTransportTypeHintMap; - deviceWizardFormGroup: FormGroup; + deviceWizardFormGroup: UntypedFormGroup; - transportConfigFormGroup: FormGroup; + transportConfigFormGroup: UntypedFormGroup; - alarmRulesFormGroup: FormGroup; + alarmRulesFormGroup: UntypedFormGroup; - provisionConfigFormGroup: FormGroup; + provisionConfigFormGroup: UntypedFormGroup; - credentialsFormGroup: FormGroup; + credentialsFormGroup: UntypedFormGroup; - customerFormGroup: FormGroup; + customerFormGroup: UntypedFormGroup; - labelPosition: MatHorizontalStepper['labelPosition'] = 'end'; + labelPosition: MatStepper['labelPosition'] = 'end'; serviceType = ServiceType.TB_RULE_ENGINE; @@ -102,7 +102,7 @@ export class DeviceWizardDialogComponent extends private deviceProfileService: DeviceProfileService, private deviceService: DeviceService, private breakpointObserver: BreakpointObserver, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store, router, dialogRef); this.deviceWizardFormGroup = this.fb.group({ name: ['', [Validators.required, Validators.maxLength(255)]], @@ -205,7 +205,7 @@ export class DeviceWizardDialogComponent extends this.subscriptions.forEach(s => s.unsubscribe()); } - isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const customErrorState = !!(control && control.invalid); return originalErrorState || customErrorState; diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html index bc8aadedae..2588c38ba3 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html @@ -1,6 +1,6 @@ - - {{section.icon}} - + + {{section.icon}} + {{section.name | translate}} diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.scss b/ui-ngx/src/app/modules/home/menu/menu-link.component.scss index da8df4b469..66df772d2d 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.scss +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts index cae14d32b6..f2d43248bd 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html index 0635205581..de4f16b163 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -1,6 +1,6 @@ - - {{section.icon}} - + + {{section.icon}} + {{section.name | translate}} + [ngClass]="{'tb-toggled' : section.opened}">
  • diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss index a076fa77d6..93d0276639 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ .tb-toggle-icon { display: inline-block; width: 15px; + min-width: 15px; margin: auto 0 auto auto; background-size: 100% auto; transition: transform .3s ease-in-out; @@ -32,11 +33,11 @@ z-index: 1; overflow: hidden; - transition: .75s cubic-bezier(.35, 0, .25, 1); + transition: .45s cubic-bezier(.35, 0, .25, 1); transition-property: height; - a.mat-button { + a.mat-mdc-button { padding: 0 16px 0 32px; font-weight: 500; text-transform: none !important; diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts index 76a818c16b..70d711469a 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -17,6 +17,9 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { MenuSection } from '@core/services/menu.models'; import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActionPreferencesUpdateOpenedMenuSection } from '@core/auth/auth.actions'; @Component({ selector: 'tb-menu-toggle', @@ -28,24 +31,27 @@ export class MenuToggleComponent implements OnInit { @Input() section: MenuSection; - constructor(private router: Router) { + constructor(private router: Router, + private store: Store) { } ngOnInit() { } - sectionActive(): boolean { - return this.router.isActive(this.section.path, false); - } - sectionHeight(): string { - if (this.router.isActive(this.section.path, false)) { - return this.section.height; + if (this.section.opened) { + return this.section.pages.length * 40 + 'px'; } else { return '0px'; } } + toggleSection(event: MouseEvent) { + event.stopPropagation(); + this.section.opened = !this.section.opened; + this.store.dispatch(new ActionPreferencesUpdateOpenedMenuSection({path: this.section.path, opened: this.section.opened})); + } + trackBySectionPages(index: number, section: MenuSection){ return section.id; } diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.html b/ui-ngx/src/app/modules/home/menu/side-menu.component.html index 8a9f07f3c9..b41b05ca76 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.html +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.html @@ -1,6 +1,6 @@
    - - -
    - admin.general-settings -
    -
    + + + +
    + admin.general-settings +
    +
    +
    - +
    @@ -35,10 +37,10 @@ {{ 'admin.base-url-required' | translate }} - + {{ 'admin.prohibit-different-url' | translate }} -
    admin.prohibit-different-url-hint
    +
    admin.prohibit-different-url-hint
    - -
    -
    - -
    -
    -
    + +
    + + + +
    + +
    +
    + + +
    + + +
    +
    +
    +
    + + + + + admin.slack-settings + + +
    +
    + + +
    + +
    +
    +
    + + admin.slack-api-token + + +
    +
    + +
    +
    +
    +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss index da8df4b469..66df772d2d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts index bfd1a38e14..e56f02c4e4 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; @@ -25,43 +25,62 @@ import { AdminService } from '@core/http/admin.service'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { MatDialog } from '@angular/material/dialog'; import { SendTestSmsDialogComponent, SendTestSmsDialogData } from '@home/pages/admin/send-test-sms-dialog.component'; +import { NotificationSettings } from '@shared/models/notification.models'; +import { deepTrim, isEmptyStr } from '@core/utils'; +import { NotificationService } from '@core/http/notification.service'; +import { Authority } from '@shared/models/authority.enum'; +import { AuthUser } from '@shared/models/user.model'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @Component({ selector: 'tb-sms-provider', templateUrl: './sms-provider.component.html', styleUrls: ['./sms-provider.component.scss', './settings-card.scss'] }) -export class SmsProviderComponent extends PageComponent implements OnInit, HasConfirmForm { +export class SmsProviderComponent extends PageComponent implements HasConfirmForm { smsProvider: FormGroup; - adminSettings: AdminSettings; + private adminSettings: AdminSettings; + + slackSettingsForm: FormGroup; + private notificationSettings: NotificationSettings; + + private readonly authUser: AuthUser; constructor(protected store: Store, private router: Router, private adminService: AdminService, + private notificationService: NotificationService, private dialog: MatDialog, public fb: FormBuilder) { super(store); - } - - ngOnInit() { + this.authUser = getCurrentAuthUser(this.store); this.buildSmsProviderForm(); - this.adminService.getAdminSettings('sms', {ignoreErrors: true}).subscribe( - (adminSettings) => { - this.adminSettings = adminSettings; - this.smsProvider.reset({configuration: this.adminSettings.jsonValue}); - }, - () => { - this.adminSettings = { - key: 'sms', - jsonValue: null - }; - this.smsProvider.reset({configuration: this.adminSettings.jsonValue}); + this.buildGeneralServerSettingsForm(); + this.notificationService.getNotificationSettings().subscribe( + (settings) => { + this.notificationSettings = settings; + this.slackSettingsForm.reset(this.notificationSettings); } ); + if (this.isSysAdmin()) { + this.adminService.getAdminSettings('sms', {ignoreErrors: true}).subscribe({ + next: adminSettings => { + this.adminSettings = adminSettings; + this.smsProvider.reset({configuration: this.adminSettings.jsonValue}); + }, + error: () => { + this.adminSettings = { + key: 'sms', + jsonValue: null + }; + this.smsProvider.reset({configuration: this.adminSettings.jsonValue}); + } + }); + } } - buildSmsProviderForm() { + private buildSmsProviderForm() { this.smsProvider = this.fb.group({ configuration: [null, [Validators.required]] }); @@ -89,7 +108,42 @@ export class SmsProviderComponent extends PageComponent implements OnInit, HasCo } confirmForm(): FormGroup { - return this.smsProvider; + return this.smsProvider.dirty ? this.smsProvider : this.slackSettingsForm; + } + + private buildGeneralServerSettingsForm() { + this.slackSettingsForm = this.fb.group({ + deliveryMethodsConfigs: this.fb.group({ + SLACK: this.fb.group({ + botToken: [''] + }) + }) + }); + this.registerDisableOnLoadFormControl(this.slackSettingsForm.get('deliveryMethodsConfigs')); + } + + saveNotification(): void { + this.notificationSettings = deepTrim({ + ...this.notificationSettings, + ...this.slackSettingsForm.value + }); + // eslint-disable-next-line guard-for-in + for (const method in this.notificationSettings.deliveryMethodsConfigs) { + const keys = Object.keys(this.notificationSettings.deliveryMethodsConfigs[method]); + if (keys.some(item => isEmptyStr(this.notificationSettings.deliveryMethodsConfigs[method][item]))) { + delete this.notificationSettings.deliveryMethodsConfigs[method]; + } else { + this.notificationSettings.deliveryMethodsConfigs[method].method = method; + } + } + this.notificationService.saveNotificationSettings(this.notificationSettings).subscribe(setting => { + this.notificationSettings = setting; + this.slackSettingsForm.reset(this.notificationSettings); + }); + } + + isSysAdmin(): boolean { + return this.authUser.authority === Authority.SYS_ADMIN; } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index d658de9589..69cacf3604 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -1,6 +1,6 @@
    - - -
    - admin.2fa.2fa - -
    -
    -
    + + + + admin.2fa.2fa + + +
    +
    @@ -38,7 +38,7 @@ {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} @@ -153,7 +153,7 @@ - {{ 'admin.2fa.verification-code-check-rate-limit' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss index 254cc51c22..fea3045098 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - @import "../../../../../scss/constants"; :host { - mat-card.settings-card { + .mat-mdc-card.settings-card { @media #{$mat-md} { width: 90%; } @@ -58,7 +57,7 @@ height: 48px; } - .mat-slide-toggle { + .mat-mdc-slide-toggle { margin-right: 8px; } } @@ -72,6 +71,13 @@ } :host ::ng-deep { + + .mat-mdc-form-field { + .mat-mdc-form-field-infix { + width: 100%; + } + } + .mat-expansion-panel { .mat-expansion-panel-content { font-size: 16px; diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts index 9e79d2d725..6bd630ae88 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { PageComponent } from '@shared/components/page.component'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; import { TwoFactorAuthProviderConfigForm, @@ -43,7 +43,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI private readonly destroy$ = new Subject(); private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]; - twoFaFormGroup: FormGroup; + twoFaFormGroup: UntypedFormGroup; twoFactorAuthProviderType = TwoFactorAuthProviderType; twoFactorAuthProvidersData = twoFactorAuthProvidersData; @@ -51,7 +51,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI constructor(protected store: Store, private twoFaService: TwoFactorAuthenticationService, - private fb: FormBuilder) { + private fb: UntypedFormBuilder) { super(store); } @@ -68,7 +68,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI this.destroy$.complete(); } - confirmForm(): FormGroup { + confirmForm(): UntypedFormGroup { return this.twoFaFormGroup; } @@ -99,9 +99,9 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI $event.stopPropagation(); } if (currentState) { - this.getByIndexPanel(index).close(); - } else { this.getByIndexPanel(index).open(); + } else { + this.getByIndexPanel(index).close(); } } @@ -109,8 +109,8 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI return item; } - get providersForm(): FormArray { - return this.twoFaFormGroup.get('providers') as FormArray; + get providersForm(): UntypedFormArray { + return this.twoFaFormGroup.get('providers') as UntypedFormArray; } private build2faSettingsForm(): void { diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts new file mode 100644 index 0000000000..8b448f1c2d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgModule } from '@angular/core'; +import { Resolve, RouterModule, Routes } from '@angular/router'; +import { Authority } from '@shared/models/authority.enum'; +import { Observable } from 'rxjs'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component'; +import { AlarmsMode } from '@shared/models/alarm.models'; + +@Injectable() +export class OAuth2LoginProcessingUrlResolver implements Resolve { + + constructor(private oauth2Service: OAuth2Service) { + } + + resolve(): Observable { + return this.oauth2Service.getLoginProcessingUrl(); + } +} + +const routes: Routes = [ + { + path: 'alarms', + component: AlarmTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'alarm.alarms', + breadcrumb: { + label: 'alarm.alarms', + icon: 'mdi:alert-outline' + }, + isPage: true, + alarmsMode: AlarmsMode.ALL + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [] +}) +export class AlarmRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts new file mode 100644 index 0000000000..cd6f257833 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { AlarmRoutingModule } from '@home/pages/alarm/alarm-routing.module'; + +@NgModule({ + declarations: [], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule, + AlarmRoutingModule + ] +}) +export class AlarmModule { } diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts index f1db71d466..a38fbf9958 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.component.html b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.component.html index 6563b6d941..7c9cc1bba1 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.component.html +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.component.html @@ -1,6 +1,6 @@ { diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html index 452dce8bcd..22ab3d855e 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html @@ -1,6 +1,6 @@ - - + + diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss deleted file mode 100644 index 25cf304552..0000000000 --- a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import '../../../../../scss/constants'; - -:host { - flex: 1; - display: flex; - justify-content: flex-start; - min-width: 150px; -} - -:host ::ng-deep { - tb-device-profile-autocomplete { - width: 100%; - - mat-form-field { - font-size: 16px; - - .mat-form-field-wrapper { - padding-bottom: 0; - } - - .mat-form-field-underline { - bottom: 0; - } - - @media #{$mat-xs} { - width: 100%; - - .mat-form-field-infix { - width: auto !important; - } - } - } - } -} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts index e2a1cd7bde..e169de5f07 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -18,14 +18,14 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; -import { DeviceInfo } from '@app/shared/models/device.models'; +import { DeviceInfo, DeviceInfoFilter } from '@app/shared/models/device.models'; import { EntityType } from '@shared/models/entity-type.models'; import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id'; @Component({ selector: 'tb-device-table-header', templateUrl: './device-table-header.component.html', - styleUrls: ['./device-table-header.component.scss'] + styleUrls: [] }) export class DeviceTableHeaderComponent extends EntityTableHeaderComponent { @@ -35,8 +35,8 @@ export class DeviceTableHeaderComponent extends EntityTableHeaderComponent { protected translate: TranslateService, @Inject('entity') protected entityValue: DeviceInfo, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, - public fb: FormBuilder, + public fb: UntypedFormBuilder, protected cd: ChangeDetectorRef) { super(store, fb, entityValue, entitiesTableConfigValue, cd); } @@ -80,7 +80,7 @@ export class DeviceComponent extends EntityComponent { return entity && entity.customerId && entity.customerId.id !== NULL_UUID; } - buildForm(entity: DeviceInfo): FormGroup { + buildForm(entity: DeviceInfo): UntypedFormGroup { const form = this.fb.group( { name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], diff --git a/ui-ngx/src/app/modules/home/pages/device/device.module.ts b/ui-ngx/src/app/modules/home/pages/device/device.module.ts index cf1cbf4491..0754c747f3 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.module.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts index 4d320b6926..83064c27ff 100644 --- a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { ActivatedRoute, ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; import { CellActionDescriptor, checkBoxCell, @@ -30,7 +30,13 @@ import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; -import { Device, DeviceCredentials, DeviceInfo } from '@app/shared/models/device.models'; +import { + Device, + DeviceCredentials, + DeviceInfo, + DeviceInfoFilter, + DeviceInfoQuery +} from '@app/shared/models/device.models'; import { DeviceComponent } from '@modules/home/pages/device/device.component'; import { forkJoin, Observable, of, Subject } from 'rxjs'; import { select, Store } from '@ngrx/store'; @@ -63,12 +69,21 @@ import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component'; import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component'; import { BaseData, HasId } from '@shared/models/base-data'; -import { isDefinedAndNotNull } from '@core/utils'; +import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils'; import { EdgeService } from '@core/http/edge.service'; import { AddEntitiesToEdgeDialogComponent, AddEntitiesToEdgeDialogData } from '@home/dialogs/add-entities-to-edge-dialog.component'; +import { EdgeId } from '@shared/models/id/edge-id'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { PageLink, PageQueryParam } from '@shared/models/page/page-link'; +import { DeviceProfileId } from '@shared/models/id/device-profile-id'; + +interface DevicePageQueryParams extends PageQueryParam { + deviceProfileId?: string; + active?: boolean | string; +} @Injectable() export class DevicesTableConfigResolver implements Resolve> { @@ -103,17 +118,16 @@ export class DevicesTableConfigResolver implements Resolve this.translate.instant('device.delete-devices-text'); this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id); - this.config.saveEntity = device => { - return this.deviceService.saveDevice(device).pipe( + this.config.saveEntity = device => this.deviceService.saveDevice(device).pipe( tap(() => { this.broadcast.broadcast('deviceSaved'); }), mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id) )); - }; this.config.onEntityAction = action => this.onDeviceAction(action, this.config); this.config.detailsReadonly = () => (this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user'); + this.config.onLoadAction = (route) => this.onLoadAction(route); this.config.headerComponent = DeviceTableHeaderComponent; @@ -123,7 +137,7 @@ export class DevicesTableConfigResolver implements Resolve(), edgeId: routeParams.edgeId }; @@ -162,7 +176,8 @@ export class DevicesTableConfigResolver implements Resolve this.config.componentsData.deviceScope === 'tenant'; return this.config; @@ -170,48 +185,97 @@ export class DevicesTableConfigResolver implements Resolve> { const columns: Array> = [ new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), new EntityTableColumn('name', 'device.name', '25%'), new EntityTableColumn('deviceProfileName', 'device-profile.device-profile', '25%'), - new EntityTableColumn('label', 'device.label', '25%') + new EntityTableColumn('label', 'device.label', '25%'), + new EntityTableColumn('active', 'device.state', '80px', + entity => this.deviceState(entity), entity => this.deviceStateStyle(entity)) ]; if (deviceScope === 'tenant') { columns.push( new EntityTableColumn('customerTitle', 'customer.customer', '25%'), new EntityTableColumn('customerIsPublic', 'device.public', '60px', - entity => { - return checkBoxCell(entity.customerIsPublic); - }, () => ({}), false), + entity => checkBoxCell(entity.customerIsPublic), () => ({})), ); } columns.push( new EntityTableColumn('gateway', 'device.is-gateway', '60px', - entity => { - return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway); - }, () => ({}), false) + entity => checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway), () => ({}), false) ); return columns; } + private deviceState(device: DeviceInfo): string { + let translateKey = 'device.active'; + let backgroundColor = 'rgba(25, 128, 56, 0.08)'; + if (!device.active) { + translateKey = 'device.inactive'; + backgroundColor = 'rgba(209, 39, 48, 0.08)'; + } + return `
    + ${this.translate.instant(translateKey)} +
    `; + } + + private deviceStateStyle(device: DeviceInfo): object { + const styleObj = { + fontSize: '14px', + color: '#198038', + cursor: 'pointer' + }; + if (!device.active) { + styleObj.color = '#d12730'; + } + return styleObj; + } + configureEntityFunctions(deviceScope: string): void { + this.config.entitiesFetchFunction = pageLink => this.deviceService.getDeviceInfosByQuery(this.prepareDeviceInfoQuery(pageLink)); if (deviceScope === 'tenant') { - this.config.entitiesFetchFunction = pageLink => - this.deviceService.getTenantDeviceInfosByDeviceProfileId(pageLink, - this.config.componentsData.deviceProfileId !== null ? - this.config.componentsData.deviceProfileId.id : ''); this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id); - } else if (deviceScope === 'edge' || deviceScope === 'edge_customer_user') { - this.config.entitiesFetchFunction = pageLink => - this.deviceService.getEdgeDevices(this.config.componentsData.edgeId, pageLink, this.config.componentsData.edgeType); } else { - this.config.entitiesFetchFunction = pageLink => - this.deviceService.getCustomerDeviceInfosByDeviceProfileId(this.customerId, pageLink, - this.config.componentsData.deviceProfileId !== null ? - this.config.componentsData.deviceProfileId.id : ''); - this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id); + this.config.deleteEntity = () => of(); + } + } + + prepareDeviceInfoQuery(pageLink: PageLink): DeviceInfoQuery { + const deviceInfoFilter: DeviceInfoFilter = deepClone(this.config.componentsData.deviceInfoFilter); + if (this.config.componentsData.deviceScope === 'edge' || this.config.componentsData.deviceScope === 'edge_customer_user') { + deviceInfoFilter.edgeId = new EdgeId(this.config.componentsData.edgeId); + } else if (this.config.componentsData.deviceScope !== 'tenant') { + deviceInfoFilter.customerId = new CustomerId(this.customerId); } + return new DeviceInfoQuery(pageLink, deviceInfoFilter); } configureCellActions(deviceScope: string): Array> { @@ -331,6 +395,7 @@ export class DevicesTableConfigResolver implements Resolve { + this.config.addEntity = null; const actions: Array = []; if (deviceScope === 'tenant') { actions.push( @@ -347,6 +412,7 @@ export class DevicesTableConfigResolver implements Resolve this.importDevices($event) }, ); + this.config.addEntity = () => {this.deviceWizard(null); return of(null); }; } if (deviceScope === 'customer') { actions.push( @@ -538,7 +604,8 @@ export class DevicesTableConfigResolver implements Resolve { if (isDefinedAndNotNull(deviceCredentials)) { diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html new file mode 100644 index 0000000000..599d984738 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html @@ -0,0 +1,43 @@ + +
    + +

    info_outline + {{ 'edge.install-connect-instructions' | translate }}

    + + +
    + + +
    +
    + +
    +
    + +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts new file mode 100644 index 0000000000..a4dfe4345b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { DialogComponent } from "@shared/components/dialog.component"; +import { Store } from "@ngrx/store"; +import { AppState } from "@core/core.state"; +import { Router } from "@angular/router"; + +export interface EdgeInstructionsData { + instructions: string; +} + +@Component({ + selector: 'tb-edge-instructions', + templateUrl: './edge-instructions-dialog.component.html' +}) +export class EdgeInstructionsDialogComponent extends DialogComponent { + + instructions: string = this.data.instructions; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: EdgeInstructionsData) { + super(store, router, dialogRef); + } + + cancel(): void { + this.dialogRef.close(null); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts index 297669002a..9ec7d15aa0 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -44,49 +44,28 @@ import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages const routes: Routes = [ { - path: 'edgeInstances', + path: 'edgeManagement', data: { breadcrumb: { - label: 'edge.edge-instances', - icon: 'router' + label: 'edge.management', + icon: 'settings_input_antenna' } }, children: [ { path: '', - component: EntitiesTableComponent, + children: [], data: { auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'edge.edge-instances', - edgesType: 'tenant' - }, - resolve: { - entitiesTableConfig: EdgesTableConfigResolver + redirectTo: '/edgeManagement/instances' } }, { - path: ':entityId', - component: EntityDetailsPageComponent, - canDeactivate: [ConfirmOnExitGuard], + path: 'instances', data: { breadcrumb: { - labelFunction: entityDetailsPageBreadcrumbLabelFunction, + label: 'edge.instances', icon: 'router' - } as BreadCrumbConfig, - auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'edge.edge-instances', - edgesType: 'tenant' - }, - resolve: { - entitiesTableConfig: EdgesTableConfigResolver - } - }, - { - path: ':edgeId/assets', - data: { - breadcrumb: { - label: 'edge.assets', - icon: 'domain' } }, children: [ @@ -95,11 +74,11 @@ const routes: Routes = [ component: EntitiesTableComponent, data: { auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'edge.assets', - assetsType: 'edge' + title: 'edge.edge-instances', + edgesType: 'tenant' }, resolve: { - entitiesTableConfig: AssetsTableConfigResolver + entitiesTableConfig: EdgesTableConfigResolver } }, { @@ -109,197 +88,219 @@ const routes: Routes = [ data: { breadcrumb: { labelFunction: entityDetailsPageBreadcrumbLabelFunction, - icon: 'domain' + icon: 'router' } as BreadCrumbConfig, auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'edge.assets', - assetsType: 'edge' - }, - resolve: { - entitiesTableConfig: AssetsTableConfigResolver - } - } - ] - }, - { - path: ':edgeId/devices', - data: { - breadcrumb: { - label: 'edge.devices', - icon: 'devices_other' - } - }, - children: [ - { - path: '', - component: EntitiesTableComponent, - data: { - auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'edge.devices', - devicesType: 'edge' + title: 'edge.edge-instances', + edgesType: 'tenant' }, resolve: { - entitiesTableConfig: DevicesTableConfigResolver + entitiesTableConfig: EdgesTableConfigResolver } }, { - path: ':entityId', - component: EntityDetailsPageComponent, - canDeactivate: [ConfirmOnExitGuard], + path: ':edgeId/assets', data: { breadcrumb: { - labelFunction: entityDetailsPageBreadcrumbLabelFunction, - icon: 'devices_other' - } as BreadCrumbConfig, - auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'edge.devices', - devicesType: 'edge' - }, - resolve: { - entitiesTableConfig: DevicesTableConfigResolver - } - } - ] - }, - { - path: ':edgeId/entityViews', - data: { - breadcrumb: { - label: 'edge.entity-views', - icon: 'view_quilt' - }, - }, - children: [ - { - path: '', - component: EntitiesTableComponent, - data: { - auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'edge.entity-views', - entityViewsType: 'edge' + label: 'edge.assets', + icon: 'domain' + } }, - resolve: { - entitiesTableConfig: EntityViewsTableConfigResolver - } + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'edge.assets', + assetsType: 'edge' + }, + resolve: { + entitiesTableConfig: AssetsTableConfigResolver + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'domain' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'edge.assets', + assetsType: 'edge' + }, + resolve: { + entitiesTableConfig: AssetsTableConfigResolver + } + } + ] }, { - path: ':entityId', - component: EntityDetailsPageComponent, - canDeactivate: [ConfirmOnExitGuard], + path: ':edgeId/devices', data: { breadcrumb: { - labelFunction: entityDetailsPageBreadcrumbLabelFunction, + label: 'edge.devices', icon: 'devices_other' - } as BreadCrumbConfig, - auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'edge.entity-views', - entityViewsType: 'edge' + } }, - resolve: { - entitiesTableConfig: EntityViewsTableConfigResolver - } - } - ] - }, - { - path: ':edgeId/dashboards', - data: { - breadcrumb: { - label: 'edge.dashboards', - icon: 'dashboard' - } - }, - children: [ + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'edge.devices', + devicesType: 'edge' + }, + resolve: { + entitiesTableConfig: DevicesTableConfigResolver + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'devices_other' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'edge.devices', + devicesType: 'edge' + }, + resolve: { + entitiesTableConfig: DevicesTableConfigResolver + } + } + ] + }, { - path: '', - component: EntitiesTableComponent, + path: ':edgeId/entityViews', data: { - auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - dashboardsType: 'edge' - }, - resolve: { - entitiesTableConfig: DashboardsTableConfigResolver + breadcrumb: { + label: 'edge.entity-views', + icon: 'view_quilt' + }, }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'edge.entity-views', + entityViewsType: 'edge' + }, + resolve: { + entitiesTableConfig: EntityViewsTableConfigResolver + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'devices_other' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'edge.entity-views', + entityViewsType: 'edge' + }, + resolve: { + entitiesTableConfig: EntityViewsTableConfigResolver + } + } + ] }, { - path: ':dashboardId', - component: DashboardPageComponent, + path: ':edgeId/dashboards', data: { breadcrumb: { - labelFunction: dashboardBreadcumbLabelFunction, + label: 'edge.dashboards', icon: 'dashboard' - } as BreadCrumbConfig, - auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'edge.dashboard', - widgetEditMode: false - }, - resolve: { - dashboard: DashboardResolver - } - } - ] - }, - { - path: ':edgeId/ruleChains', - data: { - breadcrumb: { - label: 'edge.edge-rulechains', - icon: 'settings_ethernet' - } - }, - children: [ - { - path: '', - component: EntitiesTableComponent, - data: { - auth: [Authority.TENANT_ADMIN], - title: 'edge.rulechains', - ruleChainsType: 'edge' + } }, - resolve: { - entitiesTableConfig: RuleChainsTableConfigResolver - } + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + dashboardsType: 'edge' + }, + resolve: { + entitiesTableConfig: DashboardsTableConfigResolver + }, + }, + { + path: ':dashboardId', + component: DashboardPageComponent, + data: { + breadcrumb: { + labelFunction: dashboardBreadcumbLabelFunction, + icon: 'dashboard' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'edge.dashboard', + widgetEditMode: false + }, + resolve: { + dashboard: DashboardResolver + } + } + ] }, { - path: ':ruleChainId', - component: RuleChainPageComponent, - canDeactivate: [ConfirmOnExitGuard], + path: ':edgeId/ruleChains', data: { breadcrumb: { - labelFunction: ruleChainBreadcumbLabelFunction, + label: 'edge.rulechains', icon: 'settings_ethernet' - } as BreadCrumbConfig, - auth: [Authority.TENANT_ADMIN], - title: 'rulechain.edge-rulechain', - import: false, - ruleChainType: RuleChainType.EDGE + } }, - resolve: { - ruleChain: RuleChainResolver, - ruleChainMetaData: RuleChainMetaDataResolver, - ruleNodeComponents: RuleNodeComponentsResolver, - tooltipster: TooltipsterResolver - } + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'edge.rulechains', + ruleChainsType: 'edge' + }, + resolve: { + entitiesTableConfig: RuleChainsTableConfigResolver + } + }, + { + path: ':ruleChainId', + component: RuleChainPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: ruleChainBreadcumbLabelFunction, + icon: 'settings_ethernet' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN], + title: 'rulechain.edge-rulechain', + import: false, + ruleChainType: RuleChainType.EDGE + }, + resolve: { + ruleChain: RuleChainResolver, + ruleChainMetaData: RuleChainMetaDataResolver, + ruleNodeComponents: RuleNodeComponentsResolver, + tooltipster: TooltipsterResolver + } + } + ] } ] - } - ] - }, - { - path: 'edgeManagement', - data: { - breadcrumb: { - label: 'edge.management', - icon: 'settings_input_antenna' - } - }, - children: [ - { - path: '', - data: { - auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - redirectTo: '/edgeManagement/ruleChains' - } }, { path: 'ruleChains', @@ -366,7 +367,63 @@ const routes: Routes = [ ] } ] - }]; + }, + { + path: 'edgeInstances', + pathMatch: 'full', + redirectTo: '/edgeManagement/instances' + }, + { + path: 'edgeInstances/:entityId', + pathMatch: 'full', + redirectTo: '/edgeManagement/instances/:entityId' + }, + { + path: 'edgeInstances/:edgeId/assets', + pathMatch: 'full', + redirectTo: '/edgeManagement/instances/:edgeId/assets' + }, + { + path: 'edgeInstances/:edgeId/assets/:entityId', + redirectTo: '/edgeManagement/instances/:edgeId/assets/:entityId' + }, + { + path: 'edgeInstances/:edgeId/devices', + pathMatch: 'full', + redirectTo: '/edgeManagement/instances/:edgeId/devices' + }, + { + path: 'edgeInstances/:edgeId/devices/:entityId', + redirectTo: '/edgeManagement/instances/:edgeId/devices/:entityId' + }, + { + path: 'edgeInstances/:edgeId/entityViews', + pathMatch: 'full', + redirectTo: '/edgeManagement/instances/:edgeId/entityViews' + }, + { + path: 'edgeInstances/:edgeId/entityViews/:entityId', + redirectTo: '/edgeManagement/instances/:edgeId/entityViews/:entityId' + }, + { + path: 'edgeInstances/:edgeId/dashboards', + pathMatch: 'full', + redirectTo: '/edgeManagement/instances/:edgeId/dashboards' + }, + { + path: 'edgeInstances/:edgeId/dashboards/:dashboardId', + redirectTo: '/edgeManagement/instances/:edgeId/dashboards/:dashboardId' + }, + { + path: 'edgeInstances/:edgeId/ruleChains', + pathMatch: 'full', + redirectTo: '/edgeManagement/instances/:edgeId/ruleChains' + }, + { + path: 'edgeInstances/:edgeId/ruleChains/:ruleChainId', + redirectTo: '/edgeManagement/instances/:edgeId/ruleChains/:ruleChainId' + } +]; @NgModule({ imports: [RouterModule.forChild(routes)], diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-table-header.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge-table-header.component.html index 2b2d8a1e4c..8a7338a8ff 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-table-header.component.html @@ -1,6 +1,6 @@ { diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html index 99a8cd7ebb..caaf368f3d 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html @@ -1,6 +1,6 @@ { diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html index 79307ddcb2..87ee70f1a1 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html @@ -1,6 +1,6 @@ + diff --git a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.scss new file mode 100644 index 0000000000..222482999d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + display: block; + max-width: 400px; + min-width: 350px; + padding: 2px; +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.ts new file mode 100644 index 0000000000..275caa6647 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Notification } from '@shared/models/notification.models'; + +export interface InboxNotificationDialogData { + notification: Notification; +} + +@Component({ + selector: 'tb-inbox-notification-dialog', + templateUrl: './inbox-notification-dialog.component.html', + styleUrls: ['inbox-notification-dialog.component.scss'] +}) +export class InboxNotificationDialogComponent extends DialogComponent { + + notification: Notification; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: InboxNotificationDialogData) { + super(store, router, dialogRef); + + this.notification = data.notification; + } + + markAsRead(id: string) { + this.dialogRef.close(id); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-config.resolver.ts new file mode 100644 index 0000000000..f8088c28cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-config.resolver.ts @@ -0,0 +1,165 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + CellActionDescriptor, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { EntityType, EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { DatePipe } from '@angular/common'; +import { Direction } from '@shared/models/page/sort-order'; +import { + Notification, + NotificationStatus, + NotificationTemplateTypeTranslateMap +} from '@shared/models/notification.models'; +import { NotificationService } from '@core/http/notification.service'; +import { InboxTableHeaderComponent } from '@home/pages/notification/inbox/inbox-table-header.component'; +import { TranslateService } from '@ngx-translate/core'; +import { take } from 'rxjs/operators'; +import { MatDialog } from '@angular/material/dialog'; +import { + InboxNotificationDialogComponent, + InboxNotificationDialogData +} from '@home/pages/notification/inbox/inbox-notification-dialog.component'; +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; + +@Injectable() +export class InboxTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private notificationService: NotificationService, + private translate: TranslateService, + private dialog: MatDialog, + private datePipe: DatePipe) { + + this.config.entityType = EntityType.NOTIFICATION; + this.config.detailsPanelEnabled = false; + this.config.addEnabled = false; + this.config.rowPointer = true; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.NOTIFICATION); + this.config.entityResources = {} as EntityTypeResource; + + this.config.deleteEntityTitle = () => this.translate.instant('notification.delete-notification-title'); + this.config.deleteEntityContent = () => this.translate.instant('notification.delete-notification-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('notification.delete-notifications-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('notification.delete-notifications-text'); + + this.config.deleteEntity = id => this.notificationService.deleteNotification(id.id); + this.config.entitiesFetchFunction = pageLink => + this.notificationService.getNotifications(pageLink, this.config.componentsData.unreadOnly); + + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.handleRowClick = ($event, notification) => { + this.showNotification($event, notification); + return true; + }; + + this.config.componentsData = { + unreadOnly: true + }; + + this.config.cellActionDescriptors = this.configureCellActions(); + + this.config.headerComponent = InboxTableHeaderComponent; + + this.config.headerActionDescriptors = [{ + name: this.translate.instant('notification.mark-all-as-read'), + icon: 'done_all', + isEnabled: () => true, + onAction: $event => this.markAllRead($event) + }]; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('type', 'notification.type', '10%', (notification) => + this.translate.instant(NotificationTemplateTypeTranslateMap.get(notification.type).name)), + new EntityTableColumn('subject', 'notification.subject', '30%'), + new EntityTableColumn('text', 'notification.message', '60%') + ); + + } + + resolve(route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private configureCellActions(): Array> { + return [{ + name: this.translate.instant('notification.mark-as-read'), + icon: 'check_circle_outline', + isEnabled: (notification) => notification.status !== NotificationStatus.READ, + onAction: ($event, entity) => this.markAsRead($event, entity) + }]; + } + + private markAllRead($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.notificationService.markAllNotificationsAsRead().subscribe(() => { + if (this.config.componentsData.unreadOnly) { + this.config.getTable().resetSortAndFilter(true); + } else { + this.config.updateData(); + } + }); + } + + private markAsRead($event, entity){ + if ($event) { + $event.stopPropagation(); + } + this.notificationService.markNotificationAsRead(entity.id.id).subscribe(() => { + if (this.config.componentsData.unreadOnly) { + this.config.getTable().dataSource.pageData$.pipe(take(1)).subscribe( + (value) => { + if (value.data.length === 1 && this.config.getTable().pageLink.page) { + this.config.getTable().pageLink.page--; + } + this.config.updateData(); + } + ); + } else { + entity.status = NotificationStatus.READ; + this.config.getTable().detectChanges(); + } + }); + } + + private showNotification($event: Event, notification: Notification) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(InboxNotificationDialogComponent, { + disableClose: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + notification + } + }).afterClosed().subscribe(res => { + if (res) { + this.markAsRead(null, notification); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.html b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.html new file mode 100644 index 0000000000..be19b8beaf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.html @@ -0,0 +1,27 @@ + +
    + + {{ 'notification.unread' | translate }} + {{ 'notification.all' | translate }} + + +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.scss new file mode 100644 index 0000000000..01519235aa --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.scss @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../theme"; + +:host ::ng-deep { + .mat-button-toggle-group.tb-notification-unread-toggle-group { + &.mat-button-toggle-group-appearance-standard { + border: none; + border-radius: 14px; + + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + } + + .mat-button-toggle { + background: rgba(0, 0, 0, 0.06); + height: 28px; + align-items: center; + display: flex; + + .mat-button-toggle-ripple { + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border-radius: 14px; + } + } + + .mat-button-toggle-button { + color: #959595; + } + + .mat-button-toggle-focus-overlay { + border-radius: 14px; + margin: 2px; + } + + .mat-button-toggle-checked .mat-button-toggle-button { + background-color: $tb-primary-color; + color: #fff; + border-radius: 14px; + margin-left: 2px; + margin-right: 2px; + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: 24px; + font-size: 14px; + font-weight: 500; + letter-spacing: .25px; + } + + .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { + opacity: .01; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.ts new file mode 100644 index 0000000000..120d22c15e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Notification } from '@shared/models/notification.models'; + +@Component({ + selector: 'tb-inbox-table-header', + templateUrl: './inbox-table-header.component.html', + styleUrls: ['inbox-table-header.component.scss'] +}) +export class InboxTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } + + changeUnreadOnly(unreadOnly: boolean) { + this.entitiesTableConfig.componentsData.unreadOnly = unreadOnly; + this.entitiesTableConfig.getTable().resetSortAndFilter(true); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts b/ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts new file mode 100644 index 0000000000..480cf97f99 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts @@ -0,0 +1,140 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { RouterModule, Routes } from '@angular/router'; +import { Authority } from '@shared/models/authority.enum'; +import { NgModule } from '@angular/core'; +import { RouterTabsComponent } from '@home/components/router-tabs.component'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { InboxTableConfigResolver } from '@home/pages/notification/inbox/inbox-table-config.resolver'; +import { SentTableConfigResolver } from '@home/pages/notification/sent/sent-table-config.resolver'; +import { RecipientTableConfigResolver } from '@home/pages/notification/recipient/recipient-table-config.resolver'; +import { TemplateTableConfigResolver } from '@home/pages/notification/template/template-table-config.resolver'; +import { RuleTableConfigResolver } from '@home/pages/notification/rule/rule-table-config.resolver'; +import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component'; + +const routes: Routes = [ + { + path: 'notification', + component: RouterTabsComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN], + breadcrumb: { + label: 'notification.notification-center', + icon: 'mdi:message-badge' + }, + routerTabsHeaderComponent: SendNotificationButtonComponent + }, + children: [ + { + path: '', + children: [], + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN], + redirectTo: '/notification/inbox' + } + }, + { + path: 'inbox', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN], + title: 'notification.inbox', + breadcrumb: { + label: 'notification.inbox', + icon: 'inbox' + } + }, + resolve: { + entitiesTableConfig: InboxTableConfigResolver + } + }, + { + path: 'sent', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'notification.sent', + breadcrumb: { + label: 'notification.sent', + icon: 'outbox' + } + }, + resolve: { + entitiesTableConfig: SentTableConfigResolver + } + }, + { + path: 'templates', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'notification.templates', + breadcrumb: { + label: 'notification.templates', + icon: 'mdi:message-draw' + } + }, + resolve: { + entitiesTableConfig: TemplateTableConfigResolver + } + }, + { + path: 'recipients', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'notification.recipients', + breadcrumb: { + label: 'notification.recipients', + icon: 'contacts' + }, + }, + resolve: { + entitiesTableConfig: RecipientTableConfigResolver + } + }, + { + path: 'rules', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'notification.rules', + breadcrumb: { + label: 'notification.rules', + icon: 'mdi:message-cog' + } + }, + resolve: { + entitiesTableConfig: RuleTableConfigResolver + } + } + ] + } +]; + +@NgModule({ + providers: [ + InboxTableConfigResolver, + SentTableConfigResolver, + RecipientTableConfigResolver, + TemplateTableConfigResolver, + RuleTableConfigResolver + ], + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class NotificationRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/notification/notification.module.ts b/ui-ngx/src/app/modules/home/pages/notification/notification.module.ts new file mode 100644 index 0000000000..5ab7bfa957 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/notification.module.ts @@ -0,0 +1,61 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { NotificationRoutingModule } from '@home/pages/notification/notification-routing.module'; +import { InboxTableHeaderComponent } from '@home/pages/notification/inbox/inbox-table-header.component'; +import { InboxNotificationDialogComponent } from '@home/pages/notification/inbox/inbox-notification-dialog.component'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { SentErrorDialogComponent } from '@home/pages/notification/sent/sent-error-dialog.component'; +import { SentNotificationDialogComponent } from '@home/pages/notification/sent/sent-notification-dialog.componet'; +import { + RecipientNotificationDialogComponent +} from '@home/pages/notification/recipient/recipient-notification-dialog.component'; +import { RecipientTableHeaderComponent } from '@home/pages/notification/recipient/recipient-table-header.component'; +import { + TemplateNotificationDialogComponent +} from '@home/pages/notification/template/template-notification-dialog.component'; +import { TemplateTableHeaderComponent } from '@home/pages/notification/template/template-table-header.component'; +import { EscalationFormComponent } from '@home/pages/notification/rule/escalation-form.component'; +import { EscalationsComponent } from '@home/pages/notification/rule/escalations.component'; +import { RuleNotificationDialogComponent } from '@home/pages/notification/rule/rule-notification-dialog.component'; +import { RuleTableHeaderComponent } from '@home/pages/notification/rule/rule-table-header.component'; + +@NgModule({ + declarations: [ + InboxTableHeaderComponent, + InboxNotificationDialogComponent, + SentErrorDialogComponent, + SentNotificationDialogComponent, + RecipientNotificationDialogComponent, + RecipientTableHeaderComponent, + TemplateNotificationDialogComponent, + TemplateTableHeaderComponent, + EscalationFormComponent, + EscalationsComponent, + RuleNotificationDialogComponent, + RuleTableHeaderComponent + ], + imports: [ + CommonModule, + SharedModule, + NotificationRoutingModule, + HomeComponentsModule + ] +}) +export class NotificationModule { } diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html new file mode 100644 index 0000000000..378407d862 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html @@ -0,0 +1,140 @@ + +
    + +

    {{ (isAdd ? 'notification.add-notification-recipients-group' : 'notification.edit-notification-recipients-group') | translate }}

    + +
    + +
    + + +
    +
    + + notification.name + + + {{ 'notification.name-required' | translate }} + + +
    + + + +
    +
    {{ notificationTargetTypeTranslationMap.get(notificationTargetType) | translate }}
    +
    +
    +
    +
    + + notification.recipient-type.user-filters + + + {{ notificationTargetConfigTypeInfoMap.get(type).name | translate }} + + + {{ notificationTargetConfigTypeInfoMap.get(targetNotificationForm.get('configuration.usersFilter.type').value).hint | translate }} + + + +
    +
    + + {{ 'tenant.tenant' | translate }} + {{ 'tenant-profile.tenant-profile' | translate }} + +
    + + + + + + + + +
    +
    + + + + + + + + +
    +
    +
    + + + + {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }} + + + + +
    + + notification.description + + +
    +
    + +
    + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss new file mode 100644 index 0000000000..8406cae3fe --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../scss/constants"; +@import "../../../../../../theme"; + +:host { + form.tb-dialog-container { + min-width: 600px; + max-height: 100vh; + } + + .mat-dialog-content { + height: 85%; + } + + .tb-title { + display: block; + padding-bottom: 6px; + } +} + +:host ::ng-deep { + .mat-mdc-radio-group { + display: flex; + flex-direction: row; + margin-bottom: 22px; + gap: 12px; + + .mat-mdc-radio-button { + flex: 1 1 100%; + padding: 4px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + } + } + + @media #{$mat-xs} { + .mat-mdc-radio-group { + flex-direction: column; + } + } + + .mat-button-toggle-group.tb-notification-tenant-group { + &.mat-button-toggle-group-appearance-standard { + border: none; + border-radius: 18px; + margin-bottom: 14px; + + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + } + + .mat-button-toggle { + background: rgba(0, 0, 0, 0.06); + height: 36px; + align-items: center; + display: flex; + + .mat-button-toggle-ripple { + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border-radius: 18px; + } + } + + .mat-button-toggle-button { + color: #959595; + } + + .mat-button-toggle-focus-overlay { + border-radius: 18px; + margin: 2px; + } + + .mat-button-toggle-checked .mat-button-toggle-button { + background-color: $tb-primary-color; + color: #fff; + border-radius: 18px; + margin-left: 2px; + margin-right: 2px; + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: 34px; + font-size: 16px; + font-weight: 500; + } + + .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { + opacity: .01; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts new file mode 100644 index 0000000000..c07d8beb75 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts @@ -0,0 +1,210 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + NotificationTarget, + NotificationTargetConfigType, + NotificationTargetConfigTypeInfoMap, + NotificationTargetType, + NotificationTargetTypeTranslationMap, + SlackChanelType, + SlackChanelTypesTranslateMap +} from '@shared/models/notification.models'; +import { Component, Inject, OnDestroy } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { NotificationService } from '@core/http/notification.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { deepTrim, isDefinedAndNotNull } from '@core/utils'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Authority } from '@shared/models/authority.enum'; +import { AuthState } from '@core/auth/auth.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { AuthUser } from '@shared/models/user.model'; +import { control } from 'leaflet'; + +export interface RecipientNotificationDialogData { + target?: NotificationTarget; + isAdd?: boolean; +} + +@Component({ + selector: 'tb-target-notification-dialog', + templateUrl: './recipient-notification-dialog.component.html', + styleUrls: ['recipient-notification-dialog.component.scss'] +}) +export class RecipientNotificationDialogComponent extends + DialogComponent implements OnDestroy { + + private authState: AuthState = getCurrentAuthState(this.store); + private authUser: AuthUser = this.authState.authUser; + + targetNotificationForm: FormGroup; + notificationTargetType = NotificationTargetType; + notificationTargetTypes: NotificationTargetType[] = Object.values(NotificationTargetType); + notificationTargetTypeTranslationMap = NotificationTargetTypeTranslationMap; + notificationTargetConfigType = NotificationTargetConfigType; + notificationTargetConfigTypes: NotificationTargetConfigType[] = this.allowNotificationTargetConfigTypes(); + notificationTargetConfigTypeInfoMap = NotificationTargetConfigTypeInfoMap; + slackChanelTypes = Object.keys(SlackChanelType) as SlackChanelType[]; + slackChanelTypesTranslateMap = SlackChanelTypesTranslateMap; + + entityType = EntityType; + isAdd = true; + + private readonly destroy$ = new Subject(); + private userFilterFormControls: string[]; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: RecipientNotificationDialogData, + private fb: FormBuilder, + private notificationService: NotificationService) { + super(store, router, dialogRef); + + if (isDefinedAndNotNull(data.isAdd)) { + this.isAdd = data.isAdd; + } + + this.targetNotificationForm = this.fb.group({ + name: [null, Validators.required], + configuration: this.fb.group({ + type: [NotificationTargetType.PLATFORM_USERS], + usersFilter: this.fb.group({ + type: [NotificationTargetConfigType.ALL_USERS], + filterByTenants: [{value: true, disabled: true}], + tenantsIds: [{value: null, disabled: true}], + tenantProfilesIds: [{value: null, disabled: true}], + customerId: [{value: null, disabled: true}, Validators.required], + usersIds: [{value: null, disabled: true}, Validators.required], + }), + conversationType: [{value: SlackChanelType.PUBLIC_CHANNEL, disabled: true}], + conversation: [{value: '', disabled: true}, Validators.required], + description: [null] + }) + }); + + this.targetNotificationForm.get('configuration.type').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((type: NotificationTargetType) => { + this.targetNotificationForm.get('configuration').disable({emitEvent: false}); + switch (type) { + case NotificationTargetType.PLATFORM_USERS: + this.targetNotificationForm.get('configuration.usersFilter').enable({emitEvent: false}); + this.targetNotificationForm.get('configuration.usersFilter.type').updateValueAndValidity({onlySelf: true}); + break; + case NotificationTargetType.SLACK: + this.targetNotificationForm.get('configuration.conversationType').enable({emitEvent: false}); + this.targetNotificationForm.get('configuration.conversation').enable({emitEvent: false}); + break; + } + this.targetNotificationForm.get('configuration.type').enable({emitEvent: false}); + this.targetNotificationForm.get('configuration.description').enable({emitEvent: false}); + }); + + this.userFilterFormControls = Object.keys((this.targetNotificationForm.get('configuration.usersFilter') as FormGroup).controls) + .filter((controlName) => controlName !== 'type'); + + this.targetNotificationForm.get('configuration.usersFilter.type').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((type: NotificationTargetConfigType) => { + this.userFilterFormControls.forEach( + controlName => this.targetNotificationForm.get(`configuration.usersFilter.${controlName}`).disable({emitEvent: false}) + ); + switch (type) { + case NotificationTargetConfigType.TENANT_ADMINISTRATORS: + if (this.isSysAdmin()) { + this.targetNotificationForm.get('configuration.usersFilter.filterByTenants').enable({onlySelf: true}); + } + break; + case NotificationTargetConfigType.USER_LIST: + this.targetNotificationForm.get('configuration.usersFilter.usersIds').enable({emitEvent: false}); + break; + case NotificationTargetConfigType.CUSTOMER_USERS: + this.targetNotificationForm.get('configuration.usersFilter.customerId').enable({emitEvent: false}); + break; + } + }); + + this.targetNotificationForm.get('configuration.usersFilter.filterByTenants').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value: boolean) => { + if (value) { + this.targetNotificationForm.get('configuration.usersFilter.tenantsIds').enable({emitEvent: false}); + this.targetNotificationForm.get('configuration.usersFilter.tenantProfilesIds').disable({emitEvent: false}); + } else { + this.targetNotificationForm.get('configuration.usersFilter.tenantsIds').disable({emitEvent: false}); + this.targetNotificationForm.get('configuration.usersFilter.tenantProfilesIds').enable({emitEvent: false}); + } + }); + + if (isDefinedAndNotNull(data.target)) { + this.targetNotificationForm.patchValue(data.target, {emitEvent: false}); + this.targetNotificationForm.get('configuration.type').updateValueAndValidity({onlySelf: true}); + if (this.isSysAdmin() && data.target.configuration.usersFilter.type === NotificationTargetConfigType.TENANT_ADMINISTRATORS) { + this.targetNotificationForm.get('configuration.usersFilter.filterByTenants') + .patchValue(!Array.isArray(this.data.target.configuration.usersFilter.tenantProfilesIds), {onlySelf: true}); + } + } + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save() { + let formValue = deepTrim(this.targetNotificationForm.value); + if (isDefinedAndNotNull(this.data.target)) { + formValue = Object.assign({}, this.data.target, formValue); + } + if (this.isSysAdmin() && formValue.configuration.type === NotificationTargetType.PLATFORM_USERS && + formValue.configuration.usersFilter.type === NotificationTargetConfigType.TENANT_ADMINISTRATORS) { + delete formValue.configuration.usersFilter.filterByTenants; + } + this.notificationService.saveNotificationTarget(formValue).subscribe( + (target) => this.dialogRef.close(target) + ); + } + + isSysAdmin(): boolean { + return this.authUser.authority === Authority.SYS_ADMIN; + } + + private allowNotificationTargetConfigTypes(): NotificationTargetConfigType[] { + if (this.isSysAdmin()) { + return [ + NotificationTargetConfigType.ALL_USERS, + NotificationTargetConfigType.TENANT_ADMINISTRATORS, + NotificationTargetConfigType.AFFECTED_TENANT_ADMINISTRATORS, + NotificationTargetConfigType.SYSTEM_ADMINISTRATORS + ]; + } + return Object.values(NotificationTargetConfigType).filter(type => + type !== NotificationTargetConfigType.AFFECTED_TENANT_ADMINISTRATORS && type !== NotificationTargetConfigType.SYSTEM_ADMINISTRATORS); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts new file mode 100644 index 0000000000..05259e1edb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts @@ -0,0 +1,126 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + CellActionDescriptor, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { EntityType, EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@shared/models/page/sort-order'; +import { NotificationTarget, NotificationTargetTypeTranslationMap } from '@shared/models/notification.models'; +import { NotificationService } from '@core/http/notification.service'; +import { TranslateService } from '@ngx-translate/core'; +import { + RecipientNotificationDialogComponent, + RecipientNotificationDialogData +} from '@home/pages/notification/recipient/recipient-notification-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { RecipientTableHeaderComponent } from '@home/pages/notification/recipient/recipient-table-header.component'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { DatePipe } from '@angular/common'; + +@Injectable() +export class RecipientTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private notificationService: NotificationService, + private translate: TranslateService, + private dialog: MatDialog, + private datePipe: DatePipe) { + + this.config.entityType = EntityType.NOTIFICATION_TARGET; + this.config.detailsPanelEnabled = false; + this.config.addEnabled = false; + this.config.rowPointer = true; + + this.config.entityTranslations = entityTypeTranslations.get(EntityType.NOTIFICATION_TARGET); + this.config.entityResources = {} as EntityTypeResource; + + this.config.entitiesFetchFunction = pageLink => this.notificationService.getNotificationTargets(pageLink); + + this.config.deleteEntityTitle = target => this.translate.instant('notification.delete-recipient-title', {recipientName: target.name}); + this.config.deleteEntityContent = () => this.translate.instant('notification.delete-recipient-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('notification.delete-recipients-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('notification.delete-recipients-text'); + + this.config.deleteEntity = id => this.notificationService.deleteNotificationTarget(id.id); + + this.config.cellActionDescriptors = this.configureCellActions(); + + this.config.headerComponent = RecipientTableHeaderComponent; + this.config.onEntityAction = action => this.onTargetAction(action); + + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.handleRowClick = ($event, target) => { + this.editTarget($event, target); + return true; + }; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('name', 'notification.recipient-group', '20%'), + new EntityTableColumn('configuration.type', 'notification.type', '20%', + (target) => this.translate.instant(NotificationTargetTypeTranslationMap.get(target.configuration.type)), + () => ({}), false), + new EntityTableColumn('configuration.description', 'notification.description', '60%', + (target) => target.configuration.description || '', + () => ({}), false) + ); + } + + resolve(route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private configureCellActions(): Array> { + return []; + } + + private editTarget($event: Event, target: NotificationTarget, isAdd = false) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(RecipientNotificationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + target + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.updateData(); + } + }); + } + + private onTargetAction(action: EntityAction): boolean { + switch (action.action) { + case 'add': + this.editTarget(action.event, action.entity, true); + return true; + } + return false; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-header.component.html b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-header.component.html new file mode 100644 index 0000000000..e2effa3207 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-header.component.html @@ -0,0 +1,24 @@ + +
    + +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-header.component.scss new file mode 100644 index 0000000000..33537b4b23 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-header.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + width: 10000px; +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-header.component.ts new file mode 100644 index 0000000000..f7285d3836 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-header.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { NotificationTarget } from '@shared/models/notification.models'; + +@Component({ + selector: 'tb-recipient-table-header', + templateUrl: './recipient-table-header.component.html', + styleUrls: ['recipient-table-header.component.scss'] +}) +export class RecipientTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } + + createTarget($event: Event) { + this.entitiesTableConfig.onEntityAction({event: $event, action: 'add', entity: null}); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html new file mode 100644 index 0000000000..b9a5ef2119 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html @@ -0,0 +1,48 @@ + +
    +
    +
    notification.first-recipient
    + +
    + notification.after + + notification.notify +
    +
    + + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.scss b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.scss new file mode 100644 index 0000000000..52ff558723 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.scss @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../scss/constants"; + +:host { + .escalation { + height: 100%; + min-height: 74px; + max-height: 100%; + border-radius: 8px; + background: rgba(0,0,0,0.04); + padding: 0 10px; + + > div:first-child { + padding-right: 10px; + } + } + + .escalation-notify { + text-align: center; + } + + tb-timeinterval { + min-width: 100px; + padding: 0 10px; + } + + @media #{$mat-xs} { + .escalation { + > div:first-child { + padding: 10px 0 0; + } + } + + tb-timeinterval { + padding: 0; + } + } +} + +:host ::ng-deep { + .mat-mdc-form-field { + min-width: 100px; + + .mat-mdc-form-field-infix { + width: 100%; + } + + .mat-mdc-select { + min-width: 110px !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.ts new file mode 100644 index 0000000000..089b234a24 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.ts @@ -0,0 +1,173 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { isDefinedAndNotNull } from '@core/utils'; +import { Subject } from 'rxjs'; +import { + NonConfirmedNotificationEscalation, + NotificationTarget, + NotificationType +} from '@shared/models/notification.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { takeUntil } from 'rxjs/operators'; +import { + RecipientNotificationDialogComponent, + RecipientNotificationDialogData +} from '@home/pages/notification/recipient/recipient-notification-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { MatButton } from '@angular/material/button'; + +@Component({ + selector: 'tb-escalation-form', + templateUrl: './escalation-form.component.html', + styleUrls: ['./escalation-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EscalationFormComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EscalationFormComponent), + multi: true, + } + ] +}) +export class EscalationFormComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator { + + @Input() + disabled: boolean; + + @Input() + systemEscalation = false; + + escalationFormGroup: FormGroup; + + entityType = EntityType; + notificationType = NotificationType; + + private modelValue; + private propagateChange = null; + private propagateChangePending = false; + private destroy$ = new Subject(); + + constructor(private fb: FormBuilder, + private dialog: MatDialog) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.propagateChange(this.modelValue); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.escalationFormGroup = this.fb.group( + { + delayInSec: [0], + targets: [null, Validators.required], + }); + this.escalationFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => this.updateModel()); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.escalationFormGroup.disable({emitEvent: false}); + } else { + this.escalationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: NonConfirmedNotificationEscalation): void { + this.propagateChangePending = false; + this.modelValue = value; + this.modelValue.delayInSec = +value.delayInSec; + if (isDefinedAndNotNull(this.modelValue)) { + this.escalationFormGroup.patchValue(this.modelValue, {emitEvent: false}); + } + if (!this.disabled && !this.escalationFormGroup.valid) { + this.updateModel(); + } + } + + createTarget($event: Event, button: MatButton) { + if ($event) { + $event.stopPropagation(); + } + button._elementRef.nativeElement.blur(); + this.dialog.open(RecipientNotificationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: {} + }).afterClosed() + .subscribe((res) => { + if (res) { + let formValue: string[] = this.escalationFormGroup.get('targets').value; + if (!formValue) { + formValue = []; + } + formValue.push(res.id.id); + this.escalationFormGroup.get('targets').patchValue(formValue); + } + }); + } + + public validate(c: FormControl) { + return (this.escalationFormGroup.valid) ? null : { + escalation: { + valid: false, + }, + }; + } + + private updateModel() { + const value = this.escalationFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + if (this.propagateChange) { + this.propagateChange(this.modelValue); + } else { + this.propagateChangePending = true; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.html new file mode 100644 index 0000000000..1d7041ee43 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.html @@ -0,0 +1,48 @@ + +
    +
    + + + + +
    +
    + notification.no-rule +
    +
    + +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.ts new file mode 100644 index 0000000000..1f7b9c9617 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.ts @@ -0,0 +1,160 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { Subject } from 'rxjs'; +import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models'; +import { takeUntil } from 'rxjs/operators'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-escalations-component', + templateUrl: './escalations.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EscalationsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EscalationsComponent), + multi: true, + } + ] +}) +export class EscalationsComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { + + escalationsFormGroup: FormGroup; + + @Input() + @coerceBoolean() + required: boolean; + + @Input() + disabled: boolean; + + private destroy$ = new Subject(); + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.escalationsFormGroup = this.fb.group({ + escalations: this.fb.array([]) + }); + + this.escalationsFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => this.updateModel()); + } + + get escalationsFormArray(): FormArray { + return this.escalationsFormGroup.get('escalations') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.escalationsFormGroup.disable({emitEvent: false}); + } else { + this.escalationsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(escalations: {[key: string]: Array} | null): void { + const escalationParse: Array = []; + // eslint-disable-next-line guard-for-in + for (const escalation in escalations) { + escalationParse.push({delayInSec: Number(escalation) * 1000, targets: escalations[escalation]}); + } + if (escalationParse.length === 0) { + this.addEscalation(0); + } else if (escalationParse?.length === this.escalationsFormArray.length) { + this.escalationsFormArray.patchValue(escalationParse, {emitEvent: false}); + } else { + const escalationsControls: Array = []; + escalationParse.forEach(escalation => { + escalationsControls.push(this.fb.control(escalation, [Validators.required])); + }); + this.escalationsFormGroup.setControl('escalations', this.fb.array(escalationsControls), {emitEvent: false}); + if (this.disabled) { + this.escalationsFormGroup.disable({emitEvent: false}); + } else { + this.escalationsFormGroup.enable({emitEvent: false}); + } + } + } + + public removeEscalation(index: number) { + (this.escalationsFormGroup.get('escalations') as FormArray).removeAt(index); + } + + public addEscalation(delay = 3600000) { + const escalation = { + delayInSec: delay, + targets: null + }; + const escalationArray = this.escalationsFormGroup.get('escalations') as FormArray; + escalationArray.push(this.fb.control(escalation, []), {emitEvent: false}); + } + + public validate(c: AbstractControl): ValidationErrors | null { + return this.escalationsFormGroup.valid ? null : { + escalation: { + valid: false, + }, + }; + } + + private updateModel() { + const escalations = {}; + this.escalationsFormGroup.get('escalations').value.forEach( + escalation => escalations[escalation.delayInSec / 1000] = escalation.targets + ); + this.propagateChange(escalations); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html new file mode 100644 index 0000000000..4e7a133009 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html @@ -0,0 +1,508 @@ + + +

    {{ dialogTitle | translate }}

    + +
    + +
    + + +
    +
    + + + check + + + {{ 'notification.basic-settings' | translate }} +
    + + notification.rule-name + + + {{ 'notification.rule-name-required' | translate }} + + + + {{ 'notification.rule-enable' | translate }} + + + notification.trigger.trigger + + + {{ triggerTypeTranslationMap.get(trigger) | translate }} + + + + {{ 'notification.trigger.trigger-required' | translate }} + + + + +
    + + + +
    + +
    + notification.escalation-chain +
    notification.recipients
    + + +
    +
    +
    + +
    +
    +
    +
    notification.stop-escalation-alarm-status-become
    + + alarm.alarm-status-list + + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + +
    +
    +
    +
    +
    + + {{ 'notification.alarm-trigger-settings' | translate }} +
    +
    +
    + notification.filter + + + + + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + + +
    + + notification.notify-on + + + {{ alarmActionTranslationMap.get(alarmAction) | translate }} + + + + {{ 'notification.notify-on-required' | translate }} + + +
    +
    +
    +
    + + notification.description + + +
    +
    +
    + + + {{ 'notification.device-activity-trigger-settings' | translate }} +
    +
    +
    + + {{ 'device.device' | translate }} + {{ 'device-profile.device-profile' | translate }} + +
    + + + + + + + + + + notification.notify-on + + + {{ deviceEventTranslationMap.get(deviceEvent) | translate }} + + + + {{ 'notification.notify-on-required' | translate }} + + +
    +
    +
    +
    + + notification.description + + +
    +
    +
    + + + {{ 'notification.entity-action-trigger-settings' | translate }} +
    +
    + notification.filter + + +
    + notification.status + {{ 'notification.created' | translate }} + {{ 'notification.updated' | translate }} + {{ 'notification.deleted' | translate }} +
    +
    +
    +
    +
    + + notification.description + + +
    +
    +
    + + + {{ 'notification.alarm-comment-trigger-settings' | translate }} +
    +
    +
    + notification.filter + + + + + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + + + + alarm.alarm-status-list + + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + +
    +
    + + {{ 'notification.notify-only-user-comments' | translate }} + + + {{ 'notification.notify-on-comment-update' | translate }} + +
    +
    +
    +
    +
    + + notification.description + + +
    +
    +
    + + {{ 'notification.alarm-assignment-trigger-settings' | translate }} +
    +
    +
    + notification.filter + + + + + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + + + + alarm.alarm-status-list + + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + +
    + + notification.notify-on + + + {{ alarmAssignmentActionTranslationMap.get(alarmAssignmentAction) | translate }} + + + + {{ 'notification.notify-on-required' | translate }} + + +
    +
    +
    +
    + + notification.description + + +
    +
    +
    + + {{ 'notification.rule-engine-events-trigger-settings' | translate }} +
    +
    +
    + notification.rule-engine-filter + + + + rulechain.rulechain-events + + + {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} + + + + + {{ 'notification.only-rule-chain-lifecycle-failures' | translate }} + +
    +
    + notification.rule-node-filter + + {{ 'notification.track-rule-node-events' | translate }} + +
    + + rulenode.rule-node-events + + + {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} + + + + + {{ 'notification.only-rule-node-lifecycle-failures' | translate }} + +
    +
    +
    +
    +
    +
    + + notification.description + + +
    +
    +
    + + + {{ 'notification.entities-limit-trigger-settings' | translate }} +
    +
    + notification.filter + + +
    + +
    + + + + + + +
    +
    +
    +
    +
    +
    + + notification.description + + +
    +
    +
    + + + {{ 'notification.api-usage-trigger-settings' | translate }} +
    +
    + + api-usage.api-features + + + {{ apiFeatureTranslationMap.get(apiFeature) | translate }} + + + {{ 'notification.api-feature-hint' | translate }} + + + notification.notify-on + + + {{ apiUsageStateValueTranslationMap.get(apiUsageStateValue) | translate }} + + + + {{ 'notification.notify-on-required' | translate }} + + +
    +
    +
    +
    + + notification.description + + +
    +
    +
    + + + {{ 'notification.new-platform-version-trigger-settings' | translate }} +
    +
    + + notification.description + + +
    +
    +
    + +
    +
    + +
    + + + +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss new file mode 100644 index 0000000000..7e582d2b7d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss @@ -0,0 +1,177 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../theme"; +@import "../../../../../../scss/constants"; + +:host { + width: 900px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: flex; + flex-direction: column; + + .tb-hint { + margin-top: -1.25em; + padding: 0; + } + + .fields-group { + padding: 0 16px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + &.tb-margin { + margin-bottom: 12px; + } + + &.tb-margin-before-field { + margin-bottom: 12px; + } + + &.tb-hierarchy { + padding: 0 0 8px 16px; + } + + legend { + margin-bottom: 8px; + color: rgba(0, 0, 0, .7); + width: fit-content; + } + + .mat-body-2 { + margin-bottom: 8px; + color: rgba(0,0,0,.76); + } + + .clear-button-space { + margin-right: 48px; + } + + .mat-mdc-slide-toggle { + margin-bottom: 16px; + } + } + + .limit-slider-container { + .limit-slider-value { + margin-left: 16px; + min-width: 25px; + max-width: 100px; + } + mat-form-field input[type=number] { + text-align: center; + } + } + + @media #{$mat-gt-sm} { + .history-time-input { + min-width: 364px; + } + .limit-slider-container { + > label { + margin-right: 16px; + width: min-content; + max-width: 40%; + } + } + } + + ::ng-deep { + + .mat-mdc-dialog-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0 !important; + + .mat-stepper-horizontal { + display: flex; + height: 100%; + overflow: hidden; + + .mat-horizontal-stepper-wrapper { + flex: 1 1 100%; + } + + .mat-horizontal-content-container { + height: 700px; + max-height: 100%; + width: 100%;; + overflow-y: auto; + scrollbar-gutter: stable; + @media #{$mat-gt-sm} { + min-width: 500px; + } + } + } + } + + .mat-button-toggle-group.tb-notification-unread-toggle-group { + &.mat-button-toggle-group-appearance-standard { + border: none; + border-radius: 18px; + margin-bottom: 8px; + + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + } + + .mat-button-toggle { + background: rgba(0, 0, 0, 0.06); + height: 36px; + align-items: center; + display: flex; + + .mat-button-toggle-ripple { + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border-radius: 18px; + } + } + + .mat-button-toggle-button { + color: #959595; + } + + .mat-button-toggle-focus-overlay { + border-radius: 18px; + margin: 2px; + } + + .mat-button-toggle-checked .mat-button-toggle-button { + background-color: $tb-primary-color; + color: #fff; + border-radius: 18px; + margin-left: 2px; + margin-right: 2px; + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: 34px; + font-size: 16px; + font-weight: 500; + } + + .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { + opacity: .01; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts new file mode 100644 index 0000000000..93b22911f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts @@ -0,0 +1,473 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AlarmAction, + AlarmActionTranslationMap, + AlarmAssignmentAction, + AlarmAssignmentActionTranslationMap, + ComponentLifecycleEvent, + ComponentLifecycleEventTranslationMap, + DeviceEvent, + DeviceEventTranslationMap, + NotificationRule, + NotificationTarget, + TriggerType, + TriggerTypeTranslationMap +} from '@shared/models/notification.models'; +import { Component, Inject, OnDestroy, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { NotificationService } from '@core/http/notification.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { deepClone, deepTrim, isDefined } from '@core/utils'; +import { Observable, Subject } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; +import { MatStepper } from '@angular/material/stepper'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { + AlarmSearchStatus, + alarmSearchStatusTranslations, + AlarmSeverity, + alarmSeverityTranslations +} from '@shared/models/alarm.models'; +import { TranslateService } from '@ngx-translate/core'; +import { + RecipientNotificationDialogComponent, + RecipientNotificationDialogData +} from '@home/pages/notification/recipient/recipient-notification-dialog.component'; +import { MatButton } from '@angular/material/button'; +import { AuthState } from '@core/auth/auth.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { AuthUser } from '@shared/models/user.model'; +import { Authority } from '@shared/models/authority.enum'; +import { + ApiFeature, + ApiFeatureTranslationMap, + ApiUsageStateValue, + ApiUsageStateValueTranslationMap +} from '@shared/models/api-usage.models'; + +export interface RuleNotificationDialogData { + rule?: NotificationRule; + isAdd?: boolean; + isCopy?: boolean; +} + +@Component({ + selector: 'tb-rule-notification-dialog', + templateUrl: './rule-notification-dialog.component.html', + styleUrls: ['rule-notification-dialog.component.scss'] +}) +export class RuleNotificationDialogComponent extends + DialogComponent implements OnDestroy { + + @ViewChild('addNotificationRule', {static: true}) addNotificationRule: MatStepper; + + stepperOrientation: Observable; + + ruleNotificationForm: FormGroup; + alarmTemplateForm: FormGroup; + deviceInactivityTemplateForm: FormGroup; + entityActionTemplateForm: FormGroup; + alarmCommentTemplateForm: FormGroup; + alarmAssignmentTemplateForm: FormGroup; + ruleEngineEventsTemplateForm: FormGroup; + entitiesLimitTemplateForm: FormGroup; + apiUsageLimitTemplateForm: FormGroup; + newPlatformVersionTemplateForm: FormGroup; + + triggerType = TriggerType; + triggerTypes: TriggerType[]; + triggerTypeTranslationMap = TriggerTypeTranslationMap; + + alarmSearchStatuses = [ + AlarmSearchStatus.ACTIVE, + AlarmSearchStatus.CLEARED, + AlarmSearchStatus.ACK, + AlarmSearchStatus.UNACK + ]; + alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; + + alarmSeverityTranslationMap = alarmSeverityTranslations; + alarmSeverities = Object.keys(AlarmSeverity) as Array; + + alarmActions: AlarmAction[] = Object.values(AlarmAction); + alarmActionTranslationMap = AlarmActionTranslationMap; + + alarmAssignmentActions: AlarmAssignmentAction[] = Object.values(AlarmAssignmentAction); + alarmAssignmentActionTranslationMap = AlarmAssignmentActionTranslationMap; + + componentLifecycleEvents: ComponentLifecycleEvent[] = Object.values(ComponentLifecycleEvent); + componentLifecycleEventTranslationMap = ComponentLifecycleEventTranslationMap; + + deviceEvents: DeviceEvent[] = Object.values(DeviceEvent); + deviceEventTranslationMap = DeviceEventTranslationMap; + + apiUsageStateValues: ApiUsageStateValue[] = Object.values(ApiUsageStateValue); + apiUsageStateValueTranslationMap = ApiUsageStateValueTranslationMap; + + apiFeatures: ApiFeature[] = Object.values(ApiFeature); + apiFeatureTranslationMap = ApiFeatureTranslationMap; + + entityType = EntityType; + isAdd = true; + + allowEntityTypeForEntitiesLimit = [ + EntityType.DEVICE, + EntityType.ASSET, + EntityType.CUSTOMER, + EntityType.USER, + EntityType.DASHBOARD, + EntityType.RULE_CHAIN + ]; + + selectedIndex = 0; + + dialogTitle = 'notification.edit-rule'; + + private destroy$ = new Subject(); + + private readonly ruleNotification: NotificationRule; + + private triggerTypeFormsMap: Map; + private authState: AuthState = getCurrentAuthState(this.store); + private authUser: AuthUser = this.authState.authUser; + private _allowEntityTypeForEntityAction: EntityType[]; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: RuleNotificationDialogData, + private breakpointObserver: BreakpointObserver, + private fb: FormBuilder, + public translate: TranslateService, + private notificationService: NotificationService, + private dialog: MatDialog) { + super(store, router, dialogRef); + + this.triggerTypes = this.allowTriggerTypes(); + + if (isDefined(data.isAdd)) { + this.isAdd = data.isAdd; + } + + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) + .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); + + this.ruleNotificationForm = this.fb.group({ + name: [null, Validators.required], + enabled: [true, Validators.required], + templateId: [null, Validators.required], + triggerType: [this.isSysAdmin() ? TriggerType.ENTITIES_LIMIT : TriggerType.ALARM, Validators.required], + recipientsConfig: this.fb.group({ + targets: [{value: null, disabled: !this.isSysAdmin()}, Validators.required], + escalationTable: [{value: null, disabled: this.isSysAdmin()}, Validators.required] + }), + triggerConfig: [null], + additionalConfig: this.fb.group({ + description: [''] + }) + }); + + this.ruleNotificationForm.get('triggerType').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value === TriggerType.ALARM) { + this.ruleNotificationForm.get('recipientsConfig.escalationTable').enable({emitEvent: false}); + this.ruleNotificationForm.get('recipientsConfig.targets').disable({emitEvent: false}); + } else { + this.ruleNotificationForm.get('recipientsConfig.escalationTable').disable({emitEvent: false}); + this.ruleNotificationForm.get('recipientsConfig.targets').enable({emitEvent: false}); + } + }); + + this.ruleNotificationForm.get('recipientsConfig.escalationTable').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { + if (this.countRecipientsChainConfig() > 1) { + this.alarmTemplateForm.get('triggerConfig.clearRule').enable({emitEvent: false}); + } else { + this.alarmTemplateForm.get('triggerConfig.clearRule').disable({emitEvent: false}); + } + }); + + this.alarmTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + alarmTypes: [null], + alarmSeverities: [[]], + clearRule: this.fb.group({ + alarmStatuses: [[]] + }), + notifyOn: [[AlarmAction.CREATED], Validators.required] + }) + }); + + this.deviceInactivityTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + filterByDevice: [true], + devices: [null], + deviceProfiles: [{value: null, disabled: true}], + notifyOn: [[DeviceEvent.INACTIVE], Validators.required] + }) + }); + + this.deviceInactivityTemplateForm.get('triggerConfig.filterByDevice').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.deviceInactivityTemplateForm.get('triggerConfig.devices').enable({emitEvent: false}); + this.deviceInactivityTemplateForm.get('triggerConfig.deviceProfiles').disable({emitEvent: false}); + } else { + this.deviceInactivityTemplateForm.get('triggerConfig.deviceProfiles').enable({emitEvent: false}); + this.deviceInactivityTemplateForm.get('triggerConfig.devices').disable({emitEvent: false}); + } + }); + + this.entityActionTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + entityTypes: [[EntityType.DEVICE], Validators.required], + created: [false], + updated: [false], + deleted: [false] + }) + }); + + this.alarmCommentTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + alarmTypes: [null], + alarmSeverities: [[]], + alarmStatuses: [[]], + onlyUserComments: [false], + notifyOnCommentUpdate: [false] + }) + }); + + this.alarmAssignmentTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + alarmTypes: [null], + alarmSeverities: [[]], + alarmStatuses: [[]], + notifyOn: [[AlarmAssignmentAction.ASSIGNED], Validators.required] + }) + }); + + this.ruleEngineEventsTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + ruleChains: [null], + ruleChainEvents: [null], + onlyRuleChainLifecycleFailures: [false], + trackRuleNodeEvents: [false], + ruleNodeEvents: [null], + onlyRuleNodeLifecycleFailures: [false] + }) + }); + + this.entitiesLimitTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + entityTypes: [], + threshold: [.8, [Validators.min(0), Validators.max(1)]] + }) + }); + + this.apiUsageLimitTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + apiFeatures: [[]], + notifyOn: [[ApiUsageStateValue.WARNING], Validators.required] + }) + }); + + this.newPlatformVersionTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + + }) + }); + + this.triggerTypeFormsMap = new Map([ + [TriggerType.ALARM, this.alarmTemplateForm], + [TriggerType.ALARM_COMMENT, this.alarmCommentTemplateForm], + [TriggerType.DEVICE_ACTIVITY, this.deviceInactivityTemplateForm], + [TriggerType.ENTITY_ACTION, this.entityActionTemplateForm], + [TriggerType.ALARM_ASSIGNMENT, this.alarmAssignmentTemplateForm], + [TriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, this.ruleEngineEventsTemplateForm], + [TriggerType.ENTITIES_LIMIT, this.entitiesLimitTemplateForm], + [TriggerType.API_USAGE_LIMIT, this.apiUsageLimitTemplateForm], + [TriggerType.NEW_PLATFORM_VERSION, this.newPlatformVersionTemplateForm] + ]); + + if (data.isAdd || data.isCopy) { + this.dialogTitle = 'notification.add-rule'; + } + this.ruleNotification = deepClone(this.data.rule); + + if (this.ruleNotification) { + if (this.data.isCopy) { + this.ruleNotification.name += ` (${this.translate.instant('action.copy')})`; + } else { + this.ruleNotificationForm.get('triggerType').disable({emitEvent: false}); + } + this.ruleNotificationForm.reset({}, {emitEvent: false}); + this.ruleNotificationForm.patchValue(this.ruleNotification, {emitEvent: false}); + this.ruleNotificationForm.get('triggerType').updateValueAndValidity({onlySelf: true}); + const currentForm = this.triggerTypeFormsMap.get(this.ruleNotification.triggerType); + currentForm.patchValue(this.ruleNotification, {emitEvent: false}); + if (this.ruleNotification.triggerType === TriggerType.DEVICE_ACTIVITY) { + this.deviceInactivityTemplateForm.get('triggerConfig.filterByDevice') + .patchValue(!!this.ruleNotification.triggerConfig.devices, {onlySelf: true}); + } + } + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + changeStep($event: StepperSelectionEvent) { + this.selectedIndex = $event.selectedIndex; + } + + backStep() { + this.addNotificationRule.previous(); + } + + nextStep() { + if (this.selectedIndex >= this.maxStepperIndex) { + this.add(); + } else { + this.addNotificationRule.next(); + } + } + + nextStepLabel(): string { + if (this.selectedIndex !== 0) { + return (this.data.isAdd || this.data.isCopy) ? 'action.add' : 'action.save'; + } + return 'action.next'; + } + + private get maxStepperIndex(): number { + return this.addNotificationRule?._steps?.length - 1; + } + + private add(): void { + if (this.allValid()) { + let formValue = this.ruleNotificationForm.value; + const triggerType: TriggerType = this.ruleNotificationForm.get('triggerType').value; + const currentForm = this.triggerTypeFormsMap.get(triggerType); + Object.assign(formValue, currentForm.value); + if (triggerType === TriggerType.DEVICE_ACTIVITY) { + delete formValue.triggerConfig.filterByDevice; + } + formValue.recipientsConfig.triggerType = triggerType; + formValue.triggerConfig.triggerType = triggerType; + if (this.ruleNotification && !this.data.isCopy) { + formValue = {...this.ruleNotification, ...formValue}; + } + this.notificationService.saveNotificationRule(deepTrim(formValue)).subscribe( + (target) => this.dialogRef.close(target) + ); + } + } + + private allValid(): boolean { + return !this.addNotificationRule.steps.find((item, index) => { + if (item.stepControl.invalid) { + item.interacted = true; + this.addNotificationRule.selectedIndex = index; + return true; + } else { + return false; + } + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + createTarget($event: Event, button: MatButton) { + if ($event) { + $event.stopPropagation(); + } + button._elementRef.nativeElement.blur(); + this.dialog.open(RecipientNotificationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: {} + }).afterClosed() + .subscribe((res) => { + if (res) { + let formValue: string[] = this.ruleNotificationForm.get('recipientsConfig.targets').value; + if (!formValue) { + formValue = []; + } + formValue.push(res.id.id); + this.ruleNotificationForm.get('recipientsConfig.targets').patchValue(formValue); + } + }); + } + + countRecipientsChainConfig(): number { + return Object.keys(this.ruleNotificationForm.get('recipientsConfig.escalationTable').value ?? {}).length; + } + + formatLabel(value: number): string { + const formatValue = (value * 100).toFixed(); + return `${formatValue}%`; + } + + private isSysAdmin(): boolean { + return this.authUser.authority === Authority.SYS_ADMIN; + } + + private allowTriggerTypes(): TriggerType[] { + const sysAdminAllowTriggerTypes = new Set([ + TriggerType.ENTITIES_LIMIT, + TriggerType.API_USAGE_LIMIT, + TriggerType.NEW_PLATFORM_VERSION, + ]); + + if (this.isSysAdmin()) { + return Array.from(sysAdminAllowTriggerTypes); + } + return Object.values(TriggerType).filter(type => !sysAdminAllowTriggerTypes.has(type)); + } + + get allowEntityTypeForEntityAction(): EntityType[] { + if (!this._allowEntityTypeForEntityAction) { + const excludeEntityType: Set = new Set([ + EntityType.API_USAGE_STATE, + EntityType.TENANT_PROFILE, + EntityType.RPC, + EntityType.QUEUE, + EntityType.NOTIFICATION, + EntityType.NOTIFICATION_REQUEST, + EntityType.WIDGET_TYPE + ]); + this._allowEntityTypeForEntityAction = Object.values(EntityType).filter(type => !excludeEntityType.has(type)); + } + return this._allowEntityTypeForEntityAction; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-config.resolver.ts new file mode 100644 index 0000000000..86f8120ae0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-config.resolver.ts @@ -0,0 +1,157 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + CellActionDescriptor, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { EntityType, EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@shared/models/page/sort-order'; +import { NotificationRule, TriggerTypeTranslationMap } from '@shared/models/notification.models'; +import { NotificationService } from '@core/http/notification.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { RuleTableHeaderComponent } from '@home/pages/notification/rule/rule-table-header.component'; +import { + RuleNotificationDialogComponent, + RuleNotificationDialogData +} from '@home/pages/notification/rule/rule-notification-dialog.component'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { DatePipe } from '@angular/common'; + +@Injectable() +export class RuleTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private notificationService: NotificationService, + private translate: TranslateService, + private dialog: MatDialog, + private datePipe: DatePipe) { + + this.config.entityType = EntityType.NOTIFICATION_RULE; + this.config.detailsPanelEnabled = false; + this.config.addEnabled = false; + this.config.rowPointer = true; + + this.config.entityTranslations = entityTypeTranslations.get(EntityType.NOTIFICATION_RULE); + this.config.entityResources = {} as EntityTypeResource; + + this.config.entitiesFetchFunction = pageLink => this.notificationService.getNotificationRules(pageLink); + + this.config.deleteEntityTitle = rule => this.translate.instant('notification.delete-rule-title', {ruleName: rule.name}); + this.config.deleteEntityContent = () => this.translate.instant('notification.delete-rule-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('notification.delete-rules-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('notification.delete-rules-text'); + this.config.deleteEntity = id => this.notificationService.deleteNotificationRule(id.id); + + this.config.cellActionDescriptors = this.configureCellActions(); + this.config.headerComponent = RuleTableHeaderComponent; + this.config.onEntityAction = action => this.onTargetAction(action); + + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.handleRowClick = ($event, rule) => { + this.editRule($event, rule); + return true; + }; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('name', 'notification.rule-name', '30%'), + new EntityTableColumn('templateName', 'notification.template', '20%'), + new EntityTableColumn('triggerType', 'notification.trigger.trigger', '20%', + (rule) => this.translate.instant(TriggerTypeTranslationMap.get(rule.triggerType)) || '', + () => ({}), true), + new EntityTableColumn('additionalConfig.description', 'notification.description', '30%', + (target) => target.additionalConfig.description || '', + () => ({}), false) + ); + } + + resolve(route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private configureCellActions(): Array> { + return [{ + name: '', + nameFunction: (entity) => + this.translate.instant(entity.enabled ? 'notification.rule-disable' : 'notification.rule-enable'), + mdiIcon: 'mdi:toggle-switch', + isEnabled: () => true, + mdiIconFunction: (entity) => entity.enabled ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', + onAction: ($event, entity) => this.toggleEnableMode($event, entity) + }, + { + name: this.translate.instant('notification.copy-rule'), + icon: 'content_copy', + isEnabled: () => true, + onAction: ($event, entity) => this.editRule($event, entity, false, true) + }]; + } + + private editRule($event: Event, rule: NotificationRule, isAdd = false, isCopy = false) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(RuleNotificationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + isCopy, + rule + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.updateData(); + } + }); + } + + private onTargetAction(action: EntityAction): boolean { + switch (action.action) { + case 'add': + this.editRule(action.event, action.entity, true); + return true; + } + return false; + } + + private toggleEnableMode($event: Event, rule: NotificationRule): void { + if ($event) { + $event.stopPropagation(); + } + + const modifyRule: NotificationRule = { + ...rule, + enabled: !rule.enabled + }; + + this.notificationService.saveNotificationRule(modifyRule, {ignoreLoading: true}) + .subscribe((notificationRule) => { + rule.enabled = notificationRule.enabled; + this.config.getTable().detectChanges(); + }); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-header.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-header.component.html new file mode 100644 index 0000000000..b466fa2d06 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-header.component.html @@ -0,0 +1,24 @@ + +
    + +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-header.component.scss new file mode 100644 index 0000000000..33537b4b23 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-header.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + width: 10000px; +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-header.component.ts new file mode 100644 index 0000000000..c6270310d6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-header.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { NotificationRule } from '@shared/models/notification.models'; + +@Component({ + selector: 'tb-rule-table-header', + templateUrl: './rule-table-header.component.html', + styleUrls: ['rule-table-header.component.scss'] +}) +export class RuleTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } + + createTarget($event: Event) { + this.entitiesTableConfig.onEntityAction({event: $event, action: 'add', entity: null}); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html new file mode 100644 index 0000000000..654c75f515 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html @@ -0,0 +1,40 @@ + + +

    notification.failed-send

    + + +
    +
    +
    +
    {{ notificationDeliveryMethodTranslateMap.get(errorStat.key) | translate }}
    + + + + + +
    + {{ error.key }} + {{ error.value }}
    + +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.scss new file mode 100644 index 0000000000..95301b4daa --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.scss @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + display: block; + width: 600px; + max-width: 100%; + + h6 { + font-weight: 500; + font-size: 16px; + line-height: 20px; + margin-top: 0; + } + + .tb-table-list { + table-layout: fixed; + width: 100%; + border: hidden; + border-collapse: collapse; + + td { + border: 8px solid transparent; + border-collapse: collapse; + font-size: 14px; + } + + .tb-row-error { + word-wrap: break-word; + } + } + + .mat-divider { + margin: 24px 0; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts new file mode 100644 index 0000000000..7ce286717a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + NotificationDeliveryMethod, + NotificationDeliveryMethodTranslateMap, + NotificationRequest +} from '@shared/models/notification.models'; + +export interface NotificationRequestErrorDialogData { + notificationRequest: NotificationRequest; +} + +@Component({ + selector: 'tb-notification-send-error-dialog', + templateUrl: './sent-error-dialog.component.html', + styleUrls: ['sent-error-dialog.component.scss'] +}) +export class SentErrorDialogComponent extends DialogComponent { + + errorStats: { [key in NotificationDeliveryMethod]: {[errorKey in string]: string}}; + + notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: NotificationRequestErrorDialogData) { + super(store, router, dialogRef); + + this.errorStats = data.notificationRequest.stats.errors; + } + + cancel(): void { + this.dialogRef.close(null); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html new file mode 100644 index 0000000000..4ff13778e5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -0,0 +1,409 @@ + + +

    {{ dialogTitle | translate }}

    + +
    + +
    + + +
    +
    + + + check + + + {{ 'notification.compose' | translate }} +
    +
    + + {{ 'notification.start-from-scratch' | translate }} + {{ 'notification.use-template' | translate }} + +
    +
    + + + +
    + + +
    +
    +
    +
    + +
    notification.at-least-one-should-be-selected
    +
    + +
    +
    + + + + {{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }} + + + chevron_right + + + +
    + + {{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }} + +
    +
    +
    +
    +
    +
    +
    + + + + + +
    + + {{ 'notification.scheduler-later' | translate }} + +
    + + + + notification.time + + + + +
    +
    +
    +
    + + {{ 'notification.delivery-method.web' | translate }} +
    + {{ 'notification.input-fields-support-templatization' | translate}} + +
    +
    + + notification.subject + + + {{ 'notification.subject-required' | translate }} + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
    +
    + + {{ 'icon.icon' | translate }} + +
    + + + + +
    +
    +
    + + {{ 'notification.action-button' | translate }} + +
    +
    + + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{ 'notification.button-text-max-length' | translate : + {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} + }} + + +
    +
    + + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + + + + + + + +
    + + {{ 'notification.set-entity-from-notification' | translate }} + +
    +
    +
    +
    +
    + + {{ 'notification.delivery-method.email' | translate }} + +
    + {{ 'notification.input-fields-support-templatization' | translate}} + +
    +
    + + notification.subject + + + {{ 'notification.subject-required' | translate }} + + + notification.message + + + {{ 'notification.message-required' | translate }} + +
    +
    +
    + + {{ 'notification.delivery-method.sms' | translate }} +
    + {{ 'notification.input-field-support-templatization' | translate}} + +
    +
    + + notification.message + + + {{ 'notification.message-required' | translate }} + + + {{ 'notification.message-max-length' | translate : + {length: smsTemplateForm.get('body').getError('maxlength').requiredLength} + }} + + +
    +
    + + {{ 'notification.delivery-method.slack' | translate }} +
    + {{ 'notification.input-field-support-templatization' | translate}} + +
    +
    + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
    +
    + + {{ 'notification.review' | translate }} + + +
    +
    +
    + +
    notification.delivery-method.web-preview
    +
    +
    + +
    +
    +
    +
    + +
    notification.delivery-method.email-preview
    +
    +
    +
    {{ preview.processedTemplates.EMAIL.subject }}
    + +
    +
    +
    +
    +
    + +
    notification.delivery-method.sms-preview
    +
    +
    + {{ preview.processedTemplates.SMS.body }} +
    +
    +
    +
    + +
    notification.delivery-method.slack-preview
    +
    +
    + {{ preview.processedTemplates.SLACK.body }} +
    +
    +
    +
    + supervisor_account +
    {{ 'notification.recipients-count' | translate : {count: preview.totalRecipientsCount} }}
    +
    +
    +
    + {{ detail.value }}{{ detail.key }} +
    +
    + + + + {{ recipientTitle }} + + +
    +
    +
    +
    +
    + +
    + + + +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss new file mode 100644 index 0000000000..0702d6bf52 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss @@ -0,0 +1,286 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../scss/constants"; +@import "../../../../../../theme"; + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + width: 820px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: flex; + flex-direction: column; + + .tb-title { + font-size: 16px; + line-height: 24px; + &.tb-required::after { + font-size: initial; + content: "*"; + } + + &.tb-error { + color: var(--mdc-theme-error, #f44336); + + &.tb-required::after { + color: var(--mdc-theme-error, #f44336); + } + } + } + + .tb-mat-error { + font-size: 13px; + } + + .tb-hint { + padding: 0 0 8px; + } + + .tb-hint-available-params { + border-radius: 6px; + background-color: rgba(48, 86, 128, 0.04); + margin-bottom: 8px; + padding: 8px 16px; + + .content { + vertical-align: middle; + } + } + + .delivery-title { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .delivery-methods-container { + margin-bottom: 20px; + display: flex; + flex-wrap: wrap; + gap: 8px; + + .delivery-method-container { + display: inline-flex; + flex: 1 1 calc(50% - 8px); + padding: 16px 12px; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 6px; + + &.interact { + cursor: pointer; + color: inherit; + } + + .delivery-method { + width: 100%; + height: 100%; + } + } + } + + .additional-config-group { + padding: 16px 16px 0; + margin-bottom: 12px; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 6px; + width: 100%; + height: 100%; + + .toggle { + margin-bottom: 16px; + } + } + + .preview-group { + padding: 16px; + margin-bottom: 10px; + border: 1px groove rgba(0, 0, 0, .12); + border-radius: 4px; + + &.notification { + background-color: #F3F6FA; + } + + .group-title { + font-weight: 500; + font-size: 14px; + } + + &> div:not(:last-child) { + margin-bottom: 12px; + } + + .details-recipient { + font-size: 14px; + line-height: 14px; + + .number { + font-weight: 500; + margin-right: 4px; + } + + &:not(:last-of-type) { + margin-bottom: 8px; + } + } + + .divider { + margin-bottom: 8px; + } + + .web-preview { + flex-direction: row; + align-items: center; + justify-content: center; + display: flex; + + tb-notification { + border: 1px groove $tb-primary-color; + border-radius: 4px; + max-width: 400px; + width: 80%; + background-color: #fff; + } + } + + .notification-content { + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 4px; + background: #fff; + padding: 12px 16px; + display: inline-grid; + width: 100%; + + .subject { + padding-bottom: 12px; + font-weight: 500; + letter-spacing: 0.25px; + } + + .html-content { + margin-top: 12px; + font-weight: 400; + font-size: 14px; + line-height: 20px; + overflow-x: auto; + } + } + } +} + +:host ::ng-deep { + .mat-mdc-dialog-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0 !important; + + .mat-stepper-horizontal { + display: flex; + height: 100%; + overflow: hidden; + + .mat-horizontal-stepper-wrapper { + flex: 1 1 100%; + } + + .mat-horizontal-content-container { + height: 680px; + max-height: 100%; + width: 100%;; + overflow-y: auto; + scrollbar-gutter: stable; + @media #{$mat-gt-sm} { + min-width: 500px; + } + } + } + } + + .mat-button-toggle-group.tb-notification-use-template-toggle-group { + &.mat-button-toggle-group-appearance-standard { + border: none; + border-radius: 18px; + margin-bottom: 24px; + + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + } + + .mat-button-toggle { + background: rgba(0, 0, 0, 0.06); + height: 36px; + align-items: center; + display: flex; + + .mat-button-toggle-ripple { + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border-radius: 18px; + } + } + + .mat-button-toggle-button { + color: #959595; + } + + .mat-button-toggle-focus-overlay { + border-radius: 18px; + margin: 2px; + } + + .mat-button-toggle-checked .mat-button-toggle-button { + background-color: $tb-primary-color; + color: #fff; + border-radius: 18px; + margin-left: 2px; + margin-right: 2px; + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: 34px; + font-size: 16px; + font-weight: 500; + } + + .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { + opacity: .01; + } + } + + .preview-group { + .notification-content { + .html-content, + .html-content * { + all: revert; + } + } + } +} + +:host ::ng-deep { + .delivery-methods-container { + .delivery-method-container { + &.interact * { + cursor: pointer; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts new file mode 100644 index 0000000000..4846cd063d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts @@ -0,0 +1,355 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + NotificationDeliveryMethod, + NotificationRequest, + NotificationRequestPreview, + NotificationTarget, + NotificationType +} from '@shared/models/notification.models'; +import { Component, Inject, OnDestroy, ViewChild } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { NotificationService } from '@core/http/notification.service'; +import { deepTrim, guid, isDefinedAndNotNull } from '@core/utils'; +import { Observable } from 'rxjs'; +import { EntityType } from '@shared/models/entity-type.models'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { MatStepper } from '@angular/material/stepper'; +import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { map, takeUntil } from 'rxjs/operators'; +import { getCurrentTime } from '@shared/models/time/time.models'; +import { + RecipientNotificationDialogComponent, + RecipientNotificationDialogData +} from '@home/pages/notification/recipient/recipient-notification-dialog.component'; +import { MatButton } from '@angular/material/button'; +import { TemplateConfiguration } from '@home/pages/notification/template/template-configuration'; +import { Authority } from '@shared/models/authority.enum'; +import { AuthUser } from '@shared/models/user.model'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { TranslateService } from '@ngx-translate/core'; + +export interface RequestNotificationDialogData { + request?: NotificationRequest; + isAdd?: boolean; +} + +@Component({ + selector: 'tb-sent-notification-dialog', + templateUrl: './sent-notification-dialog.component.html', + styleUrls: ['./sent-notification-dialog.component.scss'] +}) +export class SentNotificationDialogComponent extends + TemplateConfiguration implements OnDestroy { + + @ViewChild('createNotification', {static: true}) createNotification: MatStepper; + stepperOrientation: Observable; + stepperLabelPosition: Observable<'bottom' | 'end'>; + + isAdd = true; + entityType = EntityType; + notificationType = NotificationType; + + notificationRequestForm: FormGroup; + + selectedIndex = 0; + preview: NotificationRequestPreview = null; + + dialogTitle = 'notification.notify-again'; + + showRefresh = false; + + private authUser: AuthUser = getCurrentAuthUser(this.store); + + private allowNotificationDeliveryMethods: Array; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: RequestNotificationDialogData, + private breakpointObserver: BreakpointObserver, + protected fb: FormBuilder, + private notificationService: NotificationService, + private dialog: MatDialog, + private translate: TranslateService) { + super(store, router, dialogRef, fb); + + this.notificationDeliveryMethods.forEach(method => { + if (method !== NotificationDeliveryMethod.WEB) { + this.templateNotificationForm.get('configuration.deliveryMethodsTemplates').get(method).disable({emitEvent: false}); + } + }); + + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-sm']) + .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); + + this.stepperLabelPosition = this.breakpointObserver.observe(MediaBreakpoints['gt-md']) + .pipe(map(({matches}) => matches ? 'end' : 'bottom')); + + this.notificationRequestForm = this.fb.group({ + useTemplate: [false], + templateId: [{value: null, disabled: true}, Validators.required], + targets: [null, Validators.required], + template: this.templateNotificationForm, + additionalConfig: this.fb.group({ + enabled: [false], + timezone: [{value: '', disabled: true}, Validators.required], + time: [{value: 0, disabled: true}, Validators.required] + }) + }); + + this.notificationRequestForm.get('template.name').setValue(guid()); + + this.notificationRequestForm.get('useTemplate').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.notificationRequestForm.get('templateId').enable({emitEvent: false}); + this.notificationRequestForm.get('template').disable({emitEvent: false}); + } else { + this.notificationRequestForm.get('templateId').disable({emitEvent: false}); + this.notificationRequestForm.get('template').enable({emitEvent: false}); + this.updateDeliveryMethodsDisableState(); + } + }); + + this.notificationRequestForm.get('additionalConfig.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.notificationRequestForm.get('additionalConfig.timezone').enable({emitEvent: false}); + this.notificationRequestForm.get('additionalConfig.time').enable({emitEvent: false}); + } else { + this.notificationRequestForm.get('additionalConfig.timezone').disable({emitEvent: false}); + this.notificationRequestForm.get('additionalConfig.time').disable({emitEvent: false}); + } + }); + + if (data.isAdd) { + this.dialogTitle = 'notification.new-notification'; + } + if (data.request) { + this.notificationRequestForm.reset({}, {emitEvent: false}); + this.notificationRequestForm.patchValue(this.data.request, {emitEvent: false}); + this.notificationRequestForm.get('template.name').setValue(guid()); + let useTemplate = true; + if (isDefinedAndNotNull(this.data.request.template)) { + useTemplate = false; + // eslint-disable-next-line guard-for-in + for (const method in this.data.request.template.configuration.deliveryMethodsTemplates) { + this.deliveryMethodFormsMap.get(NotificationDeliveryMethod[method]) + .patchValue(this.data.request.template.configuration.deliveryMethodsTemplates[method]); + } + } + this.notificationRequestForm.get('useTemplate').setValue(useTemplate, {onlySelf : true}); + } + + this.refreshAllowDeliveryMethod(); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + cancel(): void { + this.dialogRef.close(null); + } + + nextStepLabel(): string { + if (this.selectedIndex >= this.maxStepperIndex) { + return 'action.send'; + } + return 'action.next'; + } + + changeStep($event: StepperSelectionEvent) { + this.selectedIndex = $event.selectedIndex; + if (this.selectedIndex === this.maxStepperIndex) { + this.getPreview(); + } + } + + backStep() { + this.createNotification.previous(); + } + + nextStep() { + if (this.selectedIndex >= this.maxStepperIndex) { + this.add(); + } else { + this.createNotification.next(); + } + } + + private get maxStepperIndex(): number { + return this.createNotification?._steps?.length - 1; + } + + private add(): void { + if (this.allValid()) { + this.notificationService.createNotificationRequest(this.notificationFormValue).subscribe( + (notification) => this.dialogRef.close(notification) + ); + } + } + + private getPreview() { + if (this.allValid()) { + this.preview = null; + this.notificationService.getNotificationRequestPreview(this.notificationFormValue).pipe( + map(data => { + if (data.processedTemplates.WEB?.enabled) { + (data.processedTemplates.WEB as any).text = data.processedTemplates.WEB.body; + } + return data; + }) + ).subscribe( + (preview) => this.preview = preview + ); + } + } + + private get notificationFormValue(): NotificationRequest { + const formValue = deepTrim(this.notificationRequestForm.value); + if (!formValue.useTemplate) { + formValue.template = super.getNotificationTemplateValue(); + } + delete formValue.useTemplate; + let delay = 0; + if (formValue.additionalConfig.enabled) { + delay = (this.notificationRequestForm.value.additionalConfig.time.valueOf() - this.minDate().valueOf()) / 1000; + } + formValue.additionalConfig = { + sendingDelayInSec: delay > 0 ? delay : 0 + }; + return formValue; + } + + private allValid(): boolean { + return !this.createNotification.steps.find((item, index) => { + if (item.stepControl?.invalid) { + item.interacted = true; + this.createNotification.selectedIndex = index; + return true; + } else { + return false; + } + }); + } + + private isSysAdmin(): boolean { + return this.authUser.authority === Authority.SYS_ADMIN; + } + + private isTenantAdmin(): boolean { + return this.authUser.authority === Authority.TENANT_ADMIN; + } + + minDate(): Date { + return new Date(getCurrentTime(this.notificationRequestForm.get('additionalConfig.timezone').value).format('lll')); + } + + maxDate(): Date { + const date = this.minDate(); + date.setDate(date.getDate() + 7); + return date; + } + + createTarget($event: Event, button: MatButton) { + if ($event) { + $event.stopPropagation(); + } + button._elementRef.nativeElement.blur(); + this.dialog.open(RecipientNotificationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: {} + }).afterClosed() + .subscribe((res) => { + if (res) { + let formValue: string[] = this.notificationRequestForm.get('targets').value; + if (!formValue) { + formValue = []; + } + formValue.push(res.id.id); + this.notificationRequestForm.get('targets').patchValue(formValue); + } + }); + } + + getDeliveryMethodsTemplatesControl(deliveryMethod: NotificationDeliveryMethod): AbstractControl { + return this.templateNotificationForm.get('configuration.deliveryMethodsTemplates').get(deliveryMethod).get('enabled'); + } + + getDeliveryMethodsTooltip(deliveryMethod: NotificationDeliveryMethod): string { + if (this.allowConfigureDeliveryMethod(deliveryMethod)) { + return this.translate.instant('notification.delivery-method-not-configure-click'); + } + return this.translate.instant('notification.delivery-method-not-configure-contact'); + } + + allowConfigureDeliveryMethod(deliveryMethod: NotificationDeliveryMethod): boolean { + if (deliveryMethod === NotificationDeliveryMethod.WEB) { + return false; + } + if(this.isSysAdmin()) { + return true; + } else if (this.isTenantAdmin()) { + return deliveryMethod === NotificationDeliveryMethod.SLACK; + } + return false; + } + + isInteractDeliveryMethod(deliveryMethod: NotificationDeliveryMethod) { + return this.getDeliveryMethodsTemplatesControl(deliveryMethod).disabled && this.allowConfigureDeliveryMethod(deliveryMethod); + } + + configurationPage(deliveryMethod: NotificationDeliveryMethod) { + switch (deliveryMethod) { + case NotificationDeliveryMethod.EMAIL: + return '/settings/outgoing-mail'; + case NotificationDeliveryMethod.SMS: + case NotificationDeliveryMethod.SLACK: + return '/settings/notifications'; + } + } + + refreshAllowDeliveryMethod() { + this.notificationService.getAvailableDeliveryMethods({ignoreLoading: true}).subscribe(allowMethods => { + this.allowNotificationDeliveryMethods = allowMethods; + this.updateDeliveryMethodsDisableState(); + this.showRefresh = (this.notificationDeliveryMethods.length !== allowMethods.length); + }); + } + + private updateDeliveryMethodsDisableState() { + this.notificationDeliveryMethods.forEach(method => { + if (this.allowNotificationDeliveryMethods.includes(method)) { + this.getDeliveryMethodsTemplatesControl(method).enable({emitEvent: true}); + } else { + this.getDeliveryMethodsTemplatesControl(method).disable({emitEvent: true}); + this.getDeliveryMethodsTemplatesControl(method).setValue(false, {emitEvent: true}); //used for notify again + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-table-config.resolver.ts new file mode 100644 index 0000000000..290774b6e9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-table-config.resolver.ts @@ -0,0 +1,208 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + CellActionDescriptor, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { + NotificationDeliveryMethodTranslateMap, + NotificationRequest, + NotificationRequestInfo, + NotificationRequestStats, + NotificationRequestStatus, + NotificationRequestStatusTranslateMap, + NotificationTemplate +} from '@shared/models/notification.models'; +import { NotificationService } from '@core/http/notification.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityType, EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@shared/models/page/sort-order'; +import { DatePipe } from '@angular/common'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { + RequestNotificationDialogData, + SentNotificationDialogComponent +} from '@home/pages/notification/sent/sent-notification-dialog.componet'; +import { PageLink } from '@shared/models/page/page-link'; +import { + NotificationRequestErrorDialogData, + SentErrorDialogComponent +} from '@home/pages/notification/sent/sent-error-dialog.component'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class SentTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = + new EntityTableConfig(); + + constructor(private notificationService: NotificationService, + private translate: TranslateService, + private dialog: MatDialog, + private datePipe: DatePipe) { + + this.config.entityType = EntityType.NOTIFICATION_REQUEST; + this.config.detailsPanelEnabled = false; + this.config.addEnabled = false; + this.config.searchEnabled = false; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.NOTIFICATION_REQUEST); + this.config.entityResources = {} as EntityTypeResource; + + this.config.deleteEntityTitle = () => this.translate.instant('notification.delete-request-title'); + this.config.deleteEntityContent = () => this.translate.instant('notification.delete-request-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('notification.delete-requests-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('notification.delete-requests-text'); + + this.config.deleteEntity = id => this.notificationService.deleteNotificationRequest(id.id); + this.config.entitiesFetchFunction = pageLink => this.notificationService.getNotificationRequests(pageLink); + + this.config.cellActionDescriptors = this.configureCellActions(); + + this.config.onEntityAction = action => this.onRequestAction(action); + + this.config.handleRowClick = (event, entity) => { + if ((event.target as HTMLElement).getElementsByClassName('stats').length || (event.target as HTMLElement).className === 'stats') { + this.openStatsErrorDialog(event, entity); + } + return true; + }; + + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('status', 'notification.status', '15%', + request => `${this.requestStatus(request.status)}${this.requestStats(request.stats)}`, + request => this.requestStatusStyle(request.status)), + new EntityTableColumn('deliveryMethods', 'notification.delivery-method.delivery-method', '15%', + (request) => request.deliveryMethods + .map((deliveryMethod) => this.translate.instant(NotificationDeliveryMethodTranslateMap.get(deliveryMethod))).join(', '), + () => ({}), false), + new EntityTableColumn('templateName', 'notification.template', '70%') + ); + } + + resolve(route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private configureCellActions(): Array> { + return [{ + name: this.translate.instant('notification.notify-again'), + mdiIcon: 'mdi:repeat-variant', + isEnabled: (request) => request.status !== NotificationRequestStatus.SCHEDULED, + onAction: ($event, entity) => this.createRequest($event, entity) + }]; + } + + private createRequest($event: Event, request: NotificationRequest, isAdd = false, updateData = true) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(SentNotificationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + request + } + }).afterClosed().subscribe((res) => { + if (res && updateData) { + this.config.updateData(); + } + }); + } + + private onRequestAction(action: EntityAction): boolean { + switch (action.action) { + case 'add': + this.createRequest(action.event, action.entity, true); + return true; + case 'add-without-update': + this.createRequest(action.event, action.entity, true, false); + return true; + } + return false; + } + + private requestStatus(status: NotificationRequestStatus): string { + const translateKey = NotificationRequestStatusTranslateMap.get(status); + let backgroundColor = 'rgba(25, 128, 56, 0.08)'; + switch (status) { + case NotificationRequestStatus.SCHEDULED: + backgroundColor = 'rgba(48, 86, 128, 0.08)'; + break; + case NotificationRequestStatus.PROCESSING: + backgroundColor = 'rgba(212, 125, 24, 0.08)'; + break; + } + return `
    + ${this.translate.instant(translateKey)} +
    `; + } + + private requestStats(stats: NotificationRequestStats): string { + if (!stats?.errors) { + return ''; + } + let countError = 0; + Object.keys(stats.errors).forEach(method => countError += Object.keys(stats.errors[method]).length); + if (countError === 0) { + return ''; + } + return `
    + ${this.translate.instant('notification.fails', {count: countError})} > +
    `; + } + + private requestStatusStyle(status: NotificationRequestStatus): object { + const styleObj = { + fontSize: '14px', + color: '#198038' + }; + switch (status) { + case NotificationRequestStatus.SCHEDULED: + styleObj.color = '#305680'; + break; + case NotificationRequestStatus.PROCESSING: + styleObj.color = '#D47D18'; + break; + } + return styleObj; + } + + private openStatsErrorDialog($event: Event, notificationRequest: NotificationRequest) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(SentErrorDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + notificationRequest + } + }).afterClosed().subscribe(() => {}); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts new file mode 100644 index 0000000000..b3fa00ef72 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts @@ -0,0 +1,201 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; +import { + ActionButtonLinkType, + ActionButtonLinkTypeTranslateMap, + NotificationDeliveryMethod, + NotificationDeliveryMethodTranslateMap, + NotificationTemplate, + NotificationTemplateTypeTranslateMap, + NotificationType +} from '@shared/models/notification.models'; +import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { Directive, OnDestroy } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { deepClone, deepTrim } from '@core/utils'; + +@Directive() +// tslint:disable-next-line:directive-class-suffix +export abstract class TemplateConfiguration extends DialogComponent implements OnDestroy{ + + templateNotificationForm: FormGroup; + webTemplateForm: FormGroup; + emailTemplateForm: FormGroup; + smsTemplateForm: FormGroup; + slackTemplateForm: FormGroup; + + notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[]; + notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; + notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; + + actionButtonLinkType = ActionButtonLinkType; + actionButtonLinkTypes = Object.keys(ActionButtonLinkType) as ActionButtonLinkType[]; + actionButtonLinkTypeTranslateMap = ActionButtonLinkTypeTranslateMap; + + tinyMceOptions: Record = { + base_url: '/assets/tinymce', + suffix: '.min', + plugins: ['link table image imagetools code fullscreen'], + menubar: 'edit insert tools view format table', + toolbar: 'fontselect fontsizeselect | formatselect | bold italic strikethrough forecolor backcolor ' + + '| link | table | image | alignleft aligncenter alignright alignjustify ' + + '| numlist bullist outdent indent | removeformat | code | fullscreen', + height: 400, + autofocus: false, + branding: false + }; + + protected readonly destroy$ = new Subject(); + + protected deliveryMethodFormsMap: Map; + + protected constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + protected fb: FormBuilder) { + super(store, router, dialogRef); + + this.templateNotificationForm = this.fb.group({ + name: ['', Validators.required], + notificationType: [NotificationType.GENERAL], + configuration: this.fb.group({ + deliveryMethodsTemplates: this.fb.group({}, {validators: this.atLeastOne()}) + }) + }); + + this.notificationDeliveryMethods.forEach(method => { + (this.templateNotificationForm.get('configuration.deliveryMethodsTemplates') as FormGroup) + .addControl(method, this.fb.group({enabled: method === NotificationDeliveryMethod.WEB}), {emitEvent: false}); + }); + + this.webTemplateForm = this.fb.group({ + subject: ['', Validators.required], + body: ['', Validators.required], + additionalConfig: this.fb.group({ + icon: this.fb.group({ + enabled: [false], + icon: [{value: 'notifications', disabled: true}, Validators.required], + color: ['#757575'] + }), + actionButtonConfig: this.fb.group({ + enabled: [false], + text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], + linkType: [ActionButtonLinkType.LINK], + link: [{value: '', disabled: true}, Validators.required], + dashboardId: [{value: null, disabled: true}, Validators.required], + dashboardState: [{value: null, disabled: true}], + setEntityIdInState: [{value: true, disabled: true}], + }), + }) + }); + + this.webTemplateForm.get('additionalConfig.icon.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + if (value) { + this.webTemplateForm.get('additionalConfig.icon.icon').enable({emitEvent: false}); + } else { + this.webTemplateForm.get('additionalConfig.icon.icon').disable({emitEvent: false}); + } + }); + + this.webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + if (value) { + this.webTemplateForm.get('additionalConfig.actionButtonConfig').enable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').updateValueAndValidity({onlySelf: true}); + } else { + this.webTemplateForm.get('additionalConfig.actionButtonConfig').disable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').enable({emitEvent: false}); + } + }); + + this.webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + const isEnabled = this.webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value; + if (isEnabled) { + if (value === ActionButtonLinkType.LINK) { + this.webTemplateForm.get('additionalConfig.actionButtonConfig.link').enable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').disable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardState').disable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.actionButtonConfig.setEntityIdInState').disable({emitEvent: false}); + } else { + this.webTemplateForm.get('additionalConfig.actionButtonConfig.link').disable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').enable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardState').enable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.actionButtonConfig.setEntityIdInState').enable({emitEvent: false}); + } + } + }); + + this.emailTemplateForm = this.fb.group({ + subject: ['', Validators.required], + body: ['', Validators.required] + }); + + this.smsTemplateForm = this.fb.group({ + body: ['', [Validators.required, Validators.maxLength(320)]] + }); + + this.slackTemplateForm = this.fb.group({ + body: ['', Validators.required] + }); + + this.deliveryMethodFormsMap = new Map([ + [NotificationDeliveryMethod.WEB, this.webTemplateForm], + [NotificationDeliveryMethod.EMAIL, this.emailTemplateForm], + [NotificationDeliveryMethod.SMS, this.smsTemplateForm], + [NotificationDeliveryMethod.SLACK, this.slackTemplateForm] + ]); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + atLeastOne() { + return (group: FormGroup): ValidationErrors | null => { + let hasAtLeastOne = true; + if (group?.controls) { + const controlsFormValue: FormGroup[] = Object.entries(group.controls).map(method => method[1]) as any; + hasAtLeastOne = controlsFormValue.some(value => value.controls.enabled.value); + } + return hasAtLeastOne ? null : {atLeastOne: true}; + }; + } + + protected getNotificationTemplateValue(): NotificationTemplate { + const template: NotificationTemplate = deepClone(this.templateNotificationForm.value); + this.notificationDeliveryMethods.forEach(method => { + if (template.configuration.deliveryMethodsTemplates[method]?.enabled) { + Object.assign(template.configuration.deliveryMethodsTemplates[method], this.deliveryMethodFormsMap.get(method).value, {method}); + } else { + delete template.configuration.deliveryMethodsTemplates[method]; + } + }); + return deepTrim(template); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html new file mode 100644 index 0000000000..067258fa24 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -0,0 +1,272 @@ + + +

    {{ dialogTitle | translate }}

    + +
    + +
    + + +
    +
    + + + check + + + {{ 'notification.basic-settings' | translate }} +
    + + notification.name + + + {{ 'notification.name-required' | translate }} + + + + notification.type + + + {{ notificationTemplateTypeTranslateMap.get(notificationType).name | translate }} + + + +
    + +
    notification.at-least-one-should-be-selected
    +
    +
    + + {{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }} + +
    +
    +
    +
    +
    + + {{ 'notification.delivery-method.web' | translate }} +
    + {{ 'notification.input-fields-support-templatization' | translate}} + +
    +
    + + notification.subject + + + {{ 'notification.subject-required' | translate }} + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
    +
    + + {{ 'icon.icon' | translate }} + +
    + + + + +
    +
    +
    + + {{ 'notification.action-button' | translate }} + +
    +
    + + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{ 'notification.button-text-max-length' | translate : + {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} + }} + + +
    +
    + + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + + + + + + + +
    + + {{ 'notification.set-entity-from-notification' | translate }} + +
    +
    +
    +
    +
    + + {{ 'notification.delivery-method.email' | translate }} + +
    + {{ 'notification.input-fields-support-templatization' | translate}} + +
    +
    + + notification.subject + + + {{ 'notification.subject-required' | translate }} + + + notification.message + + + {{ 'notification.message-required' | translate }} + +
    +
    +
    + + {{ 'notification.delivery-method.sms' | translate }} +
    + {{ 'notification.input-field-support-templatization' | translate}} + +
    +
    + + notification.message + + + {{ 'notification.message-required' | translate }} + + + {{ 'notification.message-max-length' | translate : + {length: smsTemplateForm.get('body').getError('maxlength').requiredLength} + }} + + +
    +
    + + {{ 'notification.delivery-method.slack' | translate }} +
    + {{ 'notification.input-field-support-templatization' | translate}} + +
    +
    + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
    +
    +
    +
    + +
    + + + +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss new file mode 100644 index 0000000000..1a5fa270a8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss @@ -0,0 +1,126 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../scss/constants"; +@import "../../../../../../theme"; + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + width: 800px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: flex; + flex-direction: column; + + .tb-title { + font-size: 16px; + line-height: 24px; + + &.tb-required::after { + font-size: initial; + content: "*"; + } + + &.tb-error { + color: var(--mdc-theme-error, #f44336); + + &.tb-required::after { + color: var(--mdc-theme-error, #f44336); + } + } + } + + .tb-mat-error { + font-size: 13px; + } + + .tb-hint { + padding: 0 0 8px; + } + + .tb-hint-available-params { + border-radius: 6px; + background-color: rgba(48, 86, 128, 0.04); + margin-bottom: 8px; + padding: 8px 16px; + + .content { + vertical-align: middle; + } + } + + .delivery-methods-container { + margin-bottom: 20px; + display: flex; + flex-wrap: wrap; + gap: 8px; + + .delivery-method-container { + display: inline-flex; + flex: 1 1 calc(50% - 8px); + padding: 16px 12px; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 6px; + + .delivery-method { + width: 100%; + height: 100%; + } + } + } + + .additional-config-group { + padding: 16px 16px 4px; + margin-bottom: 12px; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 6px; + width: 100%; + height: 100%; + + .toggle { + margin-bottom: 12px; + } + } +} + +:host ::ng-deep { + .mat-mdc-dialog-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0 !important; + + .mat-stepper-horizontal { + display: flex; + height: 100%; + overflow: hidden; + + .mat-horizontal-stepper-wrapper { + flex: 1 1 100%; + } + + .mat-horizontal-content-container { + height: 550px; + max-height: 100%; + width: 100%;; + overflow-y: auto; + scrollbar-gutter: stable; + @media #{$mat-gt-sm} { + min-width: 500px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts new file mode 100644 index 0000000000..b4ba480732 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts @@ -0,0 +1,191 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NotificationDeliveryMethod, NotificationTemplate, NotificationType } from '@shared/models/notification.models'; +import { Component, Inject, OnDestroy, ViewChild } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder } from '@angular/forms'; +import { NotificationService } from '@core/http/notification.service'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; +import { MatStepper } from '@angular/material/stepper'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { TranslateService } from '@ngx-translate/core'; +import { TemplateConfiguration } from '@home/pages/notification/template/template-configuration'; +import { AuthState } from '@core/auth/auth.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { AuthUser } from '@shared/models/user.model'; +import { Authority } from '@shared/models/authority.enum'; + +export interface TemplateNotificationDialogData { + template?: NotificationTemplate; + predefinedType?: NotificationType; + isAdd?: boolean; + isCopy?: boolean; +} + +@Component({ + selector: 'tb-template-notification-dialog', + templateUrl: './template-notification-dialog.component.html', + styleUrls: ['./template-notification-dialog.component.scss'] +}) +export class TemplateNotificationDialogComponent + extends TemplateConfiguration implements OnDestroy { + + @ViewChild('notificationTemplateStepper', {static: true}) notificationTemplateStepper: MatStepper; + + stepperOrientation: Observable; + stepperLabelPosition: Observable<'bottom' | 'end'>; + + dialogTitle = 'notification.edit-notification-template'; + + notificationTypes: NotificationType[]; + + selectedIndex = 0; + hideSelectType = false; + + private readonly templateNotification: NotificationTemplate; + private authState: AuthState = getCurrentAuthState(this.store); + private authUser: AuthUser = this.authState.authUser; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: TemplateNotificationDialogData, + private breakpointObserver: BreakpointObserver, + protected fb: FormBuilder, + private notificationService: NotificationService, + private translate: TranslateService) { + super(store, router, dialogRef, fb); + + this.notificationTypes = this.allowNotificationType(); + + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-sm']) + .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); + + this.stepperLabelPosition = this.breakpointObserver.observe(MediaBreakpoints['gt-md']) + .pipe(map(({matches}) => matches ? 'end' : 'bottom')); + + if (isDefinedAndNotNull(this.data?.predefinedType)) { + this.hideSelectType = true; + this.templateNotificationForm.get('notificationType').setValue(this.data.predefinedType, {emitEvent: false}); + } + + if (data.isAdd || data.isCopy) { + this.dialogTitle = 'notification.add-notification-template'; + } + this.templateNotification = deepClone(this.data.template); + + if (this.templateNotification) { + if (this.data.isCopy) { + this.templateNotification.name += ` (${this.translate.instant('action.copy')})`; + } else { + this.templateNotificationForm.get('notificationType').disable({emitEvent: false}); + } + this.templateNotificationForm.reset({}, {emitEvent: false}); + this.templateNotificationForm.patchValue(this.templateNotification, {emitEvent: false}); + // eslint-disable-next-line guard-for-in + for (const method in this.templateNotification.configuration.deliveryMethodsTemplates) { + this.deliveryMethodFormsMap.get(NotificationDeliveryMethod[method]) + .patchValue(this.templateNotification.configuration.deliveryMethodsTemplates[method]); + } + } + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + cancel(): void { + this.dialogRef.close(null); + } + + changeStep($event: StepperSelectionEvent) { + this.selectedIndex = $event.selectedIndex; + } + + backStep() { + this.notificationTemplateStepper.previous(); + } + + nextStep() { + if (this.selectedIndex >= this.maxStepperIndex) { + this.add(); + } else { + this.notificationTemplateStepper.next(); + } + } + + nextStepLabel(): string { + if (this.selectedIndex >= this.maxStepperIndex && this.selectedIndex !== 0) { + return (this.data.isAdd || this.data.isCopy) ? 'action.add' : 'action.save'; + } + return 'action.next'; + } + + private get maxStepperIndex(): number { + return this.notificationTemplateStepper?._steps?.length - 1; + } + + private add(): void { + if (this.allValid()) { + let template = this.getNotificationTemplateValue(); + if (this.templateNotification && !this.data.isCopy) { + template = {...this.templateNotification, ...template}; + } + this.notificationService.saveNotificationTemplate(template).subscribe( + (target) => this.dialogRef.close(target) + ); + } + } + + private allValid(): boolean { + return !this.notificationTemplateStepper.steps.find((item, index) => { + if (item.stepControl.invalid) { + item.interacted = true; + this.notificationTemplateStepper.selectedIndex = index; + return true; + } else { + return false; + } + }); + } + + private isSysAdmin(): boolean { + return this.authUser.authority === Authority.SYS_ADMIN; + } + + private allowNotificationType(): NotificationType[] { + const sysAdminAllowNotificationTypes = new Set([ + NotificationType.ENTITIES_LIMIT, + NotificationType.API_USAGE_LIMIT, + NotificationType.NEW_PLATFORM_VERSION, + ]); + + if (this.isSysAdmin()) { + return [NotificationType.GENERAL, ...sysAdminAllowNotificationTypes]; + } + return Object.values(NotificationType).filter(type => !sysAdminAllowNotificationTypes.has(type)); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-config.resolver.ts new file mode 100644 index 0000000000..34f0b42f12 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-config.resolver.ts @@ -0,0 +1,130 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + CellActionDescriptor, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { EntityType, EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@shared/models/page/sort-order'; +import { NotificationTemplate, NotificationTemplateTypeTranslateMap } from '@shared/models/notification.models'; +import { NotificationService } from '@core/http/notification.service'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { MatDialog } from '@angular/material/dialog'; +import { + TemplateNotificationDialogComponent, + TemplateNotificationDialogData +} from '@home/pages/notification/template/template-notification-dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { TemplateTableHeaderComponent } from '@home/pages/notification/template/template-table-header.component'; +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { DatePipe } from '@angular/common'; + +@Injectable() +export class TemplateTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private notificationService: NotificationService, + private translate: TranslateService, + private dialog: MatDialog, + private datePipe: DatePipe) { + + this.config.entityType = EntityType.NOTIFICATION_TEMPLATE; + this.config.detailsPanelEnabled = false; + this.config.addEnabled = false; + this.config.rowPointer = true; + + this.config.entityTranslations = entityTypeTranslations.get(EntityType.NOTIFICATION_TEMPLATE); + this.config.entityResources = {} as EntityTypeResource; + + this.config.entitiesFetchFunction = pageLink => this.notificationService.getNotificationTemplates(pageLink); + + this.config.deleteEntityTitle = template => this.translate.instant('notification.delete-template-title', {templateName: template.name}); + this.config.deleteEntityContent = () => this.translate.instant('notification.delete-template-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('notification.delete-templates-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('notification.delete-templates-text'); + this.config.deleteEntity = id => this.notificationService.deleteNotificationTemplate(id.id); + + this.config.cellActionDescriptors = this.configureCellActions(); + + this.config.headerComponent = TemplateTableHeaderComponent; + this.config.onEntityAction = action => this.onTemplateAction(action); + + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.handleRowClick = ($event, template) => { + this.editTemplate($event, template); + return true; + }; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('notificationType', 'notification.type', '20%', + (template) => this.translate.instant(NotificationTemplateTypeTranslateMap.get(template.notificationType).name)), + new EntityTableColumn('name', 'notification.template', '80%') + ); + } + + resolve(route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private configureCellActions(): Array> { + return [ + { + name: this.translate.instant('notification.copy-template'), + icon: 'content_copy', + isEnabled: () => true, + onAction: ($event, entity) => this.editTemplate($event, entity, false, true) + } + ]; + } + + private editTemplate($event: Event, template: NotificationTemplate, isAdd = false, isCopy = false) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(TemplateNotificationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + isCopy, + template + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.updateData(); + } + }); + } + + private onTemplateAction(action: EntityAction): boolean { + switch (action.action) { + case 'add': + this.editTemplate(action.event, action.entity, true); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-table-header.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-header.component.html new file mode 100644 index 0000000000..39fe86994e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-header.component.html @@ -0,0 +1,24 @@ + +
    + +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-header.component.scss new file mode 100644 index 0000000000..33537b4b23 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-header.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + width: 10000px; +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-header.component.ts new file mode 100644 index 0000000000..f6fd3f7e75 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-header.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { NotificationTemplate } from '@shared/models/notification.models'; + +@Component({ + selector: 'tb-template-table-header', + templateUrl: './template-table-header.component.html', + styleUrls: ['template-table-header.component.scss'] +}) +export class TemplateTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } + + createTemplate($event: Event) { + this.entitiesTableConfig.onEntityAction({event: $event, action: 'add', entity: null}); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts index 9d0f01e663..2af6910e23 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; -const routes: Routes = [ +export const otaUpdatesRoutes: Routes = [ { path: 'otaUpdates', data: { @@ -65,6 +65,18 @@ const routes: Routes = [ } ]; +const routes: Routes = [ + { + path: 'otaUpdates', + pathMatch: 'full', + redirectTo: '/features/otaUpdates' + }, + { + path: 'otaUpdates/:entityId', + redirectTo: '/features/otaUpdates/:entityId' + } +]; + @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts index 8d173ad759..cbadbc8553 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -73,6 +73,7 @@ export class OtaUpdateTableConfigResolve implements Resolve + [showDetailsPageLink]="true"> ota-update.package-type diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts index 4e3db409dd..da479fa190 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { EntityComponent } from '@home/components/entity/entity.component'; import { ChecksumAlgorithm, @@ -39,7 +39,7 @@ import { isNotEmptyStr } from '@core/utils'; }) export class OtaUpdateComponent extends EntityComponent implements OnInit, OnDestroy { - private destroy$ = new Subject(); + private destroy$ = new Subject(); checksumAlgorithms = Object.values(ChecksumAlgorithm); checksumAlgorithmTranslationMap = ChecksumAlgorithmTranslationMap; @@ -50,7 +50,7 @@ export class OtaUpdateComponent extends EntityComponent implements O protected translate: TranslateService, @Inject('entity') protected entityValue: OtaPackage, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, - public fb: FormBuilder, + public fb: UntypedFormBuilder, protected cd: ChangeDetectorRef) { super(store, fb, entityValue, entitiesTableConfigValue, cd); } @@ -100,7 +100,7 @@ export class OtaUpdateComponent extends EntityComponent implements O } } - buildForm(entity: OtaPackage): FormGroup { + buildForm(entity: OtaPackage): UntypedFormGroup { const form = this.fb.group({ title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], version: [entity ? entity.version : '', [Validators.required, Validators.maxLength(255)]], diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts index c768167a5e..1734b10f65 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts index 440db606fd..c14e450745 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html index 3a15616055..fad97beb68 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -1,6 +1,6 @@
    - - -
    + +
    - profile.profile + profile.profile {{ profile ? profile.get('email').value : '' }}
    - profile.last-login-time + profile.last-login-time
    - - - +
    @@ -56,6 +54,11 @@ user.last-name + + language.language @@ -67,7 +70,7 @@
    rulenode.link-labels - - + {{label.name}} close - + - + ; @@ -72,7 +72,7 @@ export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChan @Input() sourceRuleChainId: string; - linksFormGroup: FormGroup; + linksFormGroup: UntypedFormGroup; modelValue: Array; @@ -88,7 +88,7 @@ export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChan private propagateChange = (v: any) => { }; - constructor(private fb: FormBuilder, + constructor(private fb: UntypedFormBuilder, public truncate: TruncatePipe, public translate: TranslateService, private ruleChainService: RuleChainService) { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss index d16ba312ca..931f5ac6fc 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html index b7abe74096..97f2214f2e 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html @@ -1,6 +1,6 @@ -
    +
    -
    - -
    + + + +
    - - - + + + - -
    @@ -111,7 +104,7 @@
    - + + - -
    - +
    +
    {{ 'profile.token-valid-till' | translate }} {{ jwtTokenExpiration | date: 'yyyy-MM-dd HH:mm:ss' }}
    + +
    - - + +
    @@ -123,51 +121,49 @@
    - +
    - + - admin.2fa.2fa + admin.2fa.2fa -
    security.2fa.2fa-description
    +
    security.2fa.2fa-description
    - -

    security.2fa.authenticate-with

    -
    - -
    -

    {{ providersData.get(provider).name | translate }}

    -
    -
    - {{ providersData.get(provider).description | translate }} -
    - -
    - {{ providersData.get(provider).activatedHint | translate: providerDataInfo(provider) }} -
    -
    - - +

    security.2fa.authenticate-with

    + + +
    +

    {{ providersData.get(provider).name | translate }}

    +
    +
    + {{ providersData.get(provider).description | translate }}
    - - security.2fa.main-2fa-method - - + +
    + {{ providersData.get(provider).activatedHint | translate: providerDataInfo(provider) }} +
    +
    + +
    - - - - + + security.2fa.main-2fa-method + + +
    + +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.scss b/ui-ngx/src/app/modules/home/pages/security/security.component.scss index b593be3a2a..5a9f968275 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.scss +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,7 @@ .profile-container { padding: 8px; } - - mat-card.profile-card { + .mat-mdc-card.profile-card { padding: 24px; @media #{$mat-gt-sm} { width: 80%; @@ -52,7 +51,7 @@ margin-bottom: 25px; } - .mat-form-field { + .mat-mdc-form-field { margin-bottom: 4px; } @@ -114,17 +113,12 @@ opacity: 0.87; } - .mat-checkbox { + .mat-mdc-checkbox { margin-top: 8px; } - .mat-stroked-button { + .mat-mdc-outlined-button { margin-top: 8px; } } } -:host ::ng-deep { - .mat-form-field-appearance-fill .mat-form-field-underline::before { - background-color: transparent; - } -} diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.ts b/ui-ngx/src/app/modules/home/pages/security/security.component.ts index 1ecfb66954..9a99e7d6d4 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -21,8 +21,8 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, - FormBuilder, - FormGroup, FormGroupDirective, + UntypedFormBuilder, + UntypedFormGroup, FormGroupDirective, NgForm, ValidationErrors, ValidatorFn, @@ -51,6 +51,7 @@ import { Observable, of, Subject } from 'rxjs'; import { isDefinedAndNotNull, isEqual } from '@core/utils'; import { AuthService } from '@core/auth/auth.service'; import { UserPasswordPolicy } from '@shared/models/settings.models'; +import { MatCheckboxChange } from '@angular/material/checkbox'; @Component({ selector: 'tb-security', @@ -62,8 +63,8 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro private readonly destroy$ = new Subject(); private accountConfig: AccountTwoFaSettingProviders; - twoFactorAuth: FormGroup; - changePassword: FormGroup; + twoFactorAuth: UntypedFormGroup; + changePassword: UntypedFormGroup; user: User; passwordPolicy: UserPasswordPolicy; @@ -93,7 +94,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro private twoFaService: TwoFactorAuthenticationService, public dialog: MatDialog, public dialogService: DialogService, - public fb: FormBuilder, + public fb: UntypedFormBuilder, private datePipe: DatePipe, private authService: AuthService, private clipboardService: ClipboardService) { @@ -290,14 +291,14 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro }); } - changeDefaultProvider(event: MouseEvent, provider: TwoFactorAuthProviderType) { - event.stopPropagation(); - event.preventDefault(); + changeDefaultProvider(event: MatCheckboxChange, provider: TwoFactorAuthProviderType) { if (this.useByDefault !== provider) { this.twoFactorAuth.disable({emitEvent: false}); this.twoFaService.updateTwoFaAccountConfig(provider, true) .pipe(tap(() => this.twoFactorAuth.enable({emitEvent: false}))) .subscribe(data => this.processTwoFactorAuthConfig(data)); + } else { + event.source.checked = true; } } diff --git a/ui-ngx/src/app/modules/home/pages/security/security.module.ts b/ui-ngx/src/app/modules/home/pages/security/security.module.ts index 5db12c2a68..52d6636e0f 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.module.ts +++ b/ui-ngx/src/app/modules/home/pages/security/security.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts index b2acabc0d5..f17b78021e 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html index 4aa21e2aa5..cec78fd60d 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html @@ -1,6 +1,6 @@
    - - - login.create-password - + + + + login.create-password + + @@ -28,13 +30,13 @@
    - + common.password lock - + login.password-again lock @@ -45,7 +47,7 @@ - diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss index 8ddc3b7982..e49395af06 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index 5c95dd8f87..f305d183c1 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { AuthService } from '@core/auth/auth.service'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { TranslateService } from '@ngx-translate/core'; import { ActivatedRoute } from '@angular/router'; @@ -44,7 +44,7 @@ export class CreatePasswordComponent extends PageComponent implements OnInit, On private route: ActivatedRoute, private authService: AuthService, private translate: TranslateService, - public fb: FormBuilder) { + public fb: UntypedFormBuilder) { super(store); } diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index ea814b4def..f5797ae574 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -1,6 +1,6 @@
    - + login.username email @@ -49,7 +49,7 @@ {{ 'user.invalid-email-format' | translate }} - + common.password diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.scss b/ui-ngx/src/app/modules/login/pages/login/login.component.scss index 5013345566..8b72d3ee3d 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ * limitations under the License. */ @import '../../../../../scss/constants'; +@import '../../../../../theme'; :host { display: flex; @@ -70,23 +71,17 @@ a.login-with-button { color: rgba(black, 0.87); + background-color: map-get($tb-dark-theme-background, raised-button); &:hover { border-bottom: 0; } - .icon{ + .icon { height: 20px; width: 20px; - vertical-align: sub; } } - - .centered ::ng-deep .mat-button-wrapper { - display: flex; - justify-content: center; - align-items: center; - } } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts index e889969e4d..2fc1c7c50d 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { AuthService } from '@core/auth/auth.service'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { Constants } from '@shared/models/constants'; import { Router } from '@angular/router'; @@ -40,7 +40,7 @@ export class LoginComponent extends PageComponent implements OnInit { constructor(protected store: Store, private authService: AuthService, - public fb: FormBuilder, + public fb: UntypedFormBuilder, private router: Router) { super(store); } diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html index c7832f6c91..53357903c8 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html @@ -1,6 +1,6 @@
    - - - login.request-password-reset - + + + + login.request-password-reset + + @@ -29,7 +31,7 @@
    - + login.email email @@ -42,7 +44,7 @@ - diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss index 0b2dfc2e59..9e64eef74d 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts index 233cf5f32d..e51df631c9 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { AuthService } from '@core/auth/auth.service'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; -import { FormBuilder, Validators } from '@angular/forms'; +import { UntypedFormBuilder, Validators } from '@angular/forms'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { TranslateService } from '@ngx-translate/core'; @@ -39,7 +39,7 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn constructor(protected store: Store, private authService: AuthService, private translate: TranslateService, - public fb: FormBuilder) { + public fb: UntypedFormBuilder) { super(store); } diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html index b0e0e6737b..bb45938716 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html @@ -1,6 +1,6 @@
    - - - login.password-reset - - -
    login.expired-password-reset-message
    -
    + + + + login.password-reset + + +
    login.expired-password-reset-message
    +
    +
    @@ -31,13 +33,13 @@
    - + login.new-password lock - + login.new-password-again lock @@ -48,7 +50,7 @@ - diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss index 41200be335..101054ac70 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index ab0928ec66..68be67a9da 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { AuthService } from '@core/auth/auth.service'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { TranslateService } from '@ngx-translate/core'; import { ActivatedRoute } from '@angular/router'; @@ -46,7 +46,7 @@ export class ResetPasswordComponent extends PageComponent implements OnInit, OnD private route: ActivatedRoute, private authService: AuthService, private translate: TranslateService, - public fb: FormBuilder) { + public fb: UntypedFormBuilder) { super(store); } diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html index f6592e686e..56565588cd 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html @@ -1,6 +1,6 @@
    + +
    + + +
    diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss index a8de9e6623..70f43f410c 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,14 @@ @import "../../../../scss/constants"; :host { - width: 100%; - height: 100%; - form, - fieldset { - height: 100%; - } + display: flex; + flex-direction: column; + max-height: 100%; + max-width: 100%; + background-color: #fff; .mat-content { overflow: hidden; - background-color: #fff; } .mat-padding { @@ -45,7 +43,7 @@ .limit-slider-value { margin-left: 16px; min-width: 25px; - max-width: 80px; + max-width: 100px; } mat-form-field input[type=number] { text-align: center; @@ -68,21 +66,16 @@ } :host ::ng-deep { - mat-radio-button { + .mat-mdc-radio-button { display: block; - margin-bottom: 16px; - .mat-radio-label { - width: 100%; + .mdc-form-field { align-items: start; - .mat-radio-label-content { - width: 100%; + > label { + padding-top: 10px; } } } - .mat-slider-horizontal .mat-slider-thumb-label { - width: 38px; - height: 38px; - top: -46px; - right: -19px; + .mat-mdc-tab-group:not(.tb-headless) { + height: 100%; } } diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index 67ee2ac72b..f4a2e8b916 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -25,17 +25,17 @@ import { Timewindow, TimewindowType } from '@shared/models/time/time.models'; -import { OverlayRef } from '@angular/cdk/overlay'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { TimeService } from '@core/services/time.service'; - -export const TIMEWINDOW_PANEL_DATA = new InjectionToken('TimewindowPanelData'); +import { isDefined } from '@core/utils'; +import { OverlayRef } from '@angular/cdk/overlay'; export interface TimewindowPanelData { historyOnly: boolean; + forAllTimeEnabled: boolean; quickIntervalOnly: boolean; timewindow: Timewindow; aggregation: boolean; @@ -43,6 +43,8 @@ export interface TimewindowPanelData { isEdit: boolean; } +export const TIMEWINDOW_PANEL_DATA = new InjectionToken('TimewindowPanelData'); + @Component({ selector: 'tb-timewindow-panel', templateUrl: './timewindow-panel.component.html', @@ -52,6 +54,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { historyOnly = false; + forAllTimeEnabled = false; + quickIntervalOnly = false; aggregation = false; @@ -62,9 +66,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { timewindow: Timewindow; - result: Timewindow; - - timewindowForm: FormGroup; + timewindowForm: UntypedFormGroup; historyTypes = HistoryWindowType; @@ -78,14 +80,17 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { aggregationTypesTranslations = aggregationTranslations; + result: Timewindow; + constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData, public overlayRef: OverlayRef, protected store: Store, - public fb: FormBuilder, + public fb: UntypedFormBuilder, private timeService: TimeService, public viewContainerRef: ViewContainerRef) { super(store); this.historyOnly = data.historyOnly; + this.forAllTimeEnabled = data.forAllTimeEnabled; this.quickIntervalOnly = data.quickIntervalOnly; this.timewindow = data.timewindow; this.aggregation = data.aggregation; @@ -101,81 +106,64 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { const hideAggInterval = this.timewindow.hideAggInterval || false; const hideTimezone = this.timewindow.hideTimezone || false; + const realtime = this.timewindow.realtime; + const history = this.timewindow.history; + const aggregation = this.timewindow.aggregation; + this.timewindowForm = this.fb.group({ - realtime: this.fb.group( - { - realtimeType: this.fb.control({ - value: this.timewindow.realtime && typeof this.timewindow.realtime.realtimeType !== 'undefined' - ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, - disabled: hideInterval - }), - timewindowMs: this.fb.control({ - value: this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' - ? this.timewindow.realtime.timewindowMs : null, - disabled: hideInterval || hideLastInterval - }), - interval: [ - this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' - ? this.timewindow.realtime.interval : null - ], - quickInterval: this.fb.control({ - value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined' - ? this.timewindow.realtime.quickInterval : null, - disabled: hideInterval || hideQuickInterval - }) - } - ), - history: this.fb.group( - { - historyType: this.fb.control({ - value: this.timewindow.history && typeof this.timewindow.history.historyType !== 'undefined' - ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL, - disabled: hideInterval - }), - timewindowMs: this.fb.control({ - value: this.timewindow.history && typeof this.timewindow.history.timewindowMs !== 'undefined' - ? this.timewindow.history.timewindowMs : null, - disabled: hideInterval - }), - interval: [ - this.timewindow.history && typeof this.timewindow.history.interval !== 'undefined' - ? this.timewindow.history.interval : null - ], - fixedTimewindow: this.fb.control({ - value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined' - ? this.timewindow.history.fixedTimewindow : null, - disabled: hideInterval - }), - quickInterval: this.fb.control({ - value: this.timewindow.history && typeof this.timewindow.history.quickInterval !== 'undefined' - ? this.timewindow.history.quickInterval : null, - disabled: hideInterval - }) - } - ), - aggregation: this.fb.group( - { - type: this.fb.control({ - value: this.timewindow.aggregation && typeof this.timewindow.aggregation.type !== 'undefined' - ? this.timewindow.aggregation.type : null, - disabled: hideAggregation - }), - limit: this.fb.control({ - value: this.timewindow.aggregation && typeof this.timewindow.aggregation.limit !== 'undefined' - ? this.checkLimit(this.timewindow.aggregation.limit) : null, - disabled: hideAggInterval - }, []) - } - ), - timezone: this.fb.control({ - value: this.timewindow.timezone !== 'undefined' - ? this.timewindow.timezone : null, - disabled: hideTimezone - }) + realtime: this.fb.group({ + realtimeType: [{ + value: isDefined(realtime?.realtimeType) ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, + disabled: hideInterval + }], + timewindowMs: [{ + value: isDefined(realtime?.timewindowMs) ? this.timewindow.realtime.timewindowMs : null, + disabled: hideInterval || hideLastInterval + }], + interval: [isDefined(realtime?.interval) ? this.timewindow.realtime.interval : null], + quickInterval: [{ + value: isDefined(realtime?.quickInterval) ? this.timewindow.realtime.quickInterval : null, + disabled: hideInterval || hideQuickInterval + }] + }), + history: this.fb.group({ + historyType: [{ + value: isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL, + disabled: hideInterval + }], + timewindowMs: [{ + value: isDefined(history?.timewindowMs) ? this.timewindow.history.timewindowMs : null, + disabled: hideInterval + }], + interval: [ isDefined(history?.interval) ? this.timewindow.history.interval : null + ], + fixedTimewindow: [{ + value: isDefined(history?.fixedTimewindow) ? this.timewindow.history.fixedTimewindow : null, + disabled: hideInterval + }], + quickInterval: [{ + value: isDefined(history?.quickInterval) ? this.timewindow.history.quickInterval : null, + disabled: hideInterval + }] + }), + aggregation: this.fb.group({ + type: [{ + value: isDefined(aggregation?.type) ? this.timewindow.aggregation.type : null, + disabled: hideAggregation + }], + limit: [{ + value: isDefined(aggregation?.limit) ? this.checkLimit(this.timewindow.aggregation.limit) : null, + disabled: hideAggInterval + }, []] + }), + timezone: [{ + value: isDefined(this.timewindow.timezone) ? this.timewindow.timezone : null, + disabled: hideTimezone + }] }); - this.updateValidators(); - this.timewindowForm.get('aggregation.type').valueChanges.subscribe(() => { - this.updateValidators(); + this.updateValidators(this.timewindowForm.get('aggregation.type').value); + this.timewindowForm.get('aggregation.type').valueChanges.subscribe((aggregationType: AggregationType) => { + this.updateValidators(aggregationType); }); } @@ -188,8 +176,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { return limit; } - private updateValidators() { - const aggType = this.timewindowForm.get('aggregation.type').value; + private updateValidators(aggType: AggregationType) { if (aggType !== AggregationType.NONE) { this.timewindowForm.get('aggregation.limit').clearValidators(); } else { diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.html b/ui-ngx/src/app/shared/components/time/timewindow.component.html index ca1a4b1f5f..e445e99c98 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.html @@ -1,6 +1,6 @@ - -
    + + +
    - - {{innerValue?.displayValue}} | {{innerValue.displayTimezoneAbbr}} + {{innerValue?.displayValue}} | {{innerValue?.displayTimezoneAbbr}} diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.scss b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.scss index 06b464db27..8470316f0b 100644 --- a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.scss +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -.mat-option.branch-option { +.mat-mdc-option.branch-option { .mat-icon, .check-placeholder { margin-right: 8px; } @@ -21,7 +21,7 @@ width: 18px; display: inline-block; } - .mat-option-text { + .mdc-list-item__primary-text { width: 100%; .default-branch { float: right; diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts index c135b0a8f7..1b87c9ca3d 100644 --- a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import { ViewChild, ViewEncapsulation } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; @@ -35,6 +35,7 @@ import { BranchInfo } from '@shared/models/vc.models'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { isNotEmptyStr } from '@core/utils'; import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { SubscriptSizing } from '@angular/material/form-field'; @Component({ selector: 'tb-branch-autocomplete', @@ -49,10 +50,13 @@ import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autoc }) export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { - branchFormGroup: FormGroup; + branchFormGroup: UntypedFormGroup; modelValue: string | null; + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + private requiredValue: boolean; get required(): boolean { @@ -91,7 +95,7 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit @ViewChild('branchAutocomplete') matAutocomplete: MatAutocomplete; @ViewChild('branchInput', { read: MatAutocompleteTrigger, static: true }) autoCompleteTrigger: MatAutocompleteTrigger; - @ViewChild('branchInput', {static: true}) branchInput: ElementRef; + @ViewChild('branchInput', {static: true}) branchInput: ElementRef; filteredBranches: Observable>; @@ -109,7 +113,7 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit constructor(private store: Store, private entitiesVersionControlService: EntitiesVersionControlService, - private fb: FormBuilder, + private fb: UntypedFormBuilder, private zone: NgZone) { this.branchFormGroup = this.fb.group({ branch: [null, []] @@ -264,9 +268,7 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit this.searchText = searchText; return this.getBranches().pipe( map(branches => { - let res = branches.filter(branch => { - return searchText ? branch.name.toUpperCase().startsWith(searchText.toUpperCase()) : true; - }); + let res = branches.filter(branch => searchText ? branch.name.toUpperCase().startsWith(searchText.toUpperCase()) : true); if (!this.selectionMode && isNotEmptyStr(searchText) && !res.find(b => b.name === searchText)) { res = [{name: searchText, default: false}, ...res]; } diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html b/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html index 0db97bdf23..a1b63e76c9 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html +++ b/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html @@ -1,6 +1,6 @@ - - + (target: any, key: string): void => { + const getter = function() { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceBooleanProperty(next); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); +}; + +export const coerceNumber = () => (target: any, key: string): void => { + const getter = function(): number { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceNumberProperty(next); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); +}; + +export const coerceCssPixelValue = () => (target: any, key: string): void => { + const getter = function(): string { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceCssPixelValueAngular(next); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); +}; + +export const coerceArray = () => (target: any, key: string): void => { + const getter = function(): any[] { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceArrayAngular(next); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); +}; + +export const coerceStringArray = (separator?: string | RegExp) => (target: any, key: string): void => { + const getter = function(): string[] { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceStringArrayAngular(next, separator); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); +}; diff --git a/ui-ngx/src/app/shared/decorators/enumerable.ts b/ui-ngx/src/app/shared/decorators/enumerable.ts index 0c66a3bc0d..1e89ff0ca3 100644 --- a/ui-ngx/src/app/shared/decorators/enumerable.ts +++ b/ui-ngx/src/app/shared/decorators/enumerable.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/decorators/public-api.ts b/ui-ngx/src/app/shared/decorators/public-api.ts new file mode 100644 index 0000000000..a150b7eb7f --- /dev/null +++ b/ui-ngx/src/app/shared/decorators/public-api.ts @@ -0,0 +1,19 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './coercion'; +export * from './enumerable'; +export * from './tb-inject'; diff --git a/ui-ngx/src/app/shared/decorators/tb-inject.ts b/ui-ngx/src/app/shared/decorators/tb-inject.ts index 2c5150600f..6a082b9b63 100644 --- a/ui-ngx/src/app/shared/decorators/tb-inject.ts +++ b/ui-ngx/src/app/shared/decorators/tb-inject.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/layout/layout.directives.ts b/ui-ngx/src/app/shared/layout/layout.directives.ts new file mode 100644 index 0000000000..3a450cbae6 --- /dev/null +++ b/ui-ngx/src/app/shared/layout/layout.directives.ts @@ -0,0 +1,85 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Directive } from '@angular/core'; +import { BREAKPOINT, LayoutAlignDirective, LayoutDirective, LayoutGapDirective, ShowHideDirective } from '@angular/flex-layout'; + +const TB_BREAKPOINTS = [ + { + alias: 'md-lg', + mediaQuery: 'screen and (min-width: 960px) and (max-width: 1819px)', + priority: 750 + }, + { + alias: 'gt-md-lg', + mediaQuery: 'screen and (min-width: 1820px)', + priority: -600 + } +]; + +export const TbBreakPointsProvider = { + provide: BREAKPOINT, + useValue: TB_BREAKPOINTS, + multi: true +}; + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayout.md-lg]', inputs: ['fxLayout.md-lg']}) +export class MdLgLayoutDirective extends LayoutDirective { + protected inputs = ['fxLayout.md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayoutAlign.md-lg]', inputs: ['fxLayoutAlign.md-lg']}) +export class MdLgLayoutAlignDirective extends LayoutAlignDirective { + protected inputs = ['fxLayoutAlign.md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayoutGap.md-lg]', inputs: ['fxLayoutGap.md-lg']}) +export class MdLgLayoutGapDirective extends LayoutGapDirective { + protected inputs = ['fxLayoutGap.md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxHide.md-lg]', inputs: ['fxHide.md-lg']}) +export class MdLgShowHideDirective extends ShowHideDirective { + protected inputs = ['fxHide.md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayout.gt-md-lg]', inputs: ['fxLayout.gt-md-lg']}) +export class GtMdLgLayoutDirective extends LayoutDirective { + protected inputs = ['fxLayout.gt-md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayoutAlign.gt-md-lg]', inputs: ['fxLayoutAlign.gt-md-lg']}) +export class GtMdLgLayoutAlignDirective extends LayoutAlignDirective { + protected inputs = ['fxLayoutAlign.gt-md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayoutGap.gt-md-lg]', inputs: ['fxLayoutGap.gt-md-lg']}) +export class GtMdLgLayoutGapDirective extends LayoutGapDirective { + protected inputs = ['fxLayoutGap.gt-md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxHide.gt-md-lg]', inputs: ['fxHide.gt-md-lg']}) +export class GtMdLgShowHideDirective extends ShowHideDirective { + protected inputs = ['fxHide.gt-md-lg']; +} diff --git a/ui-ngx/src/app/shared/models/ace/ace.models.ts b/ui-ngx/src/app/shared/models/ace/ace.models.ts index 9777935922..f387d7998e 100644 --- a/ui-ngx/src/app/shared/models/ace/ace.models.ts +++ b/ui-ngx/src/app/shared/models/ace/ace.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ function loadAceDependencies(): Observable { aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/javascript'))); + aceObservables.push(from(import('./tbel/mode-tbel'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html'))); diff --git a/ui-ngx/src/app/shared/models/ace/completion.models.ts b/ui-ngx/src/app/shared/models/ace/completion.models.ts index 0941fcbe39..c2b8013d63 100644 --- a/ui-ngx/src/app/shared/models/ace/completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/completion.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/ace/service-completion.models.ts b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts index bcad0a000c..c96dc0d98f 100644 --- a/ui-ngx/src/app/shared/models/ace/service-completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/ace/tbel/mode-tbel.js b/ui-ngx/src/app/shared/models/ace/tbel/mode-tbel.js new file mode 100644 index 0000000000..a92de5a83e --- /dev/null +++ b/ui-ngx/src/app/shared/models/ace/tbel/mode-tbel.js @@ -0,0 +1,812 @@ +/* + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +ace.define("ace/mode/doc_comment_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var DocCommentHighlightRules = function() { + this.$rules = { + "start" : [ { + token : "comment.doc.tag", + regex : "@[\\w\\d_]+" // TODO: fix email addresses + }, + DocCommentHighlightRules.getTagRule(), + { + defaultToken : "comment.doc", + caseInsensitive: true + }] + }; +}; + +oop.inherits(DocCommentHighlightRules, TextHighlightRules); + +DocCommentHighlightRules.getTagRule = function(start) { + return { + token : "comment.doc.tag.storage.type", + regex : "\\b(?:TODO|FIXME|XXX|HACK)\\b" + }; +}; + +DocCommentHighlightRules.getStartRule = function(start) { + return { + token : "comment.doc", // doc comment + regex : "\\/\\*(?=\\*)", + next : start + }; +}; + +DocCommentHighlightRules.getEndRule = function (start) { + return { + token : "comment.doc", // closing comment + regex : "\\*\\/", + next : start + }; +}; + + +exports.DocCommentHighlightRules = DocCommentHighlightRules; + +}); + +ace.define("ace/mode/javascript_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var DocCommentHighlightRules = require("./doc_comment_highlight_rules").DocCommentHighlightRules; +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; +var identifierRe = "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*"; + +var JavaScriptHighlightRules = function(options) { + var keywordMapper = this.createKeywordMapper({ + "variable.language": + "Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|" + // Constructors + "Namespace|QName|XML|XMLList|" + // E4X + "ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|" + + "Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|" + + "Error|EvalError|InternalError|RangeError|ReferenceError|StopIteration|" + // Errors + "SyntaxError|TypeError|URIError|" + + "decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|" + // Non-constructor functions + "isNaN|parseFloat|parseInt|" + + "JSON|Math|" + // Other + "this|arguments|prototype|window|document" , // Pseudo + "keyword": + "const|yield|import|get|set|async|await|" + + "break|case|catch|continue|default|delete|do|else|finally|for|function|" + + "if|in|of|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|debugger|" + + "__parent__|__count__|escape|unescape|with|__proto__|" + + "class|enum|extends|super|export|implements|private|public|interface|package|protected|static", + "storage.type": + "const|let|var|function", + "constant.language": + "null|Infinity|NaN|undefined", + "support.function": + "alert", + "constant.language.boolean": "true|false" + }, "identifier"); + var kwBeforeRe = "case|do|else|finally|in|instanceof|return|throw|try|typeof|yield|void"; + + var escapedRe = "\\\\(?:x[0-9a-fA-F]{2}|" + // hex + "u[0-9a-fA-F]{4}|" + // unicode + "u{[0-9a-fA-F]{1,6}}|" + // es6 unicode + "[0-2][0-7]{0,2}|" + // oct + "3[0-7][0-7]?|" + // oct + "[4-7][0-7]?|" + //oct + ".)"; + + this.$rules = { + "no_regex" : [ + DocCommentHighlightRules.getStartRule("doc-start"), + comments("no_regex"), + { + token : "string", + regex : "'(?=.)", + next : "qstring" + }, { + token : "string", + regex : '"(?=.)', + next : "qqstring" + }, { + token : "constant.numeric", // hexadecimal, octal and binary + regex : /0(?:[xX][0-9a-fA-F]+|[oO][0-7]+|[bB][01]+)\b/ + }, { + token : "constant.numeric", // decimal integers and floats + regex : /(?:\d\d*(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+\b)?/ + }, { + token : [ + "storage.type", "punctuation.operator", "support.function", + "punctuation.operator", "entity.name.function", "text","keyword.operator" + ], + regex : "(" + identifierRe + ")(\\.)(prototype)(\\.)(" + identifierRe +")(\\s*)(=)", + next: "function_arguments" + }, { + token : [ + "storage.type", "punctuation.operator", "entity.name.function", "text", + "keyword.operator", "text", "storage.type", "text", "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { + token : [ + "entity.name.function", "text", "keyword.operator", "text", "storage.type", + "text", "paren.lparen" + ], + regex : "(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { + token : [ + "storage.type", "punctuation.operator", "entity.name.function", "text", + "keyword.operator", "text", + "storage.type", "text", "entity.name.function", "text", "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s+)(\\w+)(\\s*)(\\()", + next: "function_arguments" + }, { + token : [ + "storage.type", "text", "entity.name.function", "text", "paren.lparen" + ], + regex : "(function)(\\s+)(" + identifierRe + ")(\\s*)(\\()", + next: "function_arguments" + }, { + token : [ + "entity.name.function", "text", "punctuation.operator", + "text", "storage.type", "text", "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\s*)(:)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { + token : [ + "text", "text", "storage.type", "text", "paren.lparen" + ], + regex : "(:)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { + token : "keyword", + regex : "from(?=\\s*('|\"))" + }, { + token : "keyword", + regex : "(?:" + kwBeforeRe + ")\\b", + next : "start" + }, { + token : ["support.constant"], + regex : /that\b/ + }, { + token : ["storage.type", "punctuation.operator", "support.function.firebug"], + regex : /(console)(\.)(warn|info|log|error|time|trace|timeEnd|assert)\b/ + }, { + token : keywordMapper, + regex : identifierRe + }, { + token : "punctuation.operator", + regex : /[.](?![.])/, + next : "property" + }, { + token : "storage.type", + regex : /=>/, + next : "start" + }, { + token : "keyword.operator", + regex : /--|\+\+|\.{3}|===|==|=|!=|!==|<+=?|>+=?|!|&&|\|\||\?:|[!$%&*+\-~\/^]=?/, + next : "start" + }, { + token : "punctuation.operator", + regex : /[?:,;.]/, + next : "start" + }, { + token : "paren.lparen", + regex : /[\[({]/, + next : "start" + }, { + token : "paren.rparen", + regex : /[\])}]/ + }, { + token: "comment", + regex: /^#!.*$/ + } + ], + property: [{ + token : "text", + regex : "\\s+" + }, { + token : [ + "storage.type", "punctuation.operator", "entity.name.function", "text", + "keyword.operator", "text", + "storage.type", "text", "entity.name.function", "text", "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(?:(\\s+)(\\w+))?(\\s*)(\\()", + next: "function_arguments" + }, { + token : "punctuation.operator", + regex : /[.](?![.])/ + }, { + token : "support.function", + regex : /(s(?:h(?:ift|ow(?:Mod(?:elessDialog|alDialog)|Help))|croll(?:X|By(?:Pages|Lines)?|Y|To)?|t(?:op|rike)|i(?:n|zeToContent|debar|gnText)|ort|u(?:p|b(?:str(?:ing)?)?)|pli(?:ce|t)|e(?:nd|t(?:Re(?:sizable|questHeader)|M(?:i(?:nutes|lliseconds)|onth)|Seconds|Ho(?:tKeys|urs)|Year|Cursor|Time(?:out)?|Interval|ZOptions|Date|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Date|FullYear)|FullYear|Active)|arch)|qrt|lice|avePreferences|mall)|h(?:ome|andleEvent)|navigate|c(?:har(?:CodeAt|At)|o(?:s|n(?:cat|textual|firm)|mpile)|eil|lear(?:Timeout|Interval)?|a(?:ptureEvents|ll)|reate(?:StyleSheet|Popup|EventObject))|t(?:o(?:GMTString|S(?:tring|ource)|U(?:TCString|pperCase)|Lo(?:caleString|werCase))|est|a(?:n|int(?:Enabled)?))|i(?:s(?:NaN|Finite)|ndexOf|talics)|d(?:isableExternalCapture|ump|etachEvent)|u(?:n(?:shift|taint|escape|watch)|pdateCommands)|j(?:oin|avaEnabled)|p(?:o(?:p|w)|ush|lugins.refresh|a(?:ddings|rse(?:Int|Float)?)|r(?:int|ompt|eference))|e(?:scape|nableExternalCapture|val|lementFromPoint|x(?:p|ec(?:Script|Command)?))|valueOf|UTC|queryCommand(?:State|Indeterm|Enabled|Value)|f(?:i(?:nd|le(?:ModifiedDate|Size|CreatedDate|UpdatedDate)|xed)|o(?:nt(?:size|color)|rward)|loor|romCharCode)|watch|l(?:ink|o(?:ad|g)|astIndexOf)|a(?:sin|nchor|cos|t(?:tachEvent|ob|an(?:2)?)|pply|lert|b(?:s|ort))|r(?:ou(?:nd|teEvents)|e(?:size(?:By|To)|calc|turnValue|place|verse|l(?:oad|ease(?:Capture|Events)))|andom)|g(?:o|et(?:ResponseHeader|M(?:i(?:nutes|lliseconds)|onth)|Se(?:conds|lection)|Hours|Year|Time(?:zoneOffset)?|Da(?:y|te)|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Da(?:y|te)|FullYear)|FullYear|A(?:ttention|llResponseHeaders)))|m(?:in|ove(?:B(?:y|elow)|To(?:Absolute)?|Above)|ergeAttributes|a(?:tch|rgins|x))|b(?:toa|ig|o(?:ld|rderWidths)|link|ack))\b(?=\()/ + }, { + token : "support.function.dom", + regex : /(s(?:ub(?:stringData|mit)|plitText|e(?:t(?:NamedItem|Attribute(?:Node)?)|lect))|has(?:ChildNodes|Feature)|namedItem|c(?:l(?:ick|o(?:se|neNode))|reate(?:C(?:omment|DATASection|aption)|T(?:Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(?:ntityReference|lement)|Attribute))|tabIndex|i(?:nsert(?:Row|Before|Cell|Data)|tem)|open|delete(?:Row|C(?:ell|aption)|T(?:Head|Foot)|Data)|focus|write(?:ln)?|a(?:dd|ppend(?:Child|Data))|re(?:set|place(?:Child|Data)|move(?:NamedItem|Child|Attribute(?:Node)?)?)|get(?:NamedItem|Element(?:sBy(?:Name|TagName|ClassName)|ById)|Attribute(?:Node)?)|blur)\b(?=\()/ + }, { + token : "support.constant", + regex : /(s(?:ystemLanguage|cr(?:ipts|ollbars|een(?:X|Y|Top|Left))|t(?:yle(?:Sheets)?|atus(?:Text|bar)?)|ibling(?:Below|Above)|ource|uffixes|e(?:curity(?:Policy)?|l(?:ection|f)))|h(?:istory|ost(?:name)?|as(?:h|Focus))|y|X(?:MLDocument|SLDocument)|n(?:ext|ame(?:space(?:s|URI)|Prop))|M(?:IN_VALUE|AX_VALUE)|c(?:haracterSet|o(?:n(?:structor|trollers)|okieEnabled|lorDepth|mp(?:onents|lete))|urrent|puClass|l(?:i(?:p(?:boardData)?|entInformation)|osed|asses)|alle(?:e|r)|rypto)|t(?:o(?:olbar|p)|ext(?:Transform|Indent|Decoration|Align)|ags)|SQRT(?:1_2|2)|i(?:n(?:ner(?:Height|Width)|put)|ds|gnoreCase)|zIndex|o(?:scpu|n(?:readystatechange|Line)|uter(?:Height|Width)|p(?:sProfile|ener)|ffscreenBuffering)|NEGATIVE_INFINITY|d(?:i(?:splay|alog(?:Height|Top|Width|Left|Arguments)|rectories)|e(?:scription|fault(?:Status|Ch(?:ecked|arset)|View)))|u(?:ser(?:Profile|Language|Agent)|n(?:iqueID|defined)|pdateInterval)|_content|p(?:ixelDepth|ort|ersonalbar|kcs11|l(?:ugins|atform)|a(?:thname|dding(?:Right|Bottom|Top|Left)|rent(?:Window|Layer)?|ge(?:X(?:Offset)?|Y(?:Offset)?))|r(?:o(?:to(?:col|type)|duct(?:Sub)?|mpter)|e(?:vious|fix)))|e(?:n(?:coding|abledPlugin)|x(?:ternal|pando)|mbeds)|v(?:isibility|endor(?:Sub)?|Linkcolor)|URLUnencoded|P(?:I|OSITIVE_INFINITY)|f(?:ilename|o(?:nt(?:Size|Family|Weight)|rmName)|rame(?:s|Element)|gColor)|E|whiteSpace|l(?:i(?:stStyleType|n(?:eHeight|kColor))|o(?:ca(?:tion(?:bar)?|lName)|wsrc)|e(?:ngth|ft(?:Context)?)|a(?:st(?:M(?:odified|atch)|Index|Paren)|yer(?:s|X)|nguage))|a(?:pp(?:MinorVersion|Name|Co(?:deName|re)|Version)|vail(?:Height|Top|Width|Left)|ll|r(?:ity|guments)|Linkcolor|bove)|r(?:ight(?:Context)?|e(?:sponse(?:XML|Text)|adyState))|global|x|m(?:imeTypes|ultiline|enubar|argin(?:Right|Bottom|Top|Left))|L(?:N(?:10|2)|OG(?:10E|2E))|b(?:o(?:ttom|rder(?:Width|RightWidth|BottomWidth|Style|Color|TopWidth|LeftWidth))|ufferDepth|elow|ackground(?:Color|Image)))\b/ + }, { + token : "identifier", + regex : identifierRe + }, { + regex: "", + token: "empty", + next: "no_regex" + } + ], + "start": [ + DocCommentHighlightRules.getStartRule("doc-start"), + comments("start"), + { + token: "string.regexp", + regex: "\\/", + next: "regex" + }, { + token : "text", + regex : "\\s+|^$", + next : "start" + }, { + token: "empty", + regex: "", + next: "no_regex" + } + ], + "regex": [ + { + token: "regexp.keyword.operator", + regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)" + }, { + token: "string.regexp", + regex: "/[sxngimy]*", + next: "no_regex" + }, { + token : "invalid", + regex: /\{\d+\b,?\d*\}[+*]|[+*$^?][+*]|[$^][?]|\?{3,}/ + }, { + token : "constant.language.escape", + regex: /\(\?[:=!]|\)|\{\d+\b,?\d*\}|[+*]\?|[()$^+*?.]/ + }, { + token : "constant.language.delimiter", + regex: /\|/ + }, { + token: "constant.language.escape", + regex: /\[\^?/, + next: "regex_character_class" + }, { + token: "empty", + regex: "$", + next: "no_regex" + }, { + defaultToken: "string.regexp" + } + ], + "regex_character_class": [ + { + token: "regexp.charclass.keyword.operator", + regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)" + }, { + token: "constant.language.escape", + regex: "]", + next: "regex" + }, { + token: "constant.language.escape", + regex: "-" + }, { + token: "empty", + regex: "$", + next: "no_regex" + }, { + defaultToken: "string.regexp.charachterclass" + } + ], + "function_arguments": [ + { + token: "variable.parameter", + regex: identifierRe + }, { + token: "punctuation.operator", + regex: "[, ]+" + }, { + token: "punctuation.operator", + regex: "$" + }, { + token: "empty", + regex: "", + next: "no_regex" + } + ], + "qqstring" : [ + { + token : "constant.language.escape", + regex : escapedRe + }, { + token : "string", + regex : "\\\\$", + consumeLineEnd : true + }, { + token : "string", + regex : '"|$', + next : "no_regex" + }, { + defaultToken: "string" + } + ], + "qstring" : [ + { + token : "constant.language.escape", + regex : escapedRe + }, { + token : "string", + regex : "\\\\$", + consumeLineEnd : true + }, { + token : "string", + regex : "'|$", + next : "no_regex" + }, { + defaultToken: "string" + } + ] + }; + + + if (!options || !options.noES6) { + this.$rules.no_regex.unshift({ + regex: "[{}]", onMatch: function(val, state, stack) { + this.next = val == "{" ? this.nextState : ""; + if (val == "{" && stack.length) { + stack.unshift("start", state); + } + else if (val == "}" && stack.length) { + stack.shift(); + this.next = stack.shift(); + if (this.next.indexOf("string") != -1 || this.next.indexOf("jsx") != -1) + return "paren.quasi.end"; + } + return val == "{" ? "paren.lparen" : "paren.rparen"; + }, + nextState: "start" + }, { + token : "string.quasi.start", + regex : /`/, + push : [{ + token : "constant.language.escape", + regex : escapedRe + }, { + token : "paren.quasi.start", + regex : /\${/, + push : "start" + }, { + token : "string.quasi.end", + regex : /`/, + next : "pop" + }, { + defaultToken: "string.quasi" + }] + }); + + if (!options || options.jsx != false) + JSX.call(this); + } + + this.embedRules(DocCommentHighlightRules, "doc-", + [ DocCommentHighlightRules.getEndRule("no_regex") ]); + + this.normalizeRules(); +}; + +oop.inherits(JavaScriptHighlightRules, TextHighlightRules); + +function JSX() { + var tagRegex = identifierRe.replace("\\d", "\\d\\-"); + var jsxTag = { + onMatch : function(val, state, stack) { + var offset = val.charAt(1) == "/" ? 2 : 1; + if (offset == 1) { + if (state != this.nextState) + stack.unshift(this.next, this.nextState, 0); + else + stack.unshift(this.next); + stack[2]++; + } else if (offset == 2) { + if (state == this.nextState) { + stack[1]--; + if (!stack[1] || stack[1] < 0) { + stack.shift(); + stack.shift(); + } + } + } + return [{ + type: "meta.tag.punctuation." + (offset == 1 ? "" : "end-") + "tag-open.xml", + value: val.slice(0, offset) + }, { + type: "meta.tag.tag-name.xml", + value: val.substr(offset) + }]; + }, + regex : "", + onMatch : function(value, currentState, stack) { + if (currentState == stack[0]) + stack.shift(); + if (value.length == 2) { + if (stack[0] == this.nextState) + stack[1]--; + if (!stack[1] || stack[1] < 0) { + stack.splice(0, 2); + } + } + this.next = stack[0] || "start"; + return [{type: this.token, value: value}]; + }, + nextState: "jsx" + }, + jsxJsRule, + comments("jsxAttributes"), + { + token : "entity.other.attribute-name.xml", + regex : tagRegex + }, { + token : "keyword.operator.attribute-equals.xml", + regex : "=" + }, { + token : "text.tag-whitespace.xml", + regex : "\\s+" + }, { + token : "string.attribute-value.xml", + regex : "'", + stateName : "jsx_attr_q", + push : [ + {token : "string.attribute-value.xml", regex: "'", next: "pop"}, + {include : "reference"}, + {defaultToken : "string.attribute-value.xml"} + ] + }, { + token : "string.attribute-value.xml", + regex : '"', + stateName : "jsx_attr_qq", + push : [ + {token : "string.attribute-value.xml", regex: '"', next: "pop"}, + {include : "reference"}, + {defaultToken : "string.attribute-value.xml"} + ] + }, + jsxTag + ]; + this.$rules.reference = [{ + token : "constant.language.escape.reference.xml", + regex : "(?:&#[0-9]+;)|(?:&#x[0-9a-fA-F]+;)|(?:&[a-zA-Z0-9_:\\.-]+;)" + }]; +} + +function comments(next) { + return [ + { + token : "comment", // multi line comment + regex : /\/\*/, + next: [ + DocCommentHighlightRules.getTagRule(), + {token : "comment", regex : "\\*\\/", next : next || "pop"}, + {defaultToken : "comment", caseInsensitive: true} + ] + }, { + token : "comment", + regex : "\\/\\/", + next: [ + DocCommentHighlightRules.getTagRule(), + {token : "comment", regex : "$|^", next : next || "pop"}, + {defaultToken : "comment", caseInsensitive: true} + ] + } + ]; +} +exports.JavaScriptHighlightRules = JavaScriptHighlightRules; +}); + +ace.define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"], function(require, exports, module) { +"use strict"; + +var Range = require("../range").Range; + +var MatchingBraceOutdent = function() {}; + +(function() { + + this.checkOutdent = function(line, input) { + if (! /^\s+$/.test(line)) + return false; + + return /^\s*\}/.test(input); + }; + + this.autoOutdent = function(doc, row) { + var line = doc.getLine(row); + var match = line.match(/^(\s*\})/); + + if (!match) return 0; + + var column = match[1].length; + var openBracePos = doc.findMatchingBracket({row: row, column: column}); + + if (!openBracePos || openBracePos.row == row) return 0; + + var indent = this.$getIndent(doc.getLine(openBracePos.row)); + doc.replace(new Range(row, 0, row, column-1), indent); + }; + + this.$getIndent = function(line) { + return line.match(/^\s*/)[0]; + }; + +}).call(MatchingBraceOutdent.prototype); + +exports.MatchingBraceOutdent = MatchingBraceOutdent; +}); + +ace.define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Range = require("../../range").Range; +var BaseFoldMode = require("./fold_mode").FoldMode; + +var FoldMode = exports.FoldMode = function(commentRegex) { + if (commentRegex) { + this.foldingStartMarker = new RegExp( + this.foldingStartMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.start) + ); + this.foldingStopMarker = new RegExp( + this.foldingStopMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.end) + ); + } +}; +oop.inherits(FoldMode, BaseFoldMode); + +(function() { + + this.foldingStartMarker = /([\{\[\(])[^\}\]\)]*$|^\s*(\/\*)/; + this.foldingStopMarker = /^[^\[\{\(]*([\}\]\)])|^[\s\*]*(\*\/)/; + this.singleLineBlockCommentRe= /^\s*(\/\*).*\*\/\s*$/; + this.tripleStarBlockCommentRe = /^\s*(\/\*\*\*).*\*\/\s*$/; + this.startRegionRe = /^\s*(\/\*|\/\/)#?region\b/; + this._getFoldWidgetBase = this.getFoldWidget; + this.getFoldWidget = function(session, foldStyle, row) { + var line = session.getLine(row); + + if (this.singleLineBlockCommentRe.test(line)) { + if (!this.startRegionRe.test(line) && !this.tripleStarBlockCommentRe.test(line)) + return ""; + } + + var fw = this._getFoldWidgetBase(session, foldStyle, row); + + if (!fw && this.startRegionRe.test(line)) + return "start"; // lineCommentRegionStart + + return fw; + }; + + this.getFoldWidgetRange = function(session, foldStyle, row, forceMultiline) { + var line = session.getLine(row); + + if (this.startRegionRe.test(line)) + return this.getCommentRegionBlock(session, line, row); + + var match = line.match(this.foldingStartMarker); + if (match) { + var i = match.index; + + if (match[1]) + return this.openingBracketBlock(session, match[1], row, i); + + var range = session.getCommentFoldRange(row, i + match[0].length, 1); + + if (range && !range.isMultiLine()) { + if (forceMultiline) { + range = this.getSectionRange(session, row); + } else if (foldStyle != "all") + range = null; + } + + return range; + } + + if (foldStyle === "markbegin") + return; + + var match = line.match(this.foldingStopMarker); + if (match) { + var i = match.index + match[0].length; + + if (match[1]) + return this.closingBracketBlock(session, match[1], row, i); + + return session.getCommentFoldRange(row, i, -1); + } + }; + + this.getSectionRange = function(session, row) { + var line = session.getLine(row); + var startIndent = line.search(/\S/); + var startRow = row; + var startColumn = line.length; + row = row + 1; + var endRow = row; + var maxRow = session.getLength(); + while (++row < maxRow) { + line = session.getLine(row); + var indent = line.search(/\S/); + if (indent === -1) + continue; + if (startIndent > indent) + break; + var subRange = this.getFoldWidgetRange(session, "all", row); + + if (subRange) { + if (subRange.start.row <= startRow) { + break; + } else if (subRange.isMultiLine()) { + row = subRange.end.row; + } else if (startIndent == indent) { + break; + } + } + endRow = row; + } + + return new Range(startRow, startColumn, endRow, session.getLine(endRow).length); + }; + this.getCommentRegionBlock = function(session, line, row) { + var startColumn = line.search(/\s*$/); + var maxRow = session.getLength(); + var startRow = row; + + var re = /^\s*(?:\/\*|\/\/|--)#?(end)?region\b/; + var depth = 1; + while (++row < maxRow) { + line = session.getLine(row); + var m = re.exec(line); + if (!m) continue; + if (m[1]) depth--; + else depth++; + + if (!depth) break; + } + + var endRow = row; + if (endRow > startRow) { + return new Range(startRow, startColumn, endRow, line.length); + } + }; + +}).call(FoldMode.prototype); + +}); + +ace.define("ace/mode/tbel",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/worker/worker_client","ace/mode/behaviour/cstyle","ace/mode/folding/cstyle"], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var JavaScriptHighlightRules = require("./javascript_highlight_rules").JavaScriptHighlightRules; +var MatchingBraceOutdent = require("./matching_brace_outdent").MatchingBraceOutdent; +var WorkerClient = require("../worker/worker_client").WorkerClient; +var CstyleBehaviour = require("./behaviour/cstyle").CstyleBehaviour; +var CStyleFoldMode = require("./folding/cstyle").FoldMode; + +var Mode = function() { + this.HighlightRules = JavaScriptHighlightRules; + + this.$outdent = new MatchingBraceOutdent(); + this.$behaviour = new CstyleBehaviour(); + this.foldingRules = new CStyleFoldMode(); +}; +oop.inherits(Mode, TextMode); + +(function() { + + this.lineCommentStart = "//"; + this.blockComment = {start: "/*", end: "*/"}; + this.$quotes = {'"': '"', "'": "'", "`": "`"}; + + this.getNextLineIndent = function(state, line, tab) { + var indent = this.$getIndent(line); + + var tokenizedLine = this.getTokenizer().getLineTokens(line, state); + var tokens = tokenizedLine.tokens; + var endState = tokenizedLine.state; + + if (tokens.length && tokens[tokens.length-1].type == "comment") { + return indent; + } + + if (state == "start" || state == "no_regex") { + var match = line.match(/^.*(?:\bcase\b.*:|[\{\(\[])\s*$/); + if (match) { + indent += tab; + } + } else if (state == "doc-start") { + if (endState == "start" || endState == "no_regex") { + return ""; + } + var match = line.match(/^\s*(\/?)\*/); + if (match) { + if (match[1]) { + indent += " "; + } + indent += "* "; + } + } + + return indent; + }; + + this.checkOutdent = function(state, line, input) { + return this.$outdent.checkOutdent(line, input); + }; + + this.autoOutdent = function(state, doc, row) { + this.$outdent.autoOutdent(doc, row); + }; + + this.createWorker = function(session) { + var worker = new WorkerClient(["ace"], "ace/mode/tbel_worker", "TbelWorker"); + worker.attachToDocument(session.getDocument()); + + worker.on("annotate", function(results) { + session.setAnnotations(results.data); + }); + + worker.on("terminate", function() { + session.clearAnnotations(); + }); + + return worker; + }; + + this.$id = "ace/mode/tbel"; + this.snippetFileId = "ace/snippets/javascript"; +}).call(Mode.prototype); + +exports.Mode = Mode; +}); (function() { + ace.require(["ace/mode/tbel"], function(m) { + if (typeof module == "object" && typeof exports == "object" && module) { + module.exports = m; + } + }); + })(); diff --git a/ui-ngx/src/app/shared/models/ace/tbel/worker-tbel.js b/ui-ngx/src/app/shared/models/ace/tbel/worker-tbel.js new file mode 100644 index 0000000000..bc22e806c3 --- /dev/null +++ b/ui-ngx/src/app/shared/models/ace/tbel/worker-tbel.js @@ -0,0 +1,15151 @@ +/* + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +"no use strict"; +!(function(window) { +if (typeof window.window != "undefined" && window.document) + return; +if (window.require && window.define) + return; + +if (!window.console) { + window.console = function() { + var msgs = Array.prototype.slice.call(arguments, 0); + postMessage({type: "log", data: msgs}); + }; + window.console.error = + window.console.warn = + window.console.log = + window.console.trace = window.console; +} +window.window = window; +window.ace = window; + +window.onerror = function(message, file, line, col, err) { + postMessage({type: "error", data: { + message: message, + data: err.data, + file: file, + line: line, + col: col, + stack: err.stack + }}); +}; + +window.normalizeModule = function(parentId, moduleName) { + // normalize plugin requires + if (moduleName.indexOf("!") !== -1) { + var chunks = moduleName.split("!"); + return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1]); + } + // normalize relative requires + if (moduleName.charAt(0) == ".") { + var base = parentId.split("/").slice(0, -1).join("/"); + moduleName = (base ? base + "/" : "") + moduleName; + + while (moduleName.indexOf(".") !== -1 && previous != moduleName) { + var previous = moduleName; + moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, ""); + } + } + + return moduleName; +}; + +window.require = function require(parentId, id) { + if (!id) { + id = parentId; + parentId = null; + } + if (!id.charAt) + throw new Error("worker.js require() accepts only (parentId, id) as arguments"); + + id = window.normalizeModule(parentId, id); + + var module = window.require.modules[id]; + if (module) { + if (!module.initialized) { + module.initialized = true; + module.exports = module.factory().exports; + } + return module.exports; + } + + if (!window.require.tlns) + return console.log("unable to load " + id); + + var path = resolveModuleId(id, window.require.tlns); + if (path.slice(-3) != ".js") path += ".js"; + + window.require.id = id; + window.require.modules[id] = {}; // prevent infinite loop on broken modules + importScripts(path); + return window.require(parentId, id); +}; +function resolveModuleId(id, paths) { + var testPath = id, tail = ""; + while (testPath) { + var alias = paths[testPath]; + if (typeof alias == "string") { + return alias + tail; + } else if (alias) { + return alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name); + } else if (alias === false) { + return ""; + } + var i = testPath.lastIndexOf("/"); + if (i === -1) break; + tail = testPath.substr(i) + tail; + testPath = testPath.slice(0, i); + } + return id; +} +window.require.modules = {}; +window.require.tlns = {}; + +window.define = function(id, deps, factory) { + if (arguments.length == 2) { + factory = deps; + if (typeof id != "string") { + deps = id; + id = window.require.id; + } + } else if (arguments.length == 1) { + factory = id; + deps = []; + id = window.require.id; + } + + if (typeof factory != "function") { + window.require.modules[id] = { + exports: factory, + initialized: true + }; + return; + } + + if (!deps.length) + // If there is no dependencies, we inject "require", "exports" and + // "module" as dependencies, to provide CommonJS compatibility. + deps = ["require", "exports", "module"]; + + var req = function(childId) { + return window.require(id, childId); + }; + + window.require.modules[id] = { + exports: {}, + factory: function() { + var module = this; + var returnExports = factory.apply(this, deps.slice(0, factory.length).map(function(dep) { + switch (dep) { + // Because "require", "exports" and "module" aren't actual + // dependencies, we must handle them seperately. + case "require": return req; + case "exports": return module.exports; + case "module": return module; + // But for all other dependencies, we can just go ahead and + // require them. + default: return req(dep); + } + })); + if (returnExports) + module.exports = returnExports; + return module; + } + }; +}; +window.define.amd = {}; +require.tlns = {}; +window.initBaseUrls = function initBaseUrls(topLevelNamespaces) { + for (var i in topLevelNamespaces) + require.tlns[i] = topLevelNamespaces[i]; +}; + +window.initSender = function initSender() { + + var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter; + var oop = window.require("ace/lib/oop"); + + var Sender = function() {}; + + (function() { + + oop.implement(this, EventEmitter); + + this.callback = function(data, callbackId) { + postMessage({ + type: "call", + id: callbackId, + data: data + }); + }; + + this.emit = function(name, data) { + postMessage({ + type: "event", + name: name, + data: data + }); + }; + + }).call(Sender.prototype); + + return new Sender(); +}; + +var main = window.main = null; +var sender = window.sender = null; + +window.onmessage = function(e) { + var msg = e.data; + if (msg.event && sender) { + sender._signal(msg.event, msg.data); + } + else if (msg.command) { + if (main[msg.command]) + main[msg.command].apply(main, msg.args); + else if (window[msg.command]) + window[msg.command].apply(window, msg.args); + else + throw new Error("Unknown command:" + msg.command); + } + else if (msg.init) { + window.initBaseUrls(msg.tlns); + sender = window.sender = window.initSender(); + var clazz = require(msg.module)[msg.classname]; + main = window.main = new clazz(sender); + } +}; +})(this); + +ace.define("ace/lib/oop",[], function(require, exports, module) { +"use strict"; + +exports.inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +exports.mixin = function(obj, mixin) { + for (var key in mixin) { + obj[key] = mixin[key]; + } + return obj; +}; + +exports.implement = function(proto, mixin) { + exports.mixin(proto, mixin); +}; + +}); + +ace.define("ace/range",[], function(require, exports, module) { +"use strict"; +var comparePoints = function(p1, p2) { + return p1.row - p2.row || p1.column - p2.column; +}; +var Range = function(startRow, startColumn, endRow, endColumn) { + this.start = { + row: startRow, + column: startColumn + }; + + this.end = { + row: endRow, + column: endColumn + }; +}; + +(function() { + this.isEqual = function(range) { + return this.start.row === range.start.row && + this.end.row === range.end.row && + this.start.column === range.start.column && + this.end.column === range.end.column; + }; + this.toString = function() { + return ("Range: [" + this.start.row + "/" + this.start.column + + "] -> [" + this.end.row + "/" + this.end.column + "]"); + }; + + this.contains = function(row, column) { + return this.compare(row, column) == 0; + }; + this.compareRange = function(range) { + var cmp, + end = range.end, + start = range.start; + + cmp = this.compare(end.row, end.column); + if (cmp == 1) { + cmp = this.compare(start.row, start.column); + if (cmp == 1) { + return 2; + } else if (cmp == 0) { + return 1; + } else { + return 0; + } + } else if (cmp == -1) { + return -2; + } else { + cmp = this.compare(start.row, start.column); + if (cmp == -1) { + return -1; + } else if (cmp == 1) { + return 42; + } else { + return 0; + } + } + }; + this.comparePoint = function(p) { + return this.compare(p.row, p.column); + }; + this.containsRange = function(range) { + return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0; + }; + this.intersects = function(range) { + var cmp = this.compareRange(range); + return (cmp == -1 || cmp == 0 || cmp == 1); + }; + this.isEnd = function(row, column) { + return this.end.row == row && this.end.column == column; + }; + this.isStart = function(row, column) { + return this.start.row == row && this.start.column == column; + }; + this.setStart = function(row, column) { + if (typeof row == "object") { + this.start.column = row.column; + this.start.row = row.row; + } else { + this.start.row = row; + this.start.column = column; + } + }; + this.setEnd = function(row, column) { + if (typeof row == "object") { + this.end.column = row.column; + this.end.row = row.row; + } else { + this.end.row = row; + this.end.column = column; + } + }; + this.inside = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column) || this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.insideStart = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.insideEnd = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.compare = function(row, column) { + if (!this.isMultiLine()) { + if (row === this.start.row) { + return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0); + } + } + + if (row < this.start.row) + return -1; + + if (row > this.end.row) + return 1; + + if (this.start.row === row) + return column >= this.start.column ? 0 : -1; + + if (this.end.row === row) + return column <= this.end.column ? 0 : 1; + + return 0; + }; + this.compareStart = function(row, column) { + if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + }; + this.compareEnd = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else { + return this.compare(row, column); + } + }; + this.compareInside = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + }; + this.clipRows = function(firstRow, lastRow) { + if (this.end.row > lastRow) + var end = {row: lastRow + 1, column: 0}; + else if (this.end.row < firstRow) + var end = {row: firstRow, column: 0}; + + if (this.start.row > lastRow) + var start = {row: lastRow + 1, column: 0}; + else if (this.start.row < firstRow) + var start = {row: firstRow, column: 0}; + + return Range.fromPoints(start || this.start, end || this.end); + }; + this.extend = function(row, column) { + var cmp = this.compare(row, column); + + if (cmp == 0) + return this; + else if (cmp == -1) + var start = {row: row, column: column}; + else + var end = {row: row, column: column}; + + return Range.fromPoints(start || this.start, end || this.end); + }; + + this.isEmpty = function() { + return (this.start.row === this.end.row && this.start.column === this.end.column); + }; + this.isMultiLine = function() { + return (this.start.row !== this.end.row); + }; + this.clone = function() { + return Range.fromPoints(this.start, this.end); + }; + this.collapseRows = function() { + if (this.end.column == 0) + return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0); + else + return new Range(this.start.row, 0, this.end.row, 0); + }; + this.toScreenRange = function(session) { + var screenPosStart = session.documentToScreenPosition(this.start); + var screenPosEnd = session.documentToScreenPosition(this.end); + + return new Range( + screenPosStart.row, screenPosStart.column, + screenPosEnd.row, screenPosEnd.column + ); + }; + this.moveBy = function(row, column) { + this.start.row += row; + this.start.column += column; + this.end.row += row; + this.end.column += column; + }; + +}).call(Range.prototype); +Range.fromPoints = function(start, end) { + return new Range(start.row, start.column, end.row, end.column); +}; +Range.comparePoints = comparePoints; + +Range.comparePoints = function(p1, p2) { + return p1.row - p2.row || p1.column - p2.column; +}; + + +exports.Range = Range; +}); + +ace.define("ace/apply_delta",[], function(require, exports, module) { +"use strict"; + +function throwDeltaError(delta, errorText){ + console.log("Invalid Delta:", delta); + throw "Invalid Delta: " + errorText; +} + +function positionInDocument(docLines, position) { + return position.row >= 0 && position.row < docLines.length && + position.column >= 0 && position.column <= docLines[position.row].length; +} + +function validateDelta(docLines, delta) { + if (delta.action != "insert" && delta.action != "remove") + throwDeltaError(delta, "delta.action must be 'insert' or 'remove'"); + if (!(delta.lines instanceof Array)) + throwDeltaError(delta, "delta.lines must be an Array"); + if (!delta.start || !delta.end) + throwDeltaError(delta, "delta.start/end must be an present"); + var start = delta.start; + if (!positionInDocument(docLines, delta.start)) + throwDeltaError(delta, "delta.start must be contained in document"); + var end = delta.end; + if (delta.action == "remove" && !positionInDocument(docLines, end)) + throwDeltaError(delta, "delta.end must contained in document for 'remove' actions"); + var numRangeRows = end.row - start.row; + var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0)); + if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars) + throwDeltaError(delta, "delta.range must match delta lines"); +} + +exports.applyDelta = function(docLines, delta, doNotValidate) { + + var row = delta.start.row; + var startColumn = delta.start.column; + var line = docLines[row] || ""; + switch (delta.action) { + case "insert": + var lines = delta.lines; + if (lines.length === 1) { + docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn); + } else { + var args = [row, 1].concat(delta.lines); + docLines.splice.apply(docLines, args); + docLines[row] = line.substring(0, startColumn) + docLines[row]; + docLines[row + delta.lines.length - 1] += line.substring(startColumn); + } + break; + case "remove": + var endColumn = delta.end.column; + var endRow = delta.end.row; + if (row === endRow) { + docLines[row] = line.substring(0, startColumn) + line.substring(endColumn); + } else { + docLines.splice( + row, endRow - row + 1, + line.substring(0, startColumn) + docLines[endRow].substring(endColumn) + ); + } + break; + } +}; +}); + +ace.define("ace/lib/event_emitter",[], function(require, exports, module) { +"use strict"; + +var EventEmitter = {}; +var stopPropagation = function() { this.propagationStopped = true; }; +var preventDefault = function() { this.defaultPrevented = true; }; + +EventEmitter._emit = +EventEmitter._dispatchEvent = function(eventName, e) { + this._eventRegistry || (this._eventRegistry = {}); + this._defaultHandlers || (this._defaultHandlers = {}); + + var listeners = this._eventRegistry[eventName] || []; + var defaultHandler = this._defaultHandlers[eventName]; + if (!listeners.length && !defaultHandler) + return; + + if (typeof e != "object" || !e) + e = {}; + + if (!e.type) + e.type = eventName; + if (!e.stopPropagation) + e.stopPropagation = stopPropagation; + if (!e.preventDefault) + e.preventDefault = preventDefault; + + listeners = listeners.slice(); + for (var i=0; i this.row) + return; + + var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight); + this.setPosition(point.row, point.column, true); + }; + + function $pointsInOrder(point1, point2, equalPointsInOrder) { + var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; + return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter); + } + + function $getTransformedPoint(delta, point, moveIfEqual) { + var deltaIsInsert = delta.action == "insert"; + var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row); + var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column); + var deltaStart = delta.start; + var deltaEnd = deltaIsInsert ? deltaStart : delta.end; // Collapse insert range. + if ($pointsInOrder(point, deltaStart, moveIfEqual)) { + return { + row: point.row, + column: point.column + }; + } + if ($pointsInOrder(deltaEnd, point, !moveIfEqual)) { + return { + row: point.row + deltaRowShift, + column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) + }; + } + + return { + row: deltaStart.row, + column: deltaStart.column + }; + } + this.setPosition = function(row, column, noClip) { + var pos; + if (noClip) { + pos = { + row: row, + column: column + }; + } else { + pos = this.$clipPositionToDocument(row, column); + } + + if (this.row == pos.row && this.column == pos.column) + return; + + var old = { + row: this.row, + column: this.column + }; + + this.row = pos.row; + this.column = pos.column; + this._signal("change", { + old: old, + value: pos + }); + }; + this.detach = function() { + this.document.off("change", this.$onChange); + }; + this.attach = function(doc) { + this.document = doc || this.document; + this.document.on("change", this.$onChange); + }; + this.$clipPositionToDocument = function(row, column) { + var pos = {}; + + if (row >= this.document.getLength()) { + pos.row = Math.max(0, this.document.getLength() - 1); + pos.column = this.document.getLine(pos.row).length; + } + else if (row < 0) { + pos.row = 0; + pos.column = 0; + } + else { + pos.row = row; + pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column)); + } + + if (column < 0) + pos.column = 0; + + return pos; + }; + +}).call(Anchor.prototype); + +}); + +ace.define("ace/document",[], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var applyDelta = require("./apply_delta").applyDelta; +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var Range = require("./range").Range; +var Anchor = require("./anchor").Anchor; + +var Document = function(textOrLines) { + this.$lines = [""]; + if (textOrLines.length === 0) { + this.$lines = [""]; + } else if (Array.isArray(textOrLines)) { + this.insertMergedLines({row: 0, column: 0}, textOrLines); + } else { + this.insert({row: 0, column:0}, textOrLines); + } +}; + +(function() { + + oop.implement(this, EventEmitter); + this.setValue = function(text) { + var len = this.getLength() - 1; + this.remove(new Range(0, 0, len, this.getLine(len).length)); + this.insert({row: 0, column: 0}, text); + }; + this.getValue = function() { + return this.getAllLines().join(this.getNewLineCharacter()); + }; + this.createAnchor = function(row, column) { + return new Anchor(this, row, column); + }; + if ("aaa".split(/a/).length === 0) { + this.$split = function(text) { + return text.replace(/\r\n|\r/g, "\n").split("\n"); + }; + } else { + this.$split = function(text) { + return text.split(/\r\n|\r|\n/); + }; + } + + + this.$detectNewLine = function(text) { + var match = text.match(/^.*?(\r\n|\r|\n)/m); + this.$autoNewLine = match ? match[1] : "\n"; + this._signal("changeNewLineMode"); + }; + this.getNewLineCharacter = function() { + switch (this.$newLineMode) { + case "windows": + return "\r\n"; + case "unix": + return "\n"; + default: + return this.$autoNewLine || "\n"; + } + }; + + this.$autoNewLine = ""; + this.$newLineMode = "auto"; + this.setNewLineMode = function(newLineMode) { + if (this.$newLineMode === newLineMode) + return; + + this.$newLineMode = newLineMode; + this._signal("changeNewLineMode"); + }; + this.getNewLineMode = function() { + return this.$newLineMode; + }; + this.isNewLine = function(text) { + return (text == "\r\n" || text == "\r" || text == "\n"); + }; + this.getLine = function(row) { + return this.$lines[row] || ""; + }; + this.getLines = function(firstRow, lastRow) { + return this.$lines.slice(firstRow, lastRow + 1); + }; + this.getAllLines = function() { + return this.getLines(0, this.getLength()); + }; + this.getLength = function() { + return this.$lines.length; + }; + this.getTextRange = function(range) { + return this.getLinesForRange(range).join(this.getNewLineCharacter()); + }; + this.getLinesForRange = function(range) { + var lines; + if (range.start.row === range.end.row) { + lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)]; + } else { + lines = this.getLines(range.start.row, range.end.row); + lines[0] = (lines[0] || "").substring(range.start.column); + var l = lines.length - 1; + if (range.end.row - range.start.row == l) + lines[l] = lines[l].substring(0, range.end.column); + } + return lines; + }; + this.insertLines = function(row, lines) { + console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."); + return this.insertFullLines(row, lines); + }; + this.removeLines = function(firstRow, lastRow) { + console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."); + return this.removeFullLines(firstRow, lastRow); + }; + this.insertNewLine = function(position) { + console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."); + return this.insertMergedLines(position, ["", ""]); + }; + this.insert = function(position, text) { + if (this.getLength() <= 1) + this.$detectNewLine(text); + + return this.insertMergedLines(position, this.$split(text)); + }; + this.insertInLine = function(position, text) { + var start = this.clippedPos(position.row, position.column); + var end = this.pos(position.row, position.column + text.length); + + this.applyDelta({ + start: start, + end: end, + action: "insert", + lines: [text] + }, true); + + return this.clonePos(end); + }; + + this.clippedPos = function(row, column) { + var length = this.getLength(); + if (row === undefined) { + row = length; + } else if (row < 0) { + row = 0; + } else if (row >= length) { + row = length - 1; + column = undefined; + } + var line = this.getLine(row); + if (column == undefined) + column = line.length; + column = Math.min(Math.max(column, 0), line.length); + return {row: row, column: column}; + }; + + this.clonePos = function(pos) { + return {row: pos.row, column: pos.column}; + }; + + this.pos = function(row, column) { + return {row: row, column: column}; + }; + + this.$clipPosition = function(position) { + var length = this.getLength(); + if (position.row >= length) { + position.row = Math.max(0, length - 1); + position.column = this.getLine(length - 1).length; + } else { + position.row = Math.max(0, position.row); + position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length); + } + return position; + }; + this.insertFullLines = function(row, lines) { + row = Math.min(Math.max(row, 0), this.getLength()); + var column = 0; + if (row < this.getLength()) { + lines = lines.concat([""]); + column = 0; + } else { + lines = [""].concat(lines); + row--; + column = this.$lines[row].length; + } + this.insertMergedLines({row: row, column: column}, lines); + }; + this.insertMergedLines = function(position, lines) { + var start = this.clippedPos(position.row, position.column); + var end = { + row: start.row + lines.length - 1, + column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length + }; + + this.applyDelta({ + start: start, + end: end, + action: "insert", + lines: lines + }); + + return this.clonePos(end); + }; + this.remove = function(range) { + var start = this.clippedPos(range.start.row, range.start.column); + var end = this.clippedPos(range.end.row, range.end.column); + this.applyDelta({ + start: start, + end: end, + action: "remove", + lines: this.getLinesForRange({start: start, end: end}) + }); + return this.clonePos(start); + }; + this.removeInLine = function(row, startColumn, endColumn) { + var start = this.clippedPos(row, startColumn); + var end = this.clippedPos(row, endColumn); + + this.applyDelta({ + start: start, + end: end, + action: "remove", + lines: this.getLinesForRange({start: start, end: end}) + }, true); + + return this.clonePos(start); + }; + this.removeFullLines = function(firstRow, lastRow) { + firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1); + lastRow = Math.min(Math.max(0, lastRow ), this.getLength() - 1); + var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0; + var deleteLastNewLine = lastRow < this.getLength() - 1; + var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow ); + var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 ); + var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow ); + var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); + var range = new Range(startRow, startCol, endRow, endCol); + var deletedLines = this.$lines.slice(firstRow, lastRow + 1); + + this.applyDelta({ + start: range.start, + end: range.end, + action: "remove", + lines: this.getLinesForRange(range) + }); + return deletedLines; + }; + this.removeNewLine = function(row) { + if (row < this.getLength() - 1 && row >= 0) { + this.applyDelta({ + start: this.pos(row, this.getLine(row).length), + end: this.pos(row + 1, 0), + action: "remove", + lines: ["", ""] + }); + } + }; + this.replace = function(range, text) { + if (!(range instanceof Range)) + range = Range.fromPoints(range.start, range.end); + if (text.length === 0 && range.isEmpty()) + return range.start; + if (text == this.getTextRange(range)) + return range.end; + + this.remove(range); + var end; + if (text) { + end = this.insert(range.start, text); + } + else { + end = range.start; + } + + return end; + }; + this.applyDeltas = function(deltas) { + for (var i=0; i=0; i--) { + this.revertDelta(deltas[i]); + } + }; + this.applyDelta = function(delta, doNotValidate) { + var isInsert = delta.action == "insert"; + if (isInsert ? delta.lines.length <= 1 && !delta.lines[0] + : !Range.comparePoints(delta.start, delta.end)) { + return; + } + + if (isInsert && delta.lines.length > 20000) { + this.$splitAndapplyLargeDelta(delta, 20000); + } + else { + applyDelta(this.$lines, delta, doNotValidate); + this._signal("change", delta); + } + }; + + this.$safeApplyDelta = function(delta) { + var docLength = this.$lines.length; + if ( + delta.action == "remove" && delta.start.row < docLength && delta.end.row < docLength + || delta.action == "insert" && delta.start.row <= docLength + ) { + this.applyDelta(delta); + } + }; + + this.$splitAndapplyLargeDelta = function(delta, MAX) { + var lines = delta.lines; + var l = lines.length - MAX + 1; + var row = delta.start.row; + var column = delta.start.column; + for (var from = 0, to = 0; from < l; from = to) { + to += MAX - 1; + var chunk = lines.slice(from, to); + chunk.push(""); + this.applyDelta({ + start: this.pos(row + from, column), + end: this.pos(row + to, column = 0), + action: delta.action, + lines: chunk + }, true); + } + delta.lines = lines.slice(from); + delta.start.row = row + from; + delta.start.column = column; + this.applyDelta(delta, true); + }; + this.revertDelta = function(delta) { + this.$safeApplyDelta({ + start: this.clonePos(delta.start), + end: this.clonePos(delta.end), + action: (delta.action == "insert" ? "remove" : "insert"), + lines: delta.lines.slice() + }); + }; + this.indexToPosition = function(index, startRow) { + var lines = this.$lines || this.getAllLines(); + var newlineLength = this.getNewLineCharacter().length; + for (var i = startRow || 0, l = lines.length; i < l; i++) { + index -= lines[i].length + newlineLength; + if (index < 0) + return {row: i, column: index + lines[i].length + newlineLength}; + } + return {row: l-1, column: index + lines[l-1].length + newlineLength}; + }; + this.positionToIndex = function(pos, startRow) { + var lines = this.$lines || this.getAllLines(); + var newlineLength = this.getNewLineCharacter().length; + var index = 0; + var row = Math.min(pos.row, lines.length); + for (var i = startRow || 0; i < row; ++i) + index += lines[i].length + newlineLength; + + return index + pos.column; + }; + +}).call(Document.prototype); + +exports.Document = Document; +}); + +ace.define("ace/lib/lang",[], function(require, exports, module) { +"use strict"; + +exports.last = function(a) { + return a[a.length - 1]; +}; + +exports.stringReverse = function(string) { + return string.split("").reverse().join(""); +}; + +exports.stringRepeat = function (string, count) { + var result = ''; + while (count > 0) { + if (count & 1) + result += string; + + if (count >>= 1) + string += string; + } + return result; +}; + +var trimBeginRegexp = /^\s\s*/; +var trimEndRegexp = /\s\s*$/; + +exports.stringTrimLeft = function (string) { + return string.replace(trimBeginRegexp, ''); +}; + +exports.stringTrimRight = function (string) { + return string.replace(trimEndRegexp, ''); +}; + +exports.copyObject = function(obj) { + var copy = {}; + for (var key in obj) { + copy[key] = obj[key]; + } + return copy; +}; + +exports.copyArray = function(array){ + var copy = []; + for (var i=0, l=array.length; i= 65 && i <= 90 || // A-Z + i === 95 || // _ + i >= 97 && i <= 122; // a-z +} + +var identifierPartTable = []; + +for (var i = 0; i < 128; i++) { + identifierPartTable[i] = + identifierStartTable[i] || // $, _, A-Z, a-z + i >= 48 && i <= 57; // 0-9 +} + +module.exports = { + asciiIdentifierStartTable: identifierStartTable, + asciiIdentifierPartTable: identifierPartTable +}; + +},{}],"/../../jshint/data/es5-identifier-names.js":[function(_dereq_,module,exports){ +module.exports = /^(?:[\$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0525\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0621-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971\u0972\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D3D\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8B\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10D0-\u10FA\u10FC\u1100-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCB\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA65F\uA662-\uA66E\uA67F-\uA697\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B\uA78C\uA7FB-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA2D\uFA30-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])(?:[\$0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u0525\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0621-\u065E\u0660-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0900-\u0939\u093C-\u094E\u0950-\u0955\u0958-\u0963\u0966-\u096F\u0971\u0972\u0979-\u097F\u0981-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C82\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0D02\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC\u0EDD\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F8B\u0F90-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10D0-\u10FA\u10FC\u1100-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17B3\u17B6-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191C\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BAA\u1BAE-\u1BB9\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF2\u1D00-\u1DE6\u1DFD-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u2094\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF1\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCB\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA65F\uA662-\uA66F\uA67C\uA67D\uA67F-\uA697\uA6A0-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B\uA78C\uA7FB-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7B\uAA80-\uAAC2\uAADB-\uAADD\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA2D\uFA30-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE26\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])*$/; +},{}],"/../../jshint/data/non-ascii-identifier-part-only.js":[function(_dereq_,module,exports){ +var str = '183,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,903,1155,1156,1157,1158,1159,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1471,1473,1474,1476,1477,1479,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1648,1750,1751,1752,1753,1754,1755,1756,1759,1760,1761,1762,1763,1764,1767,1768,1770,1771,1772,1773,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1809,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,2027,2028,2029,2030,2031,2032,2033,2034,2035,2045,2070,2071,2072,2073,2075,2076,2077,2078,2079,2080,2081,2082,2083,2085,2086,2087,2089,2090,2091,2092,2093,2137,2138,2139,2259,2260,2261,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2275,2276,2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307,2362,2363,2364,2366,2367,2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380,2381,2382,2383,2385,2386,2387,2388,2389,2390,2391,2402,2403,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2433,2434,2435,2492,2494,2495,2496,2497,2498,2499,2500,2503,2504,2507,2508,2509,2519,2530,2531,2534,2535,2536,2537,2538,2539,2540,2541,2542,2543,2558,2561,2562,2563,2620,2622,2623,2624,2625,2626,2631,2632,2635,2636,2637,2641,2662,2663,2664,2665,2666,2667,2668,2669,2670,2671,2672,2673,2677,2689,2690,2691,2748,2750,2751,2752,2753,2754,2755,2756,2757,2759,2760,2761,2763,2764,2765,2786,2787,2790,2791,2792,2793,2794,2795,2796,2797,2798,2799,2810,2811,2812,2813,2814,2815,2817,2818,2819,2876,2878,2879,2880,2881,2882,2883,2884,2887,2888,2891,2892,2893,2902,2903,2914,2915,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2946,3006,3007,3008,3009,3010,3014,3015,3016,3018,3019,3020,3021,3031,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3072,3073,3074,3075,3076,3134,3135,3136,3137,3138,3139,3140,3142,3143,3144,3146,3147,3148,3149,3157,3158,3170,3171,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3201,3202,3203,3260,3262,3263,3264,3265,3266,3267,3268,3270,3271,3272,3274,3275,3276,3277,3285,3286,3298,3299,3302,3303,3304,3305,3306,3307,3308,3309,3310,3311,3328,3329,3330,3331,3387,3388,3390,3391,3392,3393,3394,3395,3396,3398,3399,3400,3402,3403,3404,3405,3415,3426,3427,3430,3431,3432,3433,3434,3435,3436,3437,3438,3439,3458,3459,3530,3535,3536,3537,3538,3539,3540,3542,3544,3545,3546,3547,3548,3549,3550,3551,3558,3559,3560,3561,3562,3563,3564,3565,3566,3567,3570,3571,3633,3636,3637,3638,3639,3640,3641,3642,3655,3656,3657,3658,3659,3660,3661,3662,3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3761,3764,3765,3766,3767,3768,3769,3771,3772,3784,3785,3786,3787,3788,3789,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3864,3865,3872,3873,3874,3875,3876,3877,3878,3879,3880,3881,3893,3895,3897,3902,3903,3953,3954,3955,3956,3957,3958,3959,3960,3961,3962,3963,3964,3965,3966,3967,3968,3969,3970,3971,3972,3974,3975,3981,3982,3983,3984,3985,3986,3987,3988,3989,3990,3991,3993,3994,3995,3996,3997,3998,3999,4000,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4038,4139,4140,4141,4142,4143,4144,4145,4146,4147,4148,4149,4150,4151,4152,4153,4154,4155,4156,4157,4158,4160,4161,4162,4163,4164,4165,4166,4167,4168,4169,4182,4183,4184,4185,4190,4191,4192,4194,4195,4196,4199,4200,4201,4202,4203,4204,4205,4209,4210,4211,4212,4226,4227,4228,4229,4230,4231,4232,4233,4234,4235,4236,4237,4239,4240,4241,4242,4243,4244,4245,4246,4247,4248,4249,4250,4251,4252,4253,4957,4958,4959,4969,4970,4971,4972,4973,4974,4975,4976,4977,5906,5907,5908,5938,5939,5940,5970,5971,6002,6003,6068,6069,6070,6071,6072,6073,6074,6075,6076,6077,6078,6079,6080,6081,6082,6083,6084,6085,6086,6087,6088,6089,6090,6091,6092,6093,6094,6095,6096,6097,6098,6099,6109,6112,6113,6114,6115,6116,6117,6118,6119,6120,6121,6155,6156,6157,6160,6161,6162,6163,6164,6165,6166,6167,6168,6169,6313,6432,6433,6434,6435,6436,6437,6438,6439,6440,6441,6442,6443,6448,6449,6450,6451,6452,6453,6454,6455,6456,6457,6458,6459,6470,6471,6472,6473,6474,6475,6476,6477,6478,6479,6608,6609,6610,6611,6612,6613,6614,6615,6616,6617,6618,6679,6680,6681,6682,6683,6741,6742,6743,6744,6745,6746,6747,6748,6749,6750,6752,6753,6754,6755,6756,6757,6758,6759,6760,6761,6762,6763,6764,6765,6766,6767,6768,6769,6770,6771,6772,6773,6774,6775,6776,6777,6778,6779,6780,6783,6784,6785,6786,6787,6788,6789,6790,6791,6792,6793,6800,6801,6802,6803,6804,6805,6806,6807,6808,6809,6832,6833,6834,6835,6836,6837,6838,6839,6840,6841,6842,6843,6844,6845,6912,6913,6914,6915,6916,6964,6965,6966,6967,6968,6969,6970,6971,6972,6973,6974,6975,6976,6977,6978,6979,6980,6992,6993,6994,6995,6996,6997,6998,6999,7000,7001,7019,7020,7021,7022,7023,7024,7025,7026,7027,7040,7041,7042,7073,7074,7075,7076,7077,7078,7079,7080,7081,7082,7083,7084,7085,7088,7089,7090,7091,7092,7093,7094,7095,7096,7097,7142,7143,7144,7145,7146,7147,7148,7149,7150,7151,7152,7153,7154,7155,7204,7205,7206,7207,7208,7209,7210,7211,7212,7213,7214,7215,7216,7217,7218,7219,7220,7221,7222,7223,7232,7233,7234,7235,7236,7237,7238,7239,7240,7241,7248,7249,7250,7251,7252,7253,7254,7255,7256,7257,7376,7377,7378,7380,7381,7382,7383,7384,7385,7386,7387,7388,7389,7390,7391,7392,7393,7394,7395,7396,7397,7398,7399,7400,7405,7410,7411,7412,7415,7416,7417,7616,7617,7618,7619,7620,7621,7622,7623,7624,7625,7626,7627,7628,7629,7630,7631,7632,7633,7634,7635,7636,7637,7638,7639,7640,7641,7642,7643,7644,7645,7646,7647,7648,7649,7650,7651,7652,7653,7654,7655,7656,7657,7658,7659,7660,7661,7662,7663,7664,7665,7666,7667,7668,7669,7670,7671,7672,7673,7675,7676,7677,7678,7679,8204,8205,8255,8256,8276,8400,8401,8402,8403,8404,8405,8406,8407,8408,8409,8410,8411,8412,8417,8421,8422,8423,8424,8425,8426,8427,8428,8429,8430,8431,8432,11503,11504,11505,11647,11744,11745,11746,11747,11748,11749,11750,11751,11752,11753,11754,11755,11756,11757,11758,11759,11760,11761,11762,11763,11764,11765,11766,11767,11768,11769,11770,11771,11772,11773,11774,11775,12330,12331,12332,12333,12334,12335,12441,12442,42528,42529,42530,42531,42532,42533,42534,42535,42536,42537,42607,42612,42613,42614,42615,42616,42617,42618,42619,42620,42621,42654,42655,42736,42737,43010,43014,43019,43043,43044,43045,43046,43047,43136,43137,43188,43189,43190,43191,43192,43193,43194,43195,43196,43197,43198,43199,43200,43201,43202,43203,43204,43205,43216,43217,43218,43219,43220,43221,43222,43223,43224,43225,43232,43233,43234,43235,43236,43237,43238,43239,43240,43241,43242,43243,43244,43245,43246,43247,43248,43249,43263,43264,43265,43266,43267,43268,43269,43270,43271,43272,43273,43302,43303,43304,43305,43306,43307,43308,43309,43335,43336,43337,43338,43339,43340,43341,43342,43343,43344,43345,43346,43347,43392,43393,43394,43395,43443,43444,43445,43446,43447,43448,43449,43450,43451,43452,43453,43454,43455,43456,43472,43473,43474,43475,43476,43477,43478,43479,43480,43481,43493,43504,43505,43506,43507,43508,43509,43510,43511,43512,43513,43561,43562,43563,43564,43565,43566,43567,43568,43569,43570,43571,43572,43573,43574,43587,43596,43597,43600,43601,43602,43603,43604,43605,43606,43607,43608,43609,43643,43644,43645,43696,43698,43699,43700,43703,43704,43710,43711,43713,43755,43756,43757,43758,43759,43765,43766,44003,44004,44005,44006,44007,44008,44009,44010,44012,44013,44016,44017,44018,44019,44020,44021,44022,44023,44024,44025,64286,65024,65025,65026,65027,65028,65029,65030,65031,65032,65033,65034,65035,65036,65037,65038,65039,65056,65057,65058,65059,65060,65061,65062,65063,65064,65065,65066,65067,65068,65069,65070,65071,65075,65076,65101,65102,65103,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,65343'; +var arr = str.split(',').map(function(code) { + return parseInt(code, 10); +}); +module.exports = arr; +},{}],"/../../jshint/data/non-ascii-identifier-start.js":[function(_dereq_,module,exports){ +var str = '170,181,186,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,710,711,712,713,714,715,716,717,718,719,720,721,736,737,738,739,740,748,750,880,881,882,883,884,886,887,890,891,892,893,895,902,904,905,906,908,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1369,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1519,1520,1521,1522,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1646,1647,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1749,1765,1766,1774,1775,1786,1787,1788,1791,1808,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1969,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2036,2037,2042,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2074,2084,2088,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227,2228,2230,2231,2232,2233,2234,2235,2236,2237,2308,2309,2310,2311,2312,2313,2314,2315,2316,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357,2358,2359,2360,2361,2365,2384,2392,2393,2394,2395,2396,2397,2398,2399,2400,2401,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2437,2438,2439,2440,2441,2442,2443,2444,2447,2448,2451,2452,2453,2454,2455,2456,2457,2458,2459,2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,2470,2471,2472,2474,2475,2476,2477,2478,2479,2480,2482,2486,2487,2488,2489,2493,2510,2524,2525,2527,2528,2529,2544,2545,2556,2565,2566,2567,2568,2569,2570,2575,2576,2579,2580,2581,2582,2583,2584,2585,2586,2587,2588,2589,2590,2591,2592,2593,2594,2595,2596,2597,2598,2599,2600,2602,2603,2604,2605,2606,2607,2608,2610,2611,2613,2614,2616,2617,2649,2650,2651,2652,2654,2674,2675,2676,2693,2694,2695,2696,2697,2698,2699,2700,2701,2703,2704,2705,2707,2708,2709,2710,2711,2712,2713,2714,2715,2716,2717,2718,2719,2720,2721,2722,2723,2724,2725,2726,2727,2728,2730,2731,2732,2733,2734,2735,2736,2738,2739,2741,2742,2743,2744,2745,2749,2768,2784,2785,2809,2821,2822,2823,2824,2825,2826,2827,2828,2831,2832,2835,2836,2837,2838,2839,2840,2841,2842,2843,2844,2845,2846,2847,2848,2849,2850,2851,2852,2853,2854,2855,2856,2858,2859,2860,2861,2862,2863,2864,2866,2867,2869,2870,2871,2872,2873,2877,2908,2909,2911,2912,2913,2929,2947,2949,2950,2951,2952,2953,2954,2958,2959,2960,2962,2963,2964,2965,2969,2970,2972,2974,2975,2979,2980,2984,2985,2986,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3024,3077,3078,3079,3080,3081,3082,3083,3084,3086,3087,3088,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3133,3160,3161,3162,3168,3169,3200,3205,3206,3207,3208,3209,3210,3211,3212,3214,3215,3216,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3253,3254,3255,3256,3257,3261,3294,3296,3297,3313,3314,3333,3334,3335,3336,3337,3338,3339,3340,3342,3343,3344,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3389,3406,3412,3413,3414,3423,3424,3425,3450,3451,3452,3453,3454,3455,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500,3501,3502,3503,3504,3505,3507,3508,3509,3510,3511,3512,3513,3514,3515,3517,3520,3521,3522,3523,3524,3525,3526,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3634,3635,3648,3649,3650,3651,3652,3653,3654,3713,3714,3716,3719,3720,3722,3725,3732,3733,3734,3735,3737,3738,3739,3740,3741,3742,3743,3745,3746,3747,3749,3751,3754,3755,3757,3758,3759,3760,3762,3763,3773,3776,3777,3778,3779,3780,3782,3804,3805,3806,3807,3840,3904,3905,3906,3907,3908,3909,3910,3911,3913,3914,3915,3916,3917,3918,3919,3920,3921,3922,3923,3924,3925,3926,3927,3928,3929,3930,3931,3932,3933,3934,3935,3936,3937,3938,3939,3940,3941,3942,3943,3944,3945,3946,3947,3948,3976,3977,3978,3979,3980,4096,4097,4098,4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,4109,4110,4111,4112,4113,4114,4115,4116,4117,4118,4119,4120,4121,4122,4123,4124,4125,4126,4127,4128,4129,4130,4131,4132,4133,4134,4135,4136,4137,4138,4159,4176,4177,4178,4179,4180,4181,4186,4187,4188,4189,4193,4197,4198,4206,4207,4208,4213,4214,4215,4216,4217,4218,4219,4220,4221,4222,4223,4224,4225,4238,4256,4257,4258,4259,4260,4261,4262,4263,4264,4265,4266,4267,4268,4269,4270,4271,4272,4273,4274,4275,4276,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289,4290,4291,4292,4293,4295,4301,4304,4305,4306,4307,4308,4309,4310,4311,4312,4313,4314,4315,4316,4317,4318,4319,4320,4321,4322,4323,4324,4325,4326,4327,4328,4329,4330,4331,4332,4333,4334,4335,4336,4337,4338,4339,4340,4341,4342,4343,4344,4345,4346,4348,4349,4350,4351,4352,4353,4354,4355,4356,4357,4358,4359,4360,4361,4362,4363,4364,4365,4366,4367,4368,4369,4370,4371,4372,4373,4374,4375,4376,4377,4378,4379,4380,4381,4382,4383,4384,4385,4386,4387,4388,4389,4390,4391,4392,4393,4394,4395,4396,4397,4398,4399,4400,4401,4402,4403,4404,4405,4406,4407,4408,4409,4410,4411,4412,4413,4414,4415,4416,4417,4418,4419,4420,4421,4422,4423,4424,4425,4426,4427,4428,4429,4430,4431,4432,4433,4434,4435,4436,4437,4438,4439,4440,4441,4442,4443,4444,4445,4446,4447,4448,4449,4450,4451,4452,4453,4454,4455,4456,4457,4458,4459,4460,4461,4462,4463,4464,4465,4466,4467,4468,4469,4470,4471,4472,4473,4474,4475,4476,4477,4478,4479,4480,4481,4482,4483,4484,4485,4486,4487,4488,4489,4490,4491,4492,4493,4494,4495,4496,4497,4498,4499,4500,4501,4502,4503,4504,4505,4506,4507,4508,4509,4510,4511,4512,4513,4514,4515,4516,4517,4518,4519,4520,4521,4522,4523,4524,4525,4526,4527,4528,4529,4530,4531,4532,4533,4534,4535,4536,4537,4538,4539,4540,4541,4542,4543,4544,4545,4546,4547,4548,4549,4550,4551,4552,4553,4554,4555,4556,4557,4558,4559,4560,4561,4562,4563,4564,4565,4566,4567,4568,4569,4570,4571,4572,4573,4574,4575,4576,4577,4578,4579,4580,4581,4582,4583,4584,4585,4586,4587,4588,4589,4590,4591,4592,4593,4594,4595,4596,4597,4598,4599,4600,4601,4602,4603,4604,4605,4606,4607,4608,4609,4610,4611,4612,4613,4614,4615,4616,4617,4618,4619,4620,4621,4622,4623,4624,4625,4626,4627,4628,4629,4630,4631,4632,4633,4634,4635,4636,4637,4638,4639,4640,4641,4642,4643,4644,4645,4646,4647,4648,4649,4650,4651,4652,4653,4654,4655,4656,4657,4658,4659,4660,4661,4662,4663,4664,4665,4666,4667,4668,4669,4670,4671,4672,4673,4674,4675,4676,4677,4678,4679,4680,4682,4683,4684,4685,4688,4689,4690,4691,4692,4693,4694,4696,4698,4699,4700,4701,4704,4705,4706,4707,4708,4709,4710,4711,4712,4713,4714,4715,4716,4717,4718,4719,4720,4721,4722,4723,4724,4725,4726,4727,4728,4729,4730,4731,4732,4733,4734,4735,4736,4737,4738,4739,4740,4741,4742,4743,4744,4746,4747,4748,4749,4752,4753,4754,4755,4756,4757,4758,4759,4760,4761,4762,4763,4764,4765,4766,4767,4768,4769,4770,4771,4772,4773,4774,4775,4776,4777,4778,4779,4780,4781,4782,4783,4784,4786,4787,4788,4789,4792,4793,4794,4795,4796,4797,4798,4800,4802,4803,4804,4805,4808,4809,4810,4811,4812,4813,4814,4815,4816,4817,4818,4819,4820,4821,4822,4824,4825,4826,4827,4828,4829,4830,4831,4832,4833,4834,4835,4836,4837,4838,4839,4840,4841,4842,4843,4844,4845,4846,4847,4848,4849,4850,4851,4852,4853,4854,4855,4856,4857,4858,4859,4860,4861,4862,4863,4864,4865,4866,4867,4868,4869,4870,4871,4872,4873,4874,4875,4876,4877,4878,4879,4880,4882,4883,4884,4885,4888,4889,4890,4891,4892,4893,4894,4895,4896,4897,4898,4899,4900,4901,4902,4903,4904,4905,4906,4907,4908,4909,4910,4911,4912,4913,4914,4915,4916,4917,4918,4919,4920,4921,4922,4923,4924,4925,4926,4927,4928,4929,4930,4931,4932,4933,4934,4935,4936,4937,4938,4939,4940,4941,4942,4943,4944,4945,4946,4947,4948,4949,4950,4951,4952,4953,4954,4992,4993,4994,4995,4996,4997,4998,4999,5000,5001,5002,5003,5004,5005,5006,5007,5024,5025,5026,5027,5028,5029,5030,5031,5032,5033,5034,5035,5036,5037,5038,5039,5040,5041,5042,5043,5044,5045,5046,5047,5048,5049,5050,5051,5052,5053,5054,5055,5056,5057,5058,5059,5060,5061,5062,5063,5064,5065,5066,5067,5068,5069,5070,5071,5072,5073,5074,5075,5076,5077,5078,5079,5080,5081,5082,5083,5084,5085,5086,5087,5088,5089,5090,5091,5092,5093,5094,5095,5096,5097,5098,5099,5100,5101,5102,5103,5104,5105,5106,5107,5108,5109,5112,5113,5114,5115,5116,5117,5121,5122,5123,5124,5125,5126,5127,5128,5129,5130,5131,5132,5133,5134,5135,5136,5137,5138,5139,5140,5141,5142,5143,5144,5145,5146,5147,5148,5149,5150,5151,5152,5153,5154,5155,5156,5157,5158,5159,5160,5161,5162,5163,5164,5165,5166,5167,5168,5169,5170,5171,5172,5173,5174,5175,5176,5177,5178,5179,5180,5181,5182,5183,5184,5185,5186,5187,5188,5189,5190,5191,5192,5193,5194,5195,5196,5197,5198,5199,5200,5201,5202,5203,5204,5205,5206,5207,5208,5209,5210,5211,5212,5213,5214,5215,5216,5217,5218,5219,5220,5221,5222,5223,5224,5225,5226,5227,5228,5229,5230,5231,5232,5233,5234,5235,5236,5237,5238,5239,5240,5241,5242,5243,5244,5245,5246,5247,5248,5249,5250,5251,5252,5253,5254,5255,5256,5257,5258,5259,5260,5261,5262,5263,5264,5265,5266,5267,5268,5269,5270,5271,5272,5273,5274,5275,5276,5277,5278,5279,5280,5281,5282,5283,5284,5285,5286,5287,5288,5289,5290,5291,5292,5293,5294,5295,5296,5297,5298,5299,5300,5301,5302,5303,5304,5305,5306,5307,5308,5309,5310,5311,5312,5313,5314,5315,5316,5317,5318,5319,5320,5321,5322,5323,5324,5325,5326,5327,5328,5329,5330,5331,5332,5333,5334,5335,5336,5337,5338,5339,5340,5341,5342,5343,5344,5345,5346,5347,5348,5349,5350,5351,5352,5353,5354,5355,5356,5357,5358,5359,5360,5361,5362,5363,5364,5365,5366,5367,5368,5369,5370,5371,5372,5373,5374,5375,5376,5377,5378,5379,5380,5381,5382,5383,5384,5385,5386,5387,5388,5389,5390,5391,5392,5393,5394,5395,5396,5397,5398,5399,5400,5401,5402,5403,5404,5405,5406,5407,5408,5409,5410,5411,5412,5413,5414,5415,5416,5417,5418,5419,5420,5421,5422,5423,5424,5425,5426,5427,5428,5429,5430,5431,5432,5433,5434,5435,5436,5437,5438,5439,5440,5441,5442,5443,5444,5445,5446,5447,5448,5449,5450,5451,5452,5453,5454,5455,5456,5457,5458,5459,5460,5461,5462,5463,5464,5465,5466,5467,5468,5469,5470,5471,5472,5473,5474,5475,5476,5477,5478,5479,5480,5481,5482,5483,5484,5485,5486,5487,5488,5489,5490,5491,5492,5493,5494,5495,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5511,5512,5513,5514,5515,5516,5517,5518,5519,5520,5521,5522,5523,5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536,5537,5538,5539,5540,5541,5542,5543,5544,5545,5546,5547,5548,5549,5550,5551,5552,5553,5554,5555,5556,5557,5558,5559,5560,5561,5562,5563,5564,5565,5566,5567,5568,5569,5570,5571,5572,5573,5574,5575,5576,5577,5578,5579,5580,5581,5582,5583,5584,5585,5586,5587,5588,5589,5590,5591,5592,5593,5594,5595,5596,5597,5598,5599,5600,5601,5602,5603,5604,5605,5606,5607,5608,5609,5610,5611,5612,5613,5614,5615,5616,5617,5618,5619,5620,5621,5622,5623,5624,5625,5626,5627,5628,5629,5630,5631,5632,5633,5634,5635,5636,5637,5638,5639,5640,5641,5642,5643,5644,5645,5646,5647,5648,5649,5650,5651,5652,5653,5654,5655,5656,5657,5658,5659,5660,5661,5662,5663,5664,5665,5666,5667,5668,5669,5670,5671,5672,5673,5674,5675,5676,5677,5678,5679,5680,5681,5682,5683,5684,5685,5686,5687,5688,5689,5690,5691,5692,5693,5694,5695,5696,5697,5698,5699,5700,5701,5702,5703,5704,5705,5706,5707,5708,5709,5710,5711,5712,5713,5714,5715,5716,5717,5718,5719,5720,5721,5722,5723,5724,5725,5726,5727,5728,5729,5730,5731,5732,5733,5734,5735,5736,5737,5738,5739,5740,5743,5744,5745,5746,5747,5748,5749,5750,5751,5752,5753,5754,5755,5756,5757,5758,5759,5761,5762,5763,5764,5765,5766,5767,5768,5769,5770,5771,5772,5773,5774,5775,5776,5777,5778,5779,5780,5781,5782,5783,5784,5785,5786,5792,5793,5794,5795,5796,5797,5798,5799,5800,5801,5802,5803,5804,5805,5806,5807,5808,5809,5810,5811,5812,5813,5814,5815,5816,5817,5818,5819,5820,5821,5822,5823,5824,5825,5826,5827,5828,5829,5830,5831,5832,5833,5834,5835,5836,5837,5838,5839,5840,5841,5842,5843,5844,5845,5846,5847,5848,5849,5850,5851,5852,5853,5854,5855,5856,5857,5858,5859,5860,5861,5862,5863,5864,5865,5866,5870,5871,5872,5873,5874,5875,5876,5877,5878,5879,5880,5888,5889,5890,5891,5892,5893,5894,5895,5896,5897,5898,5899,5900,5902,5903,5904,5905,5920,5921,5922,5923,5924,5925,5926,5927,5928,5929,5930,5931,5932,5933,5934,5935,5936,5937,5952,5953,5954,5955,5956,5957,5958,5959,5960,5961,5962,5963,5964,5965,5966,5967,5968,5969,5984,5985,5986,5987,5988,5989,5990,5991,5992,5993,5994,5995,5996,5998,5999,6000,6016,6017,6018,6019,6020,6021,6022,6023,6024,6025,6026,6027,6028,6029,6030,6031,6032,6033,6034,6035,6036,6037,6038,6039,6040,6041,6042,6043,6044,6045,6046,6047,6048,6049,6050,6051,6052,6053,6054,6055,6056,6057,6058,6059,6060,6061,6062,6063,6064,6065,6066,6067,6103,6108,6176,6177,6178,6179,6180,6181,6182,6183,6184,6185,6186,6187,6188,6189,6190,6191,6192,6193,6194,6195,6196,6197,6198,6199,6200,6201,6202,6203,6204,6205,6206,6207,6208,6209,6210,6211,6212,6213,6214,6215,6216,6217,6218,6219,6220,6221,6222,6223,6224,6225,6226,6227,6228,6229,6230,6231,6232,6233,6234,6235,6236,6237,6238,6239,6240,6241,6242,6243,6244,6245,6246,6247,6248,6249,6250,6251,6252,6253,6254,6255,6256,6257,6258,6259,6260,6261,6262,6263,6264,6272,6273,6274,6275,6276,6277,6278,6279,6280,6281,6282,6283,6284,6285,6286,6287,6288,6289,6290,6291,6292,6293,6294,6295,6296,6297,6298,6299,6300,6301,6302,6303,6304,6305,6306,6307,6308,6309,6310,6311,6312,6314,6320,6321,6322,6323,6324,6325,6326,6327,6328,6329,6330,6331,6332,6333,6334,6335,6336,6337,6338,6339,6340,6341,6342,6343,6344,6345,6346,6347,6348,6349,6350,6351,6352,6353,6354,6355,6356,6357,6358,6359,6360,6361,6362,6363,6364,6365,6366,6367,6368,6369,6370,6371,6372,6373,6374,6375,6376,6377,6378,6379,6380,6381,6382,6383,6384,6385,6386,6387,6388,6389,6400,6401,6402,6403,6404,6405,6406,6407,6408,6409,6410,6411,6412,6413,6414,6415,6416,6417,6418,6419,6420,6421,6422,6423,6424,6425,6426,6427,6428,6429,6430,6480,6481,6482,6483,6484,6485,6486,6487,6488,6489,6490,6491,6492,6493,6494,6495,6496,6497,6498,6499,6500,6501,6502,6503,6504,6505,6506,6507,6508,6509,6512,6513,6514,6515,6516,6528,6529,6530,6531,6532,6533,6534,6535,6536,6537,6538,6539,6540,6541,6542,6543,6544,6545,6546,6547,6548,6549,6550,6551,6552,6553,6554,6555,6556,6557,6558,6559,6560,6561,6562,6563,6564,6565,6566,6567,6568,6569,6570,6571,6576,6577,6578,6579,6580,6581,6582,6583,6584,6585,6586,6587,6588,6589,6590,6591,6592,6593,6594,6595,6596,6597,6598,6599,6600,6601,6656,6657,6658,6659,6660,6661,6662,6663,6664,6665,6666,6667,6668,6669,6670,6671,6672,6673,6674,6675,6676,6677,6678,6688,6689,6690,6691,6692,6693,6694,6695,6696,6697,6698,6699,6700,6701,6702,6703,6704,6705,6706,6707,6708,6709,6710,6711,6712,6713,6714,6715,6716,6717,6718,6719,6720,6721,6722,6723,6724,6725,6726,6727,6728,6729,6730,6731,6732,6733,6734,6735,6736,6737,6738,6739,6740,6823,6917,6918,6919,6920,6921,6922,6923,6924,6925,6926,6927,6928,6929,6930,6931,6932,6933,6934,6935,6936,6937,6938,6939,6940,6941,6942,6943,6944,6945,6946,6947,6948,6949,6950,6951,6952,6953,6954,6955,6956,6957,6958,6959,6960,6961,6962,6963,6981,6982,6983,6984,6985,6986,6987,7043,7044,7045,7046,7047,7048,7049,7050,7051,7052,7053,7054,7055,7056,7057,7058,7059,7060,7061,7062,7063,7064,7065,7066,7067,7068,7069,7070,7071,7072,7086,7087,7098,7099,7100,7101,7102,7103,7104,7105,7106,7107,7108,7109,7110,7111,7112,7113,7114,7115,7116,7117,7118,7119,7120,7121,7122,7123,7124,7125,7126,7127,7128,7129,7130,7131,7132,7133,7134,7135,7136,7137,7138,7139,7140,7141,7168,7169,7170,7171,7172,7173,7174,7175,7176,7177,7178,7179,7180,7181,7182,7183,7184,7185,7186,7187,7188,7189,7190,7191,7192,7193,7194,7195,7196,7197,7198,7199,7200,7201,7202,7203,7245,7246,7247,7258,7259,7260,7261,7262,7263,7264,7265,7266,7267,7268,7269,7270,7271,7272,7273,7274,7275,7276,7277,7278,7279,7280,7281,7282,7283,7284,7285,7286,7287,7288,7289,7290,7291,7292,7293,7296,7297,7298,7299,7300,7301,7302,7303,7304,7312,7313,7314,7315,7316,7317,7318,7319,7320,7321,7322,7323,7324,7325,7326,7327,7328,7329,7330,7331,7332,7333,7334,7335,7336,7337,7338,7339,7340,7341,7342,7343,7344,7345,7346,7347,7348,7349,7350,7351,7352,7353,7354,7357,7358,7359,7401,7402,7403,7404,7406,7407,7408,7409,7413,7414,7424,7425,7426,7427,7428,7429,7430,7431,7432,7433,7434,7435,7436,7437,7438,7439,7440,7441,7442,7443,7444,7445,7446,7447,7448,7449,7450,7451,7452,7453,7454,7455,7456,7457,7458,7459,7460,7461,7462,7463,7464,7465,7466,7467,7468,7469,7470,7471,7472,7473,7474,7475,7476,7477,7478,7479,7480,7481,7482,7483,7484,7485,7486,7487,7488,7489,7490,7491,7492,7493,7494,7495,7496,7497,7498,7499,7500,7501,7502,7503,7504,7505,7506,7507,7508,7509,7510,7511,7512,7513,7514,7515,7516,7517,7518,7519,7520,7521,7522,7523,7524,7525,7526,7527,7528,7529,7530,7531,7532,7533,7534,7535,7536,7537,7538,7539,7540,7541,7542,7543,7544,7545,7546,7547,7548,7549,7550,7551,7552,7553,7554,7555,7556,7557,7558,7559,7560,7561,7562,7563,7564,7565,7566,7567,7568,7569,7570,7571,7572,7573,7574,7575,7576,7577,7578,7579,7580,7581,7582,7583,7584,7585,7586,7587,7588,7589,7590,7591,7592,7593,7594,7595,7596,7597,7598,7599,7600,7601,7602,7603,7604,7605,7606,7607,7608,7609,7610,7611,7612,7613,7614,7615,7680,7681,7682,7683,7684,7685,7686,7687,7688,7689,7690,7691,7692,7693,7694,7695,7696,7697,7698,7699,7700,7701,7702,7703,7704,7705,7706,7707,7708,7709,7710,7711,7712,7713,7714,7715,7716,7717,7718,7719,7720,7721,7722,7723,7724,7725,7726,7727,7728,7729,7730,7731,7732,7733,7734,7735,7736,7737,7738,7739,7740,7741,7742,7743,7744,7745,7746,7747,7748,7749,7750,7751,7752,7753,7754,7755,7756,7757,7758,7759,7760,7761,7762,7763,7764,7765,7766,7767,7768,7769,7770,7771,7772,7773,7774,7775,7776,7777,7778,7779,7780,7781,7782,7783,7784,7785,7786,7787,7788,7789,7790,7791,7792,7793,7794,7795,7796,7797,7798,7799,7800,7801,7802,7803,7804,7805,7806,7807,7808,7809,7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823,7824,7825,7826,7827,7828,7829,7830,7831,7832,7833,7834,7835,7836,7837,7838,7839,7840,7841,7842,7843,7844,7845,7846,7847,7848,7849,7850,7851,7852,7853,7854,7855,7856,7857,7858,7859,7860,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870,7871,7872,7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886,7887,7888,7889,7890,7891,7892,7893,7894,7895,7896,7897,7898,7899,7900,7901,7902,7903,7904,7905,7906,7907,7908,7909,7910,7911,7912,7913,7914,7915,7916,7917,7918,7919,7920,7921,7922,7923,7924,7925,7926,7927,7928,7929,7930,7931,7932,7933,7934,7935,7936,7937,7938,7939,7940,7941,7942,7943,7944,7945,7946,7947,7948,7949,7950,7951,7952,7953,7954,7955,7956,7957,7960,7961,7962,7963,7964,7965,7968,7969,7970,7971,7972,7973,7974,7975,7976,7977,7978,7979,7980,7981,7982,7983,7984,7985,7986,7987,7988,7989,7990,7991,7992,7993,7994,7995,7996,7997,7998,7999,8000,8001,8002,8003,8004,8005,8008,8009,8010,8011,8012,8013,8016,8017,8018,8019,8020,8021,8022,8023,8025,8027,8029,8031,8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047,8048,8049,8050,8051,8052,8053,8054,8055,8056,8057,8058,8059,8060,8061,8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111,8112,8113,8114,8115,8116,8118,8119,8120,8121,8122,8123,8124,8126,8130,8131,8132,8134,8135,8136,8137,8138,8139,8140,8144,8145,8146,8147,8150,8151,8152,8153,8154,8155,8160,8161,8162,8163,8164,8165,8166,8167,8168,8169,8170,8171,8172,8178,8179,8180,8182,8183,8184,8185,8186,8187,8188,8305,8319,8336,8337,8338,8339,8340,8341,8342,8343,8344,8345,8346,8347,8348,8450,8455,8458,8459,8460,8461,8462,8463,8464,8465,8466,8467,8469,8472,8473,8474,8475,8476,8477,8484,8486,8488,8490,8491,8492,8493,8494,8495,8496,8497,8498,8499,8500,8501,8502,8503,8504,8505,8508,8509,8510,8511,8517,8518,8519,8520,8521,8526,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,8554,8555,8556,8557,8558,8559,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,8570,8571,8572,8573,8574,8575,8576,8577,8578,8579,8580,8581,8582,8583,8584,11264,11265,11266,11267,11268,11269,11270,11271,11272,11273,11274,11275,11276,11277,11278,11279,11280,11281,11282,11283,11284,11285,11286,11287,11288,11289,11290,11291,11292,11293,11294,11295,11296,11297,11298,11299,11300,11301,11302,11303,11304,11305,11306,11307,11308,11309,11310,11312,11313,11314,11315,11316,11317,11318,11319,11320,11321,11322,11323,11324,11325,11326,11327,11328,11329,11330,11331,11332,11333,11334,11335,11336,11337,11338,11339,11340,11341,11342,11343,11344,11345,11346,11347,11348,11349,11350,11351,11352,11353,11354,11355,11356,11357,11358,11360,11361,11362,11363,11364,11365,11366,11367,11368,11369,11370,11371,11372,11373,11374,11375,11376,11377,11378,11379,11380,11381,11382,11383,11384,11385,11386,11387,11388,11389,11390,11391,11392,11393,11394,11395,11396,11397,11398,11399,11400,11401,11402,11403,11404,11405,11406,11407,11408,11409,11410,11411,11412,11413,11414,11415,11416,11417,11418,11419,11420,11421,11422,11423,11424,11425,11426,11427,11428,11429,11430,11431,11432,11433,11434,11435,11436,11437,11438,11439,11440,11441,11442,11443,11444,11445,11446,11447,11448,11449,11450,11451,11452,11453,11454,11455,11456,11457,11458,11459,11460,11461,11462,11463,11464,11465,11466,11467,11468,11469,11470,11471,11472,11473,11474,11475,11476,11477,11478,11479,11480,11481,11482,11483,11484,11485,11486,11487,11488,11489,11490,11491,11492,11499,11500,11501,11502,11506,11507,11520,11521,11522,11523,11524,11525,11526,11527,11528,11529,11530,11531,11532,11533,11534,11535,11536,11537,11538,11539,11540,11541,11542,11543,11544,11545,11546,11547,11548,11549,11550,11551,11552,11553,11554,11555,11556,11557,11559,11565,11568,11569,11570,11571,11572,11573,11574,11575,11576,11577,11578,11579,11580,11581,11582,11583,11584,11585,11586,11587,11588,11589,11590,11591,11592,11593,11594,11595,11596,11597,11598,11599,11600,11601,11602,11603,11604,11605,11606,11607,11608,11609,11610,11611,11612,11613,11614,11615,11616,11617,11618,11619,11620,11621,11622,11623,11631,11648,11649,11650,11651,11652,11653,11654,11655,11656,11657,11658,11659,11660,11661,11662,11663,11664,11665,11666,11667,11668,11669,11670,11680,11681,11682,11683,11684,11685,11686,11688,11689,11690,11691,11692,11693,11694,11696,11697,11698,11699,11700,11701,11702,11704,11705,11706,11707,11708,11709,11710,11712,11713,11714,11715,11716,11717,11718,11720,11721,11722,11723,11724,11725,11726,11728,11729,11730,11731,11732,11733,11734,11736,11737,11738,11739,11740,11741,11742,12293,12294,12295,12321,12322,12323,12324,12325,12326,12327,12328,12329,12337,12338,12339,12340,12341,12344,12345,12346,12347,12348,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,12436,12437,12438,12443,12444,12445,12446,12447,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,12535,12536,12537,12538,12540,12541,12542,12543,12549,12550,12551,12552,12553,12554,12555,12556,12557,12558,12559,12560,12561,12562,12563,12564,12565,12566,12567,12568,12569,12570,12571,12572,12573,12574,12575,12576,12577,12578,12579,12580,12581,12582,12583,12584,12585,12586,12587,12588,12589,12590,12591,12593,12594,12595,12596,12597,12598,12599,12600,12601,12602,12603,12604,12605,12606,12607,12608,12609,12610,12611,12612,12613,12614,12615,12616,12617,12618,12619,12620,12621,12622,12623,12624,12625,12626,12627,12628,12629,12630,12631,12632,12633,12634,12635,12636,12637,12638,12639,12640,12641,12642,12643,12644,12645,12646,12647,12648,12649,12650,12651,12652,12653,12654,12655,12656,12657,12658,12659,12660,12661,12662,12663,12664,12665,12666,12667,12668,12669,12670,12671,12672,12673,12674,12675,12676,12677,12678,12679,12680,12681,12682,12683,12684,12685,12686,12704,12705,12706,12707,12708,12709,12710,12711,12712,12713,12714,12715,12716,12717,12718,12719,12720,12721,12722,12723,12724,12725,12726,12727,12728,12729,12730,12784,12785,12786,12787,12788,12789,12790,12791,12792,12793,12794,12795,12796,12797,12798,12799,13312,13313,13314,13315,13316,13317,13318,13319,13320,13321,13322,13323,13324,13325,13326,13327,13328,13329,13330,13331,13332,13333,13334,13335,13336,13337,13338,13339,13340,13341,13342,13343,13344,13345,13346,13347,13348,13349,13350,13351,13352,13353,13354,13355,13356,13357,13358,13359,13360,13361,13362,13363,13364,13365,13366,13367,13368,13369,13370,13371,13372,13373,13374,13375,13376,13377,13378,13379,13380,13381,13382,13383,13384,13385,13386,13387,13388,13389,13390,13391,13392,13393,13394,13395,13396,13397,13398,13399,13400,13401,13402,13403,13404,13405,13406,13407,13408,13409,13410,13411,13412,13413,13414,13415,13416,13417,13418,13419,13420,13421,13422,13423,13424,13425,13426,13427,13428,13429,13430,13431,13432,13433,13434,13435,13436,13437,13438,13439,13440,13441,13442,13443,13444,13445,13446,13447,13448,13449,13450,13451,13452,13453,13454,13455,13456,13457,13458,13459,13460,13461,13462,13463,13464,13465,13466,13467,13468,13469,13470,13471,13472,13473,13474,13475,13476,13477,13478,13479,13480,13481,13482,13483,13484,13485,13486,13487,13488,13489,13490,13491,13492,13493,13494,13495,13496,13497,13498,13499,13500,13501,13502,13503,13504,13505,13506,13507,13508,13509,13510,13511,13512,13513,13514,13515,13516,13517,13518,13519,13520,13521,13522,13523,13524,13525,13526,13527,13528,13529,13530,13531,13532,13533,13534,13535,13536,13537,13538,13539,13540,13541,13542,13543,13544,13545,13546,13547,13548,13549,13550,13551,13552,13553,13554,13555,13556,13557,13558,13559,13560,13561,13562,13563,13564,13565,13566,13567,13568,13569,13570,13571,13572,13573,13574,13575,13576,13577,13578,13579,13580,13581,13582,13583,13584,13585,13586,13587,13588,13589,13590,13591,13592,13593,13594,13595,13596,13597,13598,13599,13600,13601,13602,13603,13604,13605,13606,13607,13608,13609,13610,13611,13612,13613,13614,13615,13616,13617,13618,13619,13620,13621,13622,13623,13624,13625,13626,13627,13628,13629,13630,13631,13632,13633,13634,13635,13636,13637,13638,13639,13640,13641,13642,13643,13644,13645,13646,13647,13648,13649,13650,13651,13652,13653,13654,13655,13656,13657,13658,13659,13660,13661,13662,13663,13664,13665,13666,13667,13668,13669,13670,13671,13672,13673,13674,13675,13676,13677,13678,13679,13680,13681,13682,13683,13684,13685,13686,13687,13688,13689,13690,13691,13692,13693,13694,13695,13696,13697,13698,13699,13700,13701,13702,13703,13704,13705,13706,13707,13708,13709,13710,13711,13712,13713,13714,13715,13716,13717,13718,13719,13720,13721,13722,13723,13724,13725,13726,13727,13728,13729,13730,13731,13732,13733,13734,13735,13736,13737,13738,13739,13740,13741,13742,13743,13744,13745,13746,13747,13748,13749,13750,13751,13752,13753,13754,13755,13756,13757,13758,13759,13760,13761,13762,13763,13764,13765,13766,13767,13768,13769,13770,13771,13772,13773,13774,13775,13776,13777,13778,13779,13780,13781,13782,13783,13784,13785,13786,13787,13788,13789,13790,13791,13792,13793,13794,13795,13796,13797,13798,13799,13800,13801,13802,13803,13804,13805,13806,13807,13808,13809,13810,13811,13812,13813,13814,13815,13816,13817,13818,13819,13820,13821,13822,13823,13824,13825,13826,13827,13828,13829,13830,13831,13832,13833,13834,13835,13836,13837,13838,13839,13840,13841,13842,13843,13844,13845,13846,13847,13848,13849,13850,13851,13852,13853,13854,13855,13856,13857,13858,13859,13860,13861,13862,13863,13864,13865,13866,13867,13868,13869,13870,13871,13872,13873,13874,13875,13876,13877,13878,13879,13880,13881,13882,13883,13884,13885,13886,13887,13888,13889,13890,13891,13892,13893,13894,13895,13896,13897,13898,13899,13900,13901,13902,13903,13904,13905,13906,13907,13908,13909,13910,13911,13912,13913,13914,13915,13916,13917,13918,13919,13920,13921,13922,13923,13924,13925,13926,13927,13928,13929,13930,13931,13932,13933,13934,13935,13936,13937,13938,13939,13940,13941,13942,13943,13944,13945,13946,13947,13948,13949,13950,13951,13952,13953,13954,13955,13956,13957,13958,13959,13960,13961,13962,13963,13964,13965,13966,13967,13968,13969,13970,13971,13972,13973,13974,13975,13976,13977,13978,13979,13980,13981,13982,13983,13984,13985,13986,13987,13988,13989,13990,13991,13992,13993,13994,13995,13996,13997,13998,13999,14000,14001,14002,14003,14004,14005,14006,14007,14008,14009,14010,14011,14012,14013,14014,14015,14016,14017,14018,14019,14020,14021,14022,14023,14024,14025,14026,14027,14028,14029,14030,14031,14032,14033,14034,14035,14036,14037,14038,14039,14040,14041,14042,14043,14044,14045,14046,14047,14048,14049,14050,14051,14052,14053,14054,14055,14056,14057,14058,14059,14060,14061,14062,14063,14064,14065,14066,14067,14068,14069,14070,14071,14072,14073,14074,14075,14076,14077,14078,14079,14080,14081,14082,14083,14084,14085,14086,14087,14088,14089,14090,14091,14092,14093,14094,14095,14096,14097,14098,14099,14100,14101,14102,14103,14104,14105,14106,14107,14108,14109,14110,14111,14112,14113,14114,14115,14116,14117,14118,14119,14120,14121,14122,14123,14124,14125,14126,14127,14128,14129,14130,14131,14132,14133,14134,14135,14136,14137,14138,14139,14140,14141,14142,14143,14144,14145,14146,14147,14148,14149,14150,14151,14152,14153,14154,14155,14156,14157,14158,14159,14160,14161,14162,14163,14164,14165,14166,14167,14168,14169,14170,14171,14172,14173,14174,14175,14176,14177,14178,14179,14180,14181,14182,14183,14184,14185,14186,14187,14188,14189,14190,14191,14192,14193,14194,14195,14196,14197,14198,14199,14200,14201,14202,14203,14204,14205,14206,14207,14208,14209,14210,14211,14212,14213,14214,14215,14216,14217,14218,14219,14220,14221,14222,14223,14224,14225,14226,14227,14228,14229,14230,14231,14232,14233,14234,14235,14236,14237,14238,14239,14240,14241,14242,14243,14244,14245,14246,14247,14248,14249,14250,14251,14252,14253,14254,14255,14256,14257,14258,14259,14260,14261,14262,14263,14264,14265,14266,14267,14268,14269,14270,14271,14272,14273,14274,14275,14276,14277,14278,14279,14280,14281,14282,14283,14284,14285,14286,14287,14288,14289,14290,14291,14292,14293,14294,14295,14296,14297,14298,14299,14300,14301,14302,14303,14304,14305,14306,14307,14308,14309,14310,14311,14312,14313,14314,14315,14316,14317,14318,14319,14320,14321,14322,14323,14324,14325,14326,14327,14328,14329,14330,14331,14332,14333,14334,14335,14336,14337,14338,14339,14340,14341,14342,14343,14344,14345,14346,14347,14348,14349,14350,14351,14352,14353,14354,14355,14356,14357,14358,14359,14360,14361,14362,14363,14364,14365,14366,14367,14368,14369,14370,14371,14372,14373,14374,14375,14376,14377,14378,14379,14380,14381,14382,14383,14384,14385,14386,14387,14388,14389,14390,14391,14392,14393,14394,14395,14396,14397,14398,14399,14400,14401,14402,14403,14404,14405,14406,14407,14408,14409,14410,14411,14412,14413,14414,14415,14416,14417,14418,14419,14420,14421,14422,14423,14424,14425,14426,14427,14428,14429,14430,14431,14432,14433,14434,14435,14436,14437,14438,14439,14440,14441,14442,14443,14444,14445,14446,14447,14448,14449,14450,14451,14452,14453,14454,14455,14456,14457,14458,14459,14460,14461,14462,14463,14464,14465,14466,14467,14468,14469,14470,14471,14472,14473,14474,14475,14476,14477,14478,14479,14480,14481,14482,14483,14484,14485,14486,14487,14488,14489,14490,14491,14492,14493,14494,14495,14496,14497,14498,14499,14500,14501,14502,14503,14504,14505,14506,14507,14508,14509,14510,14511,14512,14513,14514,14515,14516,14517,14518,14519,14520,14521,14522,14523,14524,14525,14526,14527,14528,14529,14530,14531,14532,14533,14534,14535,14536,14537,14538,14539,14540,14541,14542,14543,14544,14545,14546,14547,14548,14549,14550,14551,14552,14553,14554,14555,14556,14557,14558,14559,14560,14561,14562,14563,14564,14565,14566,14567,14568,14569,14570,14571,14572,14573,14574,14575,14576,14577,14578,14579,14580,14581,14582,14583,14584,14585,14586,14587,14588,14589,14590,14591,14592,14593,14594,14595,14596,14597,14598,14599,14600,14601,14602,14603,14604,14605,14606,14607,14608,14609,14610,14611,14612,14613,14614,14615,14616,14617,14618,14619,14620,14621,14622,14623,14624,14625,14626,14627,14628,14629,14630,14631,14632,14633,14634,14635,14636,14637,14638,14639,14640,14641,14642,14643,14644,14645,14646,14647,14648,14649,14650,14651,14652,14653,14654,14655,14656,14657,14658,14659,14660,14661,14662,14663,14664,14665,14666,14667,14668,14669,14670,14671,14672,14673,14674,14675,14676,14677,14678,14679,14680,14681,14682,14683,14684,14685,14686,14687,14688,14689,14690,14691,14692,14693,14694,14695,14696,14697,14698,14699,14700,14701,14702,14703,14704,14705,14706,14707,14708,14709,14710,14711,14712,14713,14714,14715,14716,14717,14718,14719,14720,14721,14722,14723,14724,14725,14726,14727,14728,14729,14730,14731,14732,14733,14734,14735,14736,14737,14738,14739,14740,14741,14742,14743,14744,14745,14746,14747,14748,14749,14750,14751,14752,14753,14754,14755,14756,14757,14758,14759,14760,14761,14762,14763,14764,14765,14766,14767,14768,14769,14770,14771,14772,14773,14774,14775,14776,14777,14778,14779,14780,14781,14782,14783,14784,14785,14786,14787,14788,14789,14790,14791,14792,14793,14794,14795,14796,14797,14798,14799,14800,14801,14802,14803,14804,14805,14806,14807,14808,14809,14810,14811,14812,14813,14814,14815,14816,14817,14818,14819,14820,14821,14822,14823,14824,14825,14826,14827,14828,14829,14830,14831,14832,14833,14834,14835,14836,14837,14838,14839,14840,14841,14842,14843,14844,14845,14846,14847,14848,14849,14850,14851,14852,14853,14854,14855,14856,14857,14858,14859,14860,14861,14862,14863,14864,14865,14866,14867,14868,14869,14870,14871,14872,14873,14874,14875,14876,14877,14878,14879,14880,14881,14882,14883,14884,14885,14886,14887,14888,14889,14890,14891,14892,14893,14894,14895,14896,14897,14898,14899,14900,14901,14902,14903,14904,14905,14906,14907,14908,14909,14910,14911,14912,14913,14914,14915,14916,14917,14918,14919,14920,14921,14922,14923,14924,14925,14926,14927,14928,14929,14930,14931,14932,14933,14934,14935,14936,14937,14938,14939,14940,14941,14942,14943,14944,14945,14946,14947,14948,14949,14950,14951,14952,14953,14954,14955,14956,14957,14958,14959,14960,14961,14962,14963,14964,14965,14966,14967,14968,14969,14970,14971,14972,14973,14974,14975,14976,14977,14978,14979,14980,14981,14982,14983,14984,14985,14986,14987,14988,14989,14990,14991,14992,14993,14994,14995,14996,14997,14998,14999,15000,15001,15002,15003,15004,15005,15006,15007,15008,15009,15010,15011,15012,15013,15014,15015,15016,15017,15018,15019,15020,15021,15022,15023,15024,15025,15026,15027,15028,15029,15030,15031,15032,15033,15034,15035,15036,15037,15038,15039,15040,15041,15042,15043,15044,15045,15046,15047,15048,15049,15050,15051,15052,15053,15054,15055,15056,15057,15058,15059,15060,15061,15062,15063,15064,15065,15066,15067,15068,15069,15070,15071,15072,15073,15074,15075,15076,15077,15078,15079,15080,15081,15082,15083,15084,15085,15086,15087,15088,15089,15090,15091,15092,15093,15094,15095,15096,15097,15098,15099,15100,15101,15102,15103,15104,15105,15106,15107,15108,15109,15110,15111,15112,15113,15114,15115,15116,15117,15118,15119,15120,15121,15122,15123,15124,15125,15126,15127,15128,15129,15130,15131,15132,15133,15134,15135,15136,15137,15138,15139,15140,15141,15142,15143,15144,15145,15146,15147,15148,15149,15150,15151,15152,15153,15154,15155,15156,15157,15158,15159,15160,15161,15162,15163,15164,15165,15166,15167,15168,15169,15170,15171,15172,15173,15174,15175,15176,15177,15178,15179,15180,15181,15182,15183,15184,15185,15186,15187,15188,15189,15190,15191,15192,15193,15194,15195,15196,15197,15198,15199,15200,15201,15202,15203,15204,15205,15206,15207,15208,15209,15210,15211,15212,15213,15214,15215,15216,15217,15218,15219,15220,15221,15222,15223,15224,15225,15226,15227,15228,15229,15230,15231,15232,15233,15234,15235,15236,15237,15238,15239,15240,15241,15242,15243,15244,15245,15246,15247,15248,15249,15250,15251,15252,15253,15254,15255,15256,15257,15258,15259,15260,15261,15262,15263,15264,15265,15266,15267,15268,15269,15270,15271,15272,15273,15274,15275,15276,15277,15278,15279,15280,15281,15282,15283,15284,15285,15286,15287,15288,15289,15290,15291,15292,15293,15294,15295,15296,15297,15298,15299,15300,15301,15302,15303,15304,15305,15306,15307,15308,15309,15310,15311,15312,15313,15314,15315,15316,15317,15318,15319,15320,15321,15322,15323,15324,15325,15326,15327,15328,15329,15330,15331,15332,15333,15334,15335,15336,15337,15338,15339,15340,15341,15342,15343,15344,15345,15346,15347,15348,15349,15350,15351,15352,15353,15354,15355,15356,15357,15358,15359,15360,15361,15362,15363,15364,15365,15366,15367,15368,15369,15370,15371,15372,15373,15374,15375,15376,15377,15378,15379,15380,15381,15382,15383,15384,15385,15386,15387,15388,15389,15390,15391,15392,15393,15394,15395,15396,15397,15398,15399,15400,15401,15402,15403,15404,15405,15406,15407,15408,15409,15410,15411,15412,15413,15414,15415,15416,15417,15418,15419,15420,15421,15422,15423,15424,15425,15426,15427,15428,15429,15430,15431,15432,15433,15434,15435,15436,15437,15438,15439,15440,15441,15442,15443,15444,15445,15446,15447,15448,15449,15450,15451,15452,15453,15454,15455,15456,15457,15458,15459,15460,15461,15462,15463,15464,15465,15466,15467,15468,15469,15470,15471,15472,15473,15474,15475,15476,15477,15478,15479,15480,15481,15482,15483,15484,15485,15486,15487,15488,15489,15490,15491,15492,15493,15494,15495,15496,15497,15498,15499,15500,15501,15502,15503,15504,15505,15506,15507,15508,15509,15510,15511,15512,15513,15514,15515,15516,15517,15518,15519,15520,15521,15522,15523,15524,15525,15526,15527,15528,15529,15530,15531,15532,15533,15534,15535,15536,15537,15538,15539,15540,15541,15542,15543,15544,15545,15546,15547,15548,15549,15550,15551,15552,15553,15554,15555,15556,15557,15558,15559,15560,15561,15562,15563,15564,15565,15566,15567,15568,15569,15570,15571,15572,15573,15574,15575,15576,15577,15578,15579,15580,15581,15582,15583,15584,15585,15586,15587,15588,15589,15590,15591,15592,15593,15594,15595,15596,15597,15598,15599,15600,15601,15602,15603,15604,15605,15606,15607,15608,15609,15610,15611,15612,15613,15614,15615,15616,15617,15618,15619,15620,15621,15622,15623,15624,15625,15626,15627,15628,15629,15630,15631,15632,15633,15634,15635,15636,15637,15638,15639,15640,15641,15642,15643,15644,15645,15646,15647,15648,15649,15650,15651,15652,15653,15654,15655,15656,15657,15658,15659,15660,15661,15662,15663,15664,15665,15666,15667,15668,15669,15670,15671,15672,15673,15674,15675,15676,15677,15678,15679,15680,15681,15682,15683,15684,15685,15686,15687,15688,15689,15690,15691,15692,15693,15694,15695,15696,15697,15698,15699,15700,15701,15702,15703,15704,15705,15706,15707,15708,15709,15710,15711,15712,15713,15714,15715,15716,15717,15718,15719,15720,15721,15722,15723,15724,15725,15726,15727,15728,15729,15730,15731,15732,15733,15734,15735,15736,15737,15738,15739,15740,15741,15742,15743,15744,15745,15746,15747,15748,15749,15750,15751,15752,15753,15754,15755,15756,15757,15758,15759,15760,15761,15762,15763,15764,15765,15766,15767,15768,15769,15770,15771,15772,15773,15774,15775,15776,15777,15778,15779,15780,15781,15782,15783,15784,15785,15786,15787,15788,15789,15790,15791,15792,15793,15794,15795,15796,15797,15798,15799,15800,15801,15802,15803,15804,15805,15806,15807,15808,15809,15810,15811,15812,15813,15814,15815,15816,15817,15818,15819,15820,15821,15822,15823,15824,15825,15826,15827,15828,15829,15830,15831,15832,15833,15834,15835,15836,15837,15838,15839,15840,15841,15842,15843,15844,15845,15846,15847,15848,15849,15850,15851,15852,15853,15854,15855,15856,15857,15858,15859,15860,15861,15862,15863,15864,15865,15866,15867,15868,15869,15870,15871,15872,15873,15874,15875,15876,15877,15878,15879,15880,15881,15882,15883,15884,15885,15886,15887,15888,15889,15890,15891,15892,15893,15894,15895,15896,15897,15898,15899,15900,15901,15902,15903,15904,15905,15906,15907,15908,15909,15910,15911,15912,15913,15914,15915,15916,15917,15918,15919,15920,15921,15922,15923,15924,15925,15926,15927,15928,15929,15930,15931,15932,15933,15934,15935,15936,15937,15938,15939,15940,15941,15942,15943,15944,15945,15946,15947,15948,15949,15950,15951,15952,15953,15954,15955,15956,15957,15958,15959,15960,15961,15962,15963,15964,15965,15966,15967,15968,15969,15970,15971,15972,15973,15974,15975,15976,15977,15978,15979,15980,15981,15982,15983,15984,15985,15986,15987,15988,15989,15990,15991,15992,15993,15994,15995,15996,15997,15998,15999,16000,16001,16002,16003,16004,16005,16006,16007,16008,16009,16010,16011,16012,16013,16014,16015,16016,16017,16018,16019,16020,16021,16022,16023,16024,16025,16026,16027,16028,16029,16030,16031,16032,16033,16034,16035,16036,16037,16038,16039,16040,16041,16042,16043,16044,16045,16046,16047,16048,16049,16050,16051,16052,16053,16054,16055,16056,16057,16058,16059,16060,16061,16062,16063,16064,16065,16066,16067,16068,16069,16070,16071,16072,16073,16074,16075,16076,16077,16078,16079,16080,16081,16082,16083,16084,16085,16086,16087,16088,16089,16090,16091,16092,16093,16094,16095,16096,16097,16098,16099,16100,16101,16102,16103,16104,16105,16106,16107,16108,16109,16110,16111,16112,16113,16114,16115,16116,16117,16118,16119,16120,16121,16122,16123,16124,16125,16126,16127,16128,16129,16130,16131,16132,16133,16134,16135,16136,16137,16138,16139,16140,16141,16142,16143,16144,16145,16146,16147,16148,16149,16150,16151,16152,16153,16154,16155,16156,16157,16158,16159,16160,16161,16162,16163,16164,16165,16166,16167,16168,16169,16170,16171,16172,16173,16174,16175,16176,16177,16178,16179,16180,16181,16182,16183,16184,16185,16186,16187,16188,16189,16190,16191,16192,16193,16194,16195,16196,16197,16198,16199,16200,16201,16202,16203,16204,16205,16206,16207,16208,16209,16210,16211,16212,16213,16214,16215,16216,16217,16218,16219,16220,16221,16222,16223,16224,16225,16226,16227,16228,16229,16230,16231,16232,16233,16234,16235,16236,16237,16238,16239,16240,16241,16242,16243,16244,16245,16246,16247,16248,16249,16250,16251,16252,16253,16254,16255,16256,16257,16258,16259,16260,16261,16262,16263,16264,16265,16266,16267,16268,16269,16270,16271,16272,16273,16274,16275,16276,16277,16278,16279,16280,16281,16282,16283,16284,16285,16286,16287,16288,16289,16290,16291,16292,16293,16294,16295,16296,16297,16298,16299,16300,16301,16302,16303,16304,16305,16306,16307,16308,16309,16310,16311,16312,16313,16314,16315,16316,16317,16318,16319,16320,16321,16322,16323,16324,16325,16326,16327,16328,16329,16330,16331,16332,16333,16334,16335,16336,16337,16338,16339,16340,16341,16342,16343,16344,16345,16346,16347,16348,16349,16350,16351,16352,16353,16354,16355,16356,16357,16358,16359,16360,16361,16362,16363,16364,16365,16366,16367,16368,16369,16370,16371,16372,16373,16374,16375,16376,16377,16378,16379,16380,16381,16382,16383,16384,16385,16386,16387,16388,16389,16390,16391,16392,16393,16394,16395,16396,16397,16398,16399,16400,16401,16402,16403,16404,16405,16406,16407,16408,16409,16410,16411,16412,16413,16414,16415,16416,16417,16418,16419,16420,16421,16422,16423,16424,16425,16426,16427,16428,16429,16430,16431,16432,16433,16434,16435,16436,16437,16438,16439,16440,16441,16442,16443,16444,16445,16446,16447,16448,16449,16450,16451,16452,16453,16454,16455,16456,16457,16458,16459,16460,16461,16462,16463,16464,16465,16466,16467,16468,16469,16470,16471,16472,16473,16474,16475,16476,16477,16478,16479,16480,16481,16482,16483,16484,16485,16486,16487,16488,16489,16490,16491,16492,16493,16494,16495,16496,16497,16498,16499,16500,16501,16502,16503,16504,16505,16506,16507,16508,16509,16510,16511,16512,16513,16514,16515,16516,16517,16518,16519,16520,16521,16522,16523,16524,16525,16526,16527,16528,16529,16530,16531,16532,16533,16534,16535,16536,16537,16538,16539,16540,16541,16542,16543,16544,16545,16546,16547,16548,16549,16550,16551,16552,16553,16554,16555,16556,16557,16558,16559,16560,16561,16562,16563,16564,16565,16566,16567,16568,16569,16570,16571,16572,16573,16574,16575,16576,16577,16578,16579,16580,16581,16582,16583,16584,16585,16586,16587,16588,16589,16590,16591,16592,16593,16594,16595,16596,16597,16598,16599,16600,16601,16602,16603,16604,16605,16606,16607,16608,16609,16610,16611,16612,16613,16614,16615,16616,16617,16618,16619,16620,16621,16622,16623,16624,16625,16626,16627,16628,16629,16630,16631,16632,16633,16634,16635,16636,16637,16638,16639,16640,16641,16642,16643,16644,16645,16646,16647,16648,16649,16650,16651,16652,16653,16654,16655,16656,16657,16658,16659,16660,16661,16662,16663,16664,16665,16666,16667,16668,16669,16670,16671,16672,16673,16674,16675,16676,16677,16678,16679,16680,16681,16682,16683,16684,16685,16686,16687,16688,16689,16690,16691,16692,16693,16694,16695,16696,16697,16698,16699,16700,16701,16702,16703,16704,16705,16706,16707,16708,16709,16710,16711,16712,16713,16714,16715,16716,16717,16718,16719,16720,16721,16722,16723,16724,16725,16726,16727,16728,16729,16730,16731,16732,16733,16734,16735,16736,16737,16738,16739,16740,16741,16742,16743,16744,16745,16746,16747,16748,16749,16750,16751,16752,16753,16754,16755,16756,16757,16758,16759,16760,16761,16762,16763,16764,16765,16766,16767,16768,16769,16770,16771,16772,16773,16774,16775,16776,16777,16778,16779,16780,16781,16782,16783,16784,16785,16786,16787,16788,16789,16790,16791,16792,16793,16794,16795,16796,16797,16798,16799,16800,16801,16802,16803,16804,16805,16806,16807,16808,16809,16810,16811,16812,16813,16814,16815,16816,16817,16818,16819,16820,16821,16822,16823,16824,16825,16826,16827,16828,16829,16830,16831,16832,16833,16834,16835,16836,16837,16838,16839,16840,16841,16842,16843,16844,16845,16846,16847,16848,16849,16850,16851,16852,16853,16854,16855,16856,16857,16858,16859,16860,16861,16862,16863,16864,16865,16866,16867,16868,16869,16870,16871,16872,16873,16874,16875,16876,16877,16878,16879,16880,16881,16882,16883,16884,16885,16886,16887,16888,16889,16890,16891,16892,16893,16894,16895,16896,16897,16898,16899,16900,16901,16902,16903,16904,16905,16906,16907,16908,16909,16910,16911,16912,16913,16914,16915,16916,16917,16918,16919,16920,16921,16922,16923,16924,16925,16926,16927,16928,16929,16930,16931,16932,16933,16934,16935,16936,16937,16938,16939,16940,16941,16942,16943,16944,16945,16946,16947,16948,16949,16950,16951,16952,16953,16954,16955,16956,16957,16958,16959,16960,16961,16962,16963,16964,16965,16966,16967,16968,16969,16970,16971,16972,16973,16974,16975,16976,16977,16978,16979,16980,16981,16982,16983,16984,16985,16986,16987,16988,16989,16990,16991,16992,16993,16994,16995,16996,16997,16998,16999,17000,17001,17002,17003,17004,17005,17006,17007,17008,17009,17010,17011,17012,17013,17014,17015,17016,17017,17018,17019,17020,17021,17022,17023,17024,17025,17026,17027,17028,17029,17030,17031,17032,17033,17034,17035,17036,17037,17038,17039,17040,17041,17042,17043,17044,17045,17046,17047,17048,17049,17050,17051,17052,17053,17054,17055,17056,17057,17058,17059,17060,17061,17062,17063,17064,17065,17066,17067,17068,17069,17070,17071,17072,17073,17074,17075,17076,17077,17078,17079,17080,17081,17082,17083,17084,17085,17086,17087,17088,17089,17090,17091,17092,17093,17094,17095,17096,17097,17098,17099,17100,17101,17102,17103,17104,17105,17106,17107,17108,17109,17110,17111,17112,17113,17114,17115,17116,17117,17118,17119,17120,17121,17122,17123,17124,17125,17126,17127,17128,17129,17130,17131,17132,17133,17134,17135,17136,17137,17138,17139,17140,17141,17142,17143,17144,17145,17146,17147,17148,17149,17150,17151,17152,17153,17154,17155,17156,17157,17158,17159,17160,17161,17162,17163,17164,17165,17166,17167,17168,17169,17170,17171,17172,17173,17174,17175,17176,17177,17178,17179,17180,17181,17182,17183,17184,17185,17186,17187,17188,17189,17190,17191,17192,17193,17194,17195,17196,17197,17198,17199,17200,17201,17202,17203,17204,17205,17206,17207,17208,17209,17210,17211,17212,17213,17214,17215,17216,17217,17218,17219,17220,17221,17222,17223,17224,17225,17226,17227,17228,17229,17230,17231,17232,17233,17234,17235,17236,17237,17238,17239,17240,17241,17242,17243,17244,17245,17246,17247,17248,17249,17250,17251,17252,17253,17254,17255,17256,17257,17258,17259,17260,17261,17262,17263,17264,17265,17266,17267,17268,17269,17270,17271,17272,17273,17274,17275,17276,17277,17278,17279,17280,17281,17282,17283,17284,17285,17286,17287,17288,17289,17290,17291,17292,17293,17294,17295,17296,17297,17298,17299,17300,17301,17302,17303,17304,17305,17306,17307,17308,17309,17310,17311,17312,17313,17314,17315,17316,17317,17318,17319,17320,17321,17322,17323,17324,17325,17326,17327,17328,17329,17330,17331,17332,17333,17334,17335,17336,17337,17338,17339,17340,17341,17342,17343,17344,17345,17346,17347,17348,17349,17350,17351,17352,17353,17354,17355,17356,17357,17358,17359,17360,17361,17362,17363,17364,17365,17366,17367,17368,17369,17370,17371,17372,17373,17374,17375,17376,17377,17378,17379,17380,17381,17382,17383,17384,17385,17386,17387,17388,17389,17390,17391,17392,17393,17394,17395,17396,17397,17398,17399,17400,17401,17402,17403,17404,17405,17406,17407,17408,17409,17410,17411,17412,17413,17414,17415,17416,17417,17418,17419,17420,17421,17422,17423,17424,17425,17426,17427,17428,17429,17430,17431,17432,17433,17434,17435,17436,17437,17438,17439,17440,17441,17442,17443,17444,17445,17446,17447,17448,17449,17450,17451,17452,17453,17454,17455,17456,17457,17458,17459,17460,17461,17462,17463,17464,17465,17466,17467,17468,17469,17470,17471,17472,17473,17474,17475,17476,17477,17478,17479,17480,17481,17482,17483,17484,17485,17486,17487,17488,17489,17490,17491,17492,17493,17494,17495,17496,17497,17498,17499,17500,17501,17502,17503,17504,17505,17506,17507,17508,17509,17510,17511,17512,17513,17514,17515,17516,17517,17518,17519,17520,17521,17522,17523,17524,17525,17526,17527,17528,17529,17530,17531,17532,17533,17534,17535,17536,17537,17538,17539,17540,17541,17542,17543,17544,17545,17546,17547,17548,17549,17550,17551,17552,17553,17554,17555,17556,17557,17558,17559,17560,17561,17562,17563,17564,17565,17566,17567,17568,17569,17570,17571,17572,17573,17574,17575,17576,17577,17578,17579,17580,17581,17582,17583,17584,17585,17586,17587,17588,17589,17590,17591,17592,17593,17594,17595,17596,17597,17598,17599,17600,17601,17602,17603,17604,17605,17606,17607,17608,17609,17610,17611,17612,17613,17614,17615,17616,17617,17618,17619,17620,17621,17622,17623,17624,17625,17626,17627,17628,17629,17630,17631,17632,17633,17634,17635,17636,17637,17638,17639,17640,17641,17642,17643,17644,17645,17646,17647,17648,17649,17650,17651,17652,17653,17654,17655,17656,17657,17658,17659,17660,17661,17662,17663,17664,17665,17666,17667,17668,17669,17670,17671,17672,17673,17674,17675,17676,17677,17678,17679,17680,17681,17682,17683,17684,17685,17686,17687,17688,17689,17690,17691,17692,17693,17694,17695,17696,17697,17698,17699,17700,17701,17702,17703,17704,17705,17706,17707,17708,17709,17710,17711,17712,17713,17714,17715,17716,17717,17718,17719,17720,17721,17722,17723,17724,17725,17726,17727,17728,17729,17730,17731,17732,17733,17734,17735,17736,17737,17738,17739,17740,17741,17742,17743,17744,17745,17746,17747,17748,17749,17750,17751,17752,17753,17754,17755,17756,17757,17758,17759,17760,17761,17762,17763,17764,17765,17766,17767,17768,17769,17770,17771,17772,17773,17774,17775,17776,17777,17778,17779,17780,17781,17782,17783,17784,17785,17786,17787,17788,17789,17790,17791,17792,17793,17794,17795,17796,17797,17798,17799,17800,17801,17802,17803,17804,17805,17806,17807,17808,17809,17810,17811,17812,17813,17814,17815,17816,17817,17818,17819,17820,17821,17822,17823,17824,17825,17826,17827,17828,17829,17830,17831,17832,17833,17834,17835,17836,17837,17838,17839,17840,17841,17842,17843,17844,17845,17846,17847,17848,17849,17850,17851,17852,17853,17854,17855,17856,17857,17858,17859,17860,17861,17862,17863,17864,17865,17866,17867,17868,17869,17870,17871,17872,17873,17874,17875,17876,17877,17878,17879,17880,17881,17882,17883,17884,17885,17886,17887,17888,17889,17890,17891,17892,17893,17894,17895,17896,17897,17898,17899,17900,17901,17902,17903,17904,17905,17906,17907,17908,17909,17910,17911,17912,17913,17914,17915,17916,17917,17918,17919,17920,17921,17922,17923,17924,17925,17926,17927,17928,17929,17930,17931,17932,17933,17934,17935,17936,17937,17938,17939,17940,17941,17942,17943,17944,17945,17946,17947,17948,17949,17950,17951,17952,17953,17954,17955,17956,17957,17958,17959,17960,17961,17962,17963,17964,17965,17966,17967,17968,17969,17970,17971,17972,17973,17974,17975,17976,17977,17978,17979,17980,17981,17982,17983,17984,17985,17986,17987,17988,17989,17990,17991,17992,17993,17994,17995,17996,17997,17998,17999,18000,18001,18002,18003,18004,18005,18006,18007,18008,18009,18010,18011,18012,18013,18014,18015,18016,18017,18018,18019,18020,18021,18022,18023,18024,18025,18026,18027,18028,18029,18030,18031,18032,18033,18034,18035,18036,18037,18038,18039,18040,18041,18042,18043,18044,18045,18046,18047,18048,18049,18050,18051,18052,18053,18054,18055,18056,18057,18058,18059,18060,18061,18062,18063,18064,18065,18066,18067,18068,18069,18070,18071,18072,18073,18074,18075,18076,18077,18078,18079,18080,18081,18082,18083,18084,18085,18086,18087,18088,18089,18090,18091,18092,18093,18094,18095,18096,18097,18098,18099,18100,18101,18102,18103,18104,18105,18106,18107,18108,18109,18110,18111,18112,18113,18114,18115,18116,18117,18118,18119,18120,18121,18122,18123,18124,18125,18126,18127,18128,18129,18130,18131,18132,18133,18134,18135,18136,18137,18138,18139,18140,18141,18142,18143,18144,18145,18146,18147,18148,18149,18150,18151,18152,18153,18154,18155,18156,18157,18158,18159,18160,18161,18162,18163,18164,18165,18166,18167,18168,18169,18170,18171,18172,18173,18174,18175,18176,18177,18178,18179,18180,18181,18182,18183,18184,18185,18186,18187,18188,18189,18190,18191,18192,18193,18194,18195,18196,18197,18198,18199,18200,18201,18202,18203,18204,18205,18206,18207,18208,18209,18210,18211,18212,18213,18214,18215,18216,18217,18218,18219,18220,18221,18222,18223,18224,18225,18226,18227,18228,18229,18230,18231,18232,18233,18234,18235,18236,18237,18238,18239,18240,18241,18242,18243,18244,18245,18246,18247,18248,18249,18250,18251,18252,18253,18254,18255,18256,18257,18258,18259,18260,18261,18262,18263,18264,18265,18266,18267,18268,18269,18270,18271,18272,18273,18274,18275,18276,18277,18278,18279,18280,18281,18282,18283,18284,18285,18286,18287,18288,18289,18290,18291,18292,18293,18294,18295,18296,18297,18298,18299,18300,18301,18302,18303,18304,18305,18306,18307,18308,18309,18310,18311,18312,18313,18314,18315,18316,18317,18318,18319,18320,18321,18322,18323,18324,18325,18326,18327,18328,18329,18330,18331,18332,18333,18334,18335,18336,18337,18338,18339,18340,18341,18342,18343,18344,18345,18346,18347,18348,18349,18350,18351,18352,18353,18354,18355,18356,18357,18358,18359,18360,18361,18362,18363,18364,18365,18366,18367,18368,18369,18370,18371,18372,18373,18374,18375,18376,18377,18378,18379,18380,18381,18382,18383,18384,18385,18386,18387,18388,18389,18390,18391,18392,18393,18394,18395,18396,18397,18398,18399,18400,18401,18402,18403,18404,18405,18406,18407,18408,18409,18410,18411,18412,18413,18414,18415,18416,18417,18418,18419,18420,18421,18422,18423,18424,18425,18426,18427,18428,18429,18430,18431,18432,18433,18434,18435,18436,18437,18438,18439,18440,18441,18442,18443,18444,18445,18446,18447,18448,18449,18450,18451,18452,18453,18454,18455,18456,18457,18458,18459,18460,18461,18462,18463,18464,18465,18466,18467,18468,18469,18470,18471,18472,18473,18474,18475,18476,18477,18478,18479,18480,18481,18482,18483,18484,18485,18486,18487,18488,18489,18490,18491,18492,18493,18494,18495,18496,18497,18498,18499,18500,18501,18502,18503,18504,18505,18506,18507,18508,18509,18510,18511,18512,18513,18514,18515,18516,18517,18518,18519,18520,18521,18522,18523,18524,18525,18526,18527,18528,18529,18530,18531,18532,18533,18534,18535,18536,18537,18538,18539,18540,18541,18542,18543,18544,18545,18546,18547,18548,18549,18550,18551,18552,18553,18554,18555,18556,18557,18558,18559,18560,18561,18562,18563,18564,18565,18566,18567,18568,18569,18570,18571,18572,18573,18574,18575,18576,18577,18578,18579,18580,18581,18582,18583,18584,18585,18586,18587,18588,18589,18590,18591,18592,18593,18594,18595,18596,18597,18598,18599,18600,18601,18602,18603,18604,18605,18606,18607,18608,18609,18610,18611,18612,18613,18614,18615,18616,18617,18618,18619,18620,18621,18622,18623,18624,18625,18626,18627,18628,18629,18630,18631,18632,18633,18634,18635,18636,18637,18638,18639,18640,18641,18642,18643,18644,18645,18646,18647,18648,18649,18650,18651,18652,18653,18654,18655,18656,18657,18658,18659,18660,18661,18662,18663,18664,18665,18666,18667,18668,18669,18670,18671,18672,18673,18674,18675,18676,18677,18678,18679,18680,18681,18682,18683,18684,18685,18686,18687,18688,18689,18690,18691,18692,18693,18694,18695,18696,18697,18698,18699,18700,18701,18702,18703,18704,18705,18706,18707,18708,18709,18710,18711,18712,18713,18714,18715,18716,18717,18718,18719,18720,18721,18722,18723,18724,18725,18726,18727,18728,18729,18730,18731,18732,18733,18734,18735,18736,18737,18738,18739,18740,18741,18742,18743,18744,18745,18746,18747,18748,18749,18750,18751,18752,18753,18754,18755,18756,18757,18758,18759,18760,18761,18762,18763,18764,18765,18766,18767,18768,18769,18770,18771,18772,18773,18774,18775,18776,18777,18778,18779,18780,18781,18782,18783,18784,18785,18786,18787,18788,18789,18790,18791,18792,18793,18794,18795,18796,18797,18798,18799,18800,18801,18802,18803,18804,18805,18806,18807,18808,18809,18810,18811,18812,18813,18814,18815,18816,18817,18818,18819,18820,18821,18822,18823,18824,18825,18826,18827,18828,18829,18830,18831,18832,18833,18834,18835,18836,18837,18838,18839,18840,18841,18842,18843,18844,18845,18846,18847,18848,18849,18850,18851,18852,18853,18854,18855,18856,18857,18858,18859,18860,18861,18862,18863,18864,18865,18866,18867,18868,18869,18870,18871,18872,18873,18874,18875,18876,18877,18878,18879,18880,18881,18882,18883,18884,18885,18886,18887,18888,18889,18890,18891,18892,18893,18894,18895,18896,18897,18898,18899,18900,18901,18902,18903,18904,18905,18906,18907,18908,18909,18910,18911,18912,18913,18914,18915,18916,18917,18918,18919,18920,18921,18922,18923,18924,18925,18926,18927,18928,18929,18930,18931,18932,18933,18934,18935,18936,18937,18938,18939,18940,18941,18942,18943,18944,18945,18946,18947,18948,18949,18950,18951,18952,18953,18954,18955,18956,18957,18958,18959,18960,18961,18962,18963,18964,18965,18966,18967,18968,18969,18970,18971,18972,18973,18974,18975,18976,18977,18978,18979,18980,18981,18982,18983,18984,18985,18986,18987,18988,18989,18990,18991,18992,18993,18994,18995,18996,18997,18998,18999,19000,19001,19002,19003,19004,19005,19006,19007,19008,19009,19010,19011,19012,19013,19014,19015,19016,19017,19018,19019,19020,19021,19022,19023,19024,19025,19026,19027,19028,19029,19030,19031,19032,19033,19034,19035,19036,19037,19038,19039,19040,19041,19042,19043,19044,19045,19046,19047,19048,19049,19050,19051,19052,19053,19054,19055,19056,19057,19058,19059,19060,19061,19062,19063,19064,19065,19066,19067,19068,19069,19070,19071,19072,19073,19074,19075,19076,19077,19078,19079,19080,19081,19082,19083,19084,19085,19086,19087,19088,19089,19090,19091,19092,19093,19094,19095,19096,19097,19098,19099,19100,19101,19102,19103,19104,19105,19106,19107,19108,19109,19110,19111,19112,19113,19114,19115,19116,19117,19118,19119,19120,19121,19122,19123,19124,19125,19126,19127,19128,19129,19130,19131,19132,19133,19134,19135,19136,19137,19138,19139,19140,19141,19142,19143,19144,19145,19146,19147,19148,19149,19150,19151,19152,19153,19154,19155,19156,19157,19158,19159,19160,19161,19162,19163,19164,19165,19166,19167,19168,19169,19170,19171,19172,19173,19174,19175,19176,19177,19178,19179,19180,19181,19182,19183,19184,19185,19186,19187,19188,19189,19190,19191,19192,19193,19194,19195,19196,19197,19198,19199,19200,19201,19202,19203,19204,19205,19206,19207,19208,19209,19210,19211,19212,19213,19214,19215,19216,19217,19218,19219,19220,19221,19222,19223,19224,19225,19226,19227,19228,19229,19230,19231,19232,19233,19234,19235,19236,19237,19238,19239,19240,19241,19242,19243,19244,19245,19246,19247,19248,19249,19250,19251,19252,19253,19254,19255,19256,19257,19258,19259,19260,19261,19262,19263,19264,19265,19266,19267,19268,19269,19270,19271,19272,19273,19274,19275,19276,19277,19278,19279,19280,19281,19282,19283,19284,19285,19286,19287,19288,19289,19290,19291,19292,19293,19294,19295,19296,19297,19298,19299,19300,19301,19302,19303,19304,19305,19306,19307,19308,19309,19310,19311,19312,19313,19314,19315,19316,19317,19318,19319,19320,19321,19322,19323,19324,19325,19326,19327,19328,19329,19330,19331,19332,19333,19334,19335,19336,19337,19338,19339,19340,19341,19342,19343,19344,19345,19346,19347,19348,19349,19350,19351,19352,19353,19354,19355,19356,19357,19358,19359,19360,19361,19362,19363,19364,19365,19366,19367,19368,19369,19370,19371,19372,19373,19374,19375,19376,19377,19378,19379,19380,19381,19382,19383,19384,19385,19386,19387,19388,19389,19390,19391,19392,19393,19394,19395,19396,19397,19398,19399,19400,19401,19402,19403,19404,19405,19406,19407,19408,19409,19410,19411,19412,19413,19414,19415,19416,19417,19418,19419,19420,19421,19422,19423,19424,19425,19426,19427,19428,19429,19430,19431,19432,19433,19434,19435,19436,19437,19438,19439,19440,19441,19442,19443,19444,19445,19446,19447,19448,19449,19450,19451,19452,19453,19454,19455,19456,19457,19458,19459,19460,19461,19462,19463,19464,19465,19466,19467,19468,19469,19470,19471,19472,19473,19474,19475,19476,19477,19478,19479,19480,19481,19482,19483,19484,19485,19486,19487,19488,19489,19490,19491,19492,19493,19494,19495,19496,19497,19498,19499,19500,19501,19502,19503,19504,19505,19506,19507,19508,19509,19510,19511,19512,19513,19514,19515,19516,19517,19518,19519,19520,19521,19522,19523,19524,19525,19526,19527,19528,19529,19530,19531,19532,19533,19534,19535,19536,19537,19538,19539,19540,19541,19542,19543,19544,19545,19546,19547,19548,19549,19550,19551,19552,19553,19554,19555,19556,19557,19558,19559,19560,19561,19562,19563,19564,19565,19566,19567,19568,19569,19570,19571,19572,19573,19574,19575,19576,19577,19578,19579,19580,19581,19582,19583,19584,19585,19586,19587,19588,19589,19590,19591,19592,19593,19594,19595,19596,19597,19598,19599,19600,19601,19602,19603,19604,19605,19606,19607,19608,19609,19610,19611,19612,19613,19614,19615,19616,19617,19618,19619,19620,19621,19622,19623,19624,19625,19626,19627,19628,19629,19630,19631,19632,19633,19634,19635,19636,19637,19638,19639,19640,19641,19642,19643,19644,19645,19646,19647,19648,19649,19650,19651,19652,19653,19654,19655,19656,19657,19658,19659,19660,19661,19662,19663,19664,19665,19666,19667,19668,19669,19670,19671,19672,19673,19674,19675,19676,19677,19678,19679,19680,19681,19682,19683,19684,19685,19686,19687,19688,19689,19690,19691,19692,19693,19694,19695,19696,19697,19698,19699,19700,19701,19702,19703,19704,19705,19706,19707,19708,19709,19710,19711,19712,19713,19714,19715,19716,19717,19718,19719,19720,19721,19722,19723,19724,19725,19726,19727,19728,19729,19730,19731,19732,19733,19734,19735,19736,19737,19738,19739,19740,19741,19742,19743,19744,19745,19746,19747,19748,19749,19750,19751,19752,19753,19754,19755,19756,19757,19758,19759,19760,19761,19762,19763,19764,19765,19766,19767,19768,19769,19770,19771,19772,19773,19774,19775,19776,19777,19778,19779,19780,19781,19782,19783,19784,19785,19786,19787,19788,19789,19790,19791,19792,19793,19794,19795,19796,19797,19798,19799,19800,19801,19802,19803,19804,19805,19806,19807,19808,19809,19810,19811,19812,19813,19814,19815,19816,19817,19818,19819,19820,19821,19822,19823,19824,19825,19826,19827,19828,19829,19830,19831,19832,19833,19834,19835,19836,19837,19838,19839,19840,19841,19842,19843,19844,19845,19846,19847,19848,19849,19850,19851,19852,19853,19854,19855,19856,19857,19858,19859,19860,19861,19862,19863,19864,19865,19866,19867,19868,19869,19870,19871,19872,19873,19874,19875,19876,19877,19878,19879,19880,19881,19882,19883,19884,19885,19886,19887,19888,19889,19890,19891,19892,19893,19968,19969,19970,19971,19972,19973,19974,19975,19976,19977,19978,19979,19980,19981,19982,19983,19984,19985,19986,19987,19988,19989,19990,19991,19992,19993,19994,19995,19996,19997,19998,19999,20000,20001,20002,20003,20004,20005,20006,20007,20008,20009,20010,20011,20012,20013,20014,20015,20016,20017,20018,20019,20020,20021,20022,20023,20024,20025,20026,20027,20028,20029,20030,20031,20032,20033,20034,20035,20036,20037,20038,20039,20040,20041,20042,20043,20044,20045,20046,20047,20048,20049,20050,20051,20052,20053,20054,20055,20056,20057,20058,20059,20060,20061,20062,20063,20064,20065,20066,20067,20068,20069,20070,20071,20072,20073,20074,20075,20076,20077,20078,20079,20080,20081,20082,20083,20084,20085,20086,20087,20088,20089,20090,20091,20092,20093,20094,20095,20096,20097,20098,20099,20100,20101,20102,20103,20104,20105,20106,20107,20108,20109,20110,20111,20112,20113,20114,20115,20116,20117,20118,20119,20120,20121,20122,20123,20124,20125,20126,20127,20128,20129,20130,20131,20132,20133,20134,20135,20136,20137,20138,20139,20140,20141,20142,20143,20144,20145,20146,20147,20148,20149,20150,20151,20152,20153,20154,20155,20156,20157,20158,20159,20160,20161,20162,20163,20164,20165,20166,20167,20168,20169,20170,20171,20172,20173,20174,20175,20176,20177,20178,20179,20180,20181,20182,20183,20184,20185,20186,20187,20188,20189,20190,20191,20192,20193,20194,20195,20196,20197,20198,20199,20200,20201,20202,20203,20204,20205,20206,20207,20208,20209,20210,20211,20212,20213,20214,20215,20216,20217,20218,20219,20220,20221,20222,20223,20224,20225,20226,20227,20228,20229,20230,20231,20232,20233,20234,20235,20236,20237,20238,20239,20240,20241,20242,20243,20244,20245,20246,20247,20248,20249,20250,20251,20252,20253,20254,20255,20256,20257,20258,20259,20260,20261,20262,20263,20264,20265,20266,20267,20268,20269,20270,20271,20272,20273,20274,20275,20276,20277,20278,20279,20280,20281,20282,20283,20284,20285,20286,20287,20288,20289,20290,20291,20292,20293,20294,20295,20296,20297,20298,20299,20300,20301,20302,20303,20304,20305,20306,20307,20308,20309,20310,20311,20312,20313,20314,20315,20316,20317,20318,20319,20320,20321,20322,20323,20324,20325,20326,20327,20328,20329,20330,20331,20332,20333,20334,20335,20336,20337,20338,20339,20340,20341,20342,20343,20344,20345,20346,20347,20348,20349,20350,20351,20352,20353,20354,20355,20356,20357,20358,20359,20360,20361,20362,20363,20364,20365,20366,20367,20368,20369,20370,20371,20372,20373,20374,20375,20376,20377,20378,20379,20380,20381,20382,20383,20384,20385,20386,20387,20388,20389,20390,20391,20392,20393,20394,20395,20396,20397,20398,20399,20400,20401,20402,20403,20404,20405,20406,20407,20408,20409,20410,20411,20412,20413,20414,20415,20416,20417,20418,20419,20420,20421,20422,20423,20424,20425,20426,20427,20428,20429,20430,20431,20432,20433,20434,20435,20436,20437,20438,20439,20440,20441,20442,20443,20444,20445,20446,20447,20448,20449,20450,20451,20452,20453,20454,20455,20456,20457,20458,20459,20460,20461,20462,20463,20464,20465,20466,20467,20468,20469,20470,20471,20472,20473,20474,20475,20476,20477,20478,20479,20480,20481,20482,20483,20484,20485,20486,20487,20488,20489,20490,20491,20492,20493,20494,20495,20496,20497,20498,20499,20500,20501,20502,20503,20504,20505,20506,20507,20508,20509,20510,20511,20512,20513,20514,20515,20516,20517,20518,20519,20520,20521,20522,20523,20524,20525,20526,20527,20528,20529,20530,20531,20532,20533,20534,20535,20536,20537,20538,20539,20540,20541,20542,20543,20544,20545,20546,20547,20548,20549,20550,20551,20552,20553,20554,20555,20556,20557,20558,20559,20560,20561,20562,20563,20564,20565,20566,20567,20568,20569,20570,20571,20572,20573,20574,20575,20576,20577,20578,20579,20580,20581,20582,20583,20584,20585,20586,20587,20588,20589,20590,20591,20592,20593,20594,20595,20596,20597,20598,20599,20600,20601,20602,20603,20604,20605,20606,20607,20608,20609,20610,20611,20612,20613,20614,20615,20616,20617,20618,20619,20620,20621,20622,20623,20624,20625,20626,20627,20628,20629,20630,20631,20632,20633,20634,20635,20636,20637,20638,20639,20640,20641,20642,20643,20644,20645,20646,20647,20648,20649,20650,20651,20652,20653,20654,20655,20656,20657,20658,20659,20660,20661,20662,20663,20664,20665,20666,20667,20668,20669,20670,20671,20672,20673,20674,20675,20676,20677,20678,20679,20680,20681,20682,20683,20684,20685,20686,20687,20688,20689,20690,20691,20692,20693,20694,20695,20696,20697,20698,20699,20700,20701,20702,20703,20704,20705,20706,20707,20708,20709,20710,20711,20712,20713,20714,20715,20716,20717,20718,20719,20720,20721,20722,20723,20724,20725,20726,20727,20728,20729,20730,20731,20732,20733,20734,20735,20736,20737,20738,20739,20740,20741,20742,20743,20744,20745,20746,20747,20748,20749,20750,20751,20752,20753,20754,20755,20756,20757,20758,20759,20760,20761,20762,20763,20764,20765,20766,20767,20768,20769,20770,20771,20772,20773,20774,20775,20776,20777,20778,20779,20780,20781,20782,20783,20784,20785,20786,20787,20788,20789,20790,20791,20792,20793,20794,20795,20796,20797,20798,20799,20800,20801,20802,20803,20804,20805,20806,20807,20808,20809,20810,20811,20812,20813,20814,20815,20816,20817,20818,20819,20820,20821,20822,20823,20824,20825,20826,20827,20828,20829,20830,20831,20832,20833,20834,20835,20836,20837,20838,20839,20840,20841,20842,20843,20844,20845,20846,20847,20848,20849,20850,20851,20852,20853,20854,20855,20856,20857,20858,20859,20860,20861,20862,20863,20864,20865,20866,20867,20868,20869,20870,20871,20872,20873,20874,20875,20876,20877,20878,20879,20880,20881,20882,20883,20884,20885,20886,20887,20888,20889,20890,20891,20892,20893,20894,20895,20896,20897,20898,20899,20900,20901,20902,20903,20904,20905,20906,20907,20908,20909,20910,20911,20912,20913,20914,20915,20916,20917,20918,20919,20920,20921,20922,20923,20924,20925,20926,20927,20928,20929,20930,20931,20932,20933,20934,20935,20936,20937,20938,20939,20940,20941,20942,20943,20944,20945,20946,20947,20948,20949,20950,20951,20952,20953,20954,20955,20956,20957,20958,20959,20960,20961,20962,20963,20964,20965,20966,20967,20968,20969,20970,20971,20972,20973,20974,20975,20976,20977,20978,20979,20980,20981,20982,20983,20984,20985,20986,20987,20988,20989,20990,20991,20992,20993,20994,20995,20996,20997,20998,20999,21000,21001,21002,21003,21004,21005,21006,21007,21008,21009,21010,21011,21012,21013,21014,21015,21016,21017,21018,21019,21020,21021,21022,21023,21024,21025,21026,21027,21028,21029,21030,21031,21032,21033,21034,21035,21036,21037,21038,21039,21040,21041,21042,21043,21044,21045,21046,21047,21048,21049,21050,21051,21052,21053,21054,21055,21056,21057,21058,21059,21060,21061,21062,21063,21064,21065,21066,21067,21068,21069,21070,21071,21072,21073,21074,21075,21076,21077,21078,21079,21080,21081,21082,21083,21084,21085,21086,21087,21088,21089,21090,21091,21092,21093,21094,21095,21096,21097,21098,21099,21100,21101,21102,21103,21104,21105,21106,21107,21108,21109,21110,21111,21112,21113,21114,21115,21116,21117,21118,21119,21120,21121,21122,21123,21124,21125,21126,21127,21128,21129,21130,21131,21132,21133,21134,21135,21136,21137,21138,21139,21140,21141,21142,21143,21144,21145,21146,21147,21148,21149,21150,21151,21152,21153,21154,21155,21156,21157,21158,21159,21160,21161,21162,21163,21164,21165,21166,21167,21168,21169,21170,21171,21172,21173,21174,21175,21176,21177,21178,21179,21180,21181,21182,21183,21184,21185,21186,21187,21188,21189,21190,21191,21192,21193,21194,21195,21196,21197,21198,21199,21200,21201,21202,21203,21204,21205,21206,21207,21208,21209,21210,21211,21212,21213,21214,21215,21216,21217,21218,21219,21220,21221,21222,21223,21224,21225,21226,21227,21228,21229,21230,21231,21232,21233,21234,21235,21236,21237,21238,21239,21240,21241,21242,21243,21244,21245,21246,21247,21248,21249,21250,21251,21252,21253,21254,21255,21256,21257,21258,21259,21260,21261,21262,21263,21264,21265,21266,21267,21268,21269,21270,21271,21272,21273,21274,21275,21276,21277,21278,21279,21280,21281,21282,21283,21284,21285,21286,21287,21288,21289,21290,21291,21292,21293,21294,21295,21296,21297,21298,21299,21300,21301,21302,21303,21304,21305,21306,21307,21308,21309,21310,21311,21312,21313,21314,21315,21316,21317,21318,21319,21320,21321,21322,21323,21324,21325,21326,21327,21328,21329,21330,21331,21332,21333,21334,21335,21336,21337,21338,21339,21340,21341,21342,21343,21344,21345,21346,21347,21348,21349,21350,21351,21352,21353,21354,21355,21356,21357,21358,21359,21360,21361,21362,21363,21364,21365,21366,21367,21368,21369,21370,21371,21372,21373,21374,21375,21376,21377,21378,21379,21380,21381,21382,21383,21384,21385,21386,21387,21388,21389,21390,21391,21392,21393,21394,21395,21396,21397,21398,21399,21400,21401,21402,21403,21404,21405,21406,21407,21408,21409,21410,21411,21412,21413,21414,21415,21416,21417,21418,21419,21420,21421,21422,21423,21424,21425,21426,21427,21428,21429,21430,21431,21432,21433,21434,21435,21436,21437,21438,21439,21440,21441,21442,21443,21444,21445,21446,21447,21448,21449,21450,21451,21452,21453,21454,21455,21456,21457,21458,21459,21460,21461,21462,21463,21464,21465,21466,21467,21468,21469,21470,21471,21472,21473,21474,21475,21476,21477,21478,21479,21480,21481,21482,21483,21484,21485,21486,21487,21488,21489,21490,21491,21492,21493,21494,21495,21496,21497,21498,21499,21500,21501,21502,21503,21504,21505,21506,21507,21508,21509,21510,21511,21512,21513,21514,21515,21516,21517,21518,21519,21520,21521,21522,21523,21524,21525,21526,21527,21528,21529,21530,21531,21532,21533,21534,21535,21536,21537,21538,21539,21540,21541,21542,21543,21544,21545,21546,21547,21548,21549,21550,21551,21552,21553,21554,21555,21556,21557,21558,21559,21560,21561,21562,21563,21564,21565,21566,21567,21568,21569,21570,21571,21572,21573,21574,21575,21576,21577,21578,21579,21580,21581,21582,21583,21584,21585,21586,21587,21588,21589,21590,21591,21592,21593,21594,21595,21596,21597,21598,21599,21600,21601,21602,21603,21604,21605,21606,21607,21608,21609,21610,21611,21612,21613,21614,21615,21616,21617,21618,21619,21620,21621,21622,21623,21624,21625,21626,21627,21628,21629,21630,21631,21632,21633,21634,21635,21636,21637,21638,21639,21640,21641,21642,21643,21644,21645,21646,21647,21648,21649,21650,21651,21652,21653,21654,21655,21656,21657,21658,21659,21660,21661,21662,21663,21664,21665,21666,21667,21668,21669,21670,21671,21672,21673,21674,21675,21676,21677,21678,21679,21680,21681,21682,21683,21684,21685,21686,21687,21688,21689,21690,21691,21692,21693,21694,21695,21696,21697,21698,21699,21700,21701,21702,21703,21704,21705,21706,21707,21708,21709,21710,21711,21712,21713,21714,21715,21716,21717,21718,21719,21720,21721,21722,21723,21724,21725,21726,21727,21728,21729,21730,21731,21732,21733,21734,21735,21736,21737,21738,21739,21740,21741,21742,21743,21744,21745,21746,21747,21748,21749,21750,21751,21752,21753,21754,21755,21756,21757,21758,21759,21760,21761,21762,21763,21764,21765,21766,21767,21768,21769,21770,21771,21772,21773,21774,21775,21776,21777,21778,21779,21780,21781,21782,21783,21784,21785,21786,21787,21788,21789,21790,21791,21792,21793,21794,21795,21796,21797,21798,21799,21800,21801,21802,21803,21804,21805,21806,21807,21808,21809,21810,21811,21812,21813,21814,21815,21816,21817,21818,21819,21820,21821,21822,21823,21824,21825,21826,21827,21828,21829,21830,21831,21832,21833,21834,21835,21836,21837,21838,21839,21840,21841,21842,21843,21844,21845,21846,21847,21848,21849,21850,21851,21852,21853,21854,21855,21856,21857,21858,21859,21860,21861,21862,21863,21864,21865,21866,21867,21868,21869,21870,21871,21872,21873,21874,21875,21876,21877,21878,21879,21880,21881,21882,21883,21884,21885,21886,21887,21888,21889,21890,21891,21892,21893,21894,21895,21896,21897,21898,21899,21900,21901,21902,21903,21904,21905,21906,21907,21908,21909,21910,21911,21912,21913,21914,21915,21916,21917,21918,21919,21920,21921,21922,21923,21924,21925,21926,21927,21928,21929,21930,21931,21932,21933,21934,21935,21936,21937,21938,21939,21940,21941,21942,21943,21944,21945,21946,21947,21948,21949,21950,21951,21952,21953,21954,21955,21956,21957,21958,21959,21960,21961,21962,21963,21964,21965,21966,21967,21968,21969,21970,21971,21972,21973,21974,21975,21976,21977,21978,21979,21980,21981,21982,21983,21984,21985,21986,21987,21988,21989,21990,21991,21992,21993,21994,21995,21996,21997,21998,21999,22000,22001,22002,22003,22004,22005,22006,22007,22008,22009,22010,22011,22012,22013,22014,22015,22016,22017,22018,22019,22020,22021,22022,22023,22024,22025,22026,22027,22028,22029,22030,22031,22032,22033,22034,22035,22036,22037,22038,22039,22040,22041,22042,22043,22044,22045,22046,22047,22048,22049,22050,22051,22052,22053,22054,22055,22056,22057,22058,22059,22060,22061,22062,22063,22064,22065,22066,22067,22068,22069,22070,22071,22072,22073,22074,22075,22076,22077,22078,22079,22080,22081,22082,22083,22084,22085,22086,22087,22088,22089,22090,22091,22092,22093,22094,22095,22096,22097,22098,22099,22100,22101,22102,22103,22104,22105,22106,22107,22108,22109,22110,22111,22112,22113,22114,22115,22116,22117,22118,22119,22120,22121,22122,22123,22124,22125,22126,22127,22128,22129,22130,22131,22132,22133,22134,22135,22136,22137,22138,22139,22140,22141,22142,22143,22144,22145,22146,22147,22148,22149,22150,22151,22152,22153,22154,22155,22156,22157,22158,22159,22160,22161,22162,22163,22164,22165,22166,22167,22168,22169,22170,22171,22172,22173,22174,22175,22176,22177,22178,22179,22180,22181,22182,22183,22184,22185,22186,22187,22188,22189,22190,22191,22192,22193,22194,22195,22196,22197,22198,22199,22200,22201,22202,22203,22204,22205,22206,22207,22208,22209,22210,22211,22212,22213,22214,22215,22216,22217,22218,22219,22220,22221,22222,22223,22224,22225,22226,22227,22228,22229,22230,22231,22232,22233,22234,22235,22236,22237,22238,22239,22240,22241,22242,22243,22244,22245,22246,22247,22248,22249,22250,22251,22252,22253,22254,22255,22256,22257,22258,22259,22260,22261,22262,22263,22264,22265,22266,22267,22268,22269,22270,22271,22272,22273,22274,22275,22276,22277,22278,22279,22280,22281,22282,22283,22284,22285,22286,22287,22288,22289,22290,22291,22292,22293,22294,22295,22296,22297,22298,22299,22300,22301,22302,22303,22304,22305,22306,22307,22308,22309,22310,22311,22312,22313,22314,22315,22316,22317,22318,22319,22320,22321,22322,22323,22324,22325,22326,22327,22328,22329,22330,22331,22332,22333,22334,22335,22336,22337,22338,22339,22340,22341,22342,22343,22344,22345,22346,22347,22348,22349,22350,22351,22352,22353,22354,22355,22356,22357,22358,22359,22360,22361,22362,22363,22364,22365,22366,22367,22368,22369,22370,22371,22372,22373,22374,22375,22376,22377,22378,22379,22380,22381,22382,22383,22384,22385,22386,22387,22388,22389,22390,22391,22392,22393,22394,22395,22396,22397,22398,22399,22400,22401,22402,22403,22404,22405,22406,22407,22408,22409,22410,22411,22412,22413,22414,22415,22416,22417,22418,22419,22420,22421,22422,22423,22424,22425,22426,22427,22428,22429,22430,22431,22432,22433,22434,22435,22436,22437,22438,22439,22440,22441,22442,22443,22444,22445,22446,22447,22448,22449,22450,22451,22452,22453,22454,22455,22456,22457,22458,22459,22460,22461,22462,22463,22464,22465,22466,22467,22468,22469,22470,22471,22472,22473,22474,22475,22476,22477,22478,22479,22480,22481,22482,22483,22484,22485,22486,22487,22488,22489,22490,22491,22492,22493,22494,22495,22496,22497,22498,22499,22500,22501,22502,22503,22504,22505,22506,22507,22508,22509,22510,22511,22512,22513,22514,22515,22516,22517,22518,22519,22520,22521,22522,22523,22524,22525,22526,22527,22528,22529,22530,22531,22532,22533,22534,22535,22536,22537,22538,22539,22540,22541,22542,22543,22544,22545,22546,22547,22548,22549,22550,22551,22552,22553,22554,22555,22556,22557,22558,22559,22560,22561,22562,22563,22564,22565,22566,22567,22568,22569,22570,22571,22572,22573,22574,22575,22576,22577,22578,22579,22580,22581,22582,22583,22584,22585,22586,22587,22588,22589,22590,22591,22592,22593,22594,22595,22596,22597,22598,22599,22600,22601,22602,22603,22604,22605,22606,22607,22608,22609,22610,22611,22612,22613,22614,22615,22616,22617,22618,22619,22620,22621,22622,22623,22624,22625,22626,22627,22628,22629,22630,22631,22632,22633,22634,22635,22636,22637,22638,22639,22640,22641,22642,22643,22644,22645,22646,22647,22648,22649,22650,22651,22652,22653,22654,22655,22656,22657,22658,22659,22660,22661,22662,22663,22664,22665,22666,22667,22668,22669,22670,22671,22672,22673,22674,22675,22676,22677,22678,22679,22680,22681,22682,22683,22684,22685,22686,22687,22688,22689,22690,22691,22692,22693,22694,22695,22696,22697,22698,22699,22700,22701,22702,22703,22704,22705,22706,22707,22708,22709,22710,22711,22712,22713,22714,22715,22716,22717,22718,22719,22720,22721,22722,22723,22724,22725,22726,22727,22728,22729,22730,22731,22732,22733,22734,22735,22736,22737,22738,22739,22740,22741,22742,22743,22744,22745,22746,22747,22748,22749,22750,22751,22752,22753,22754,22755,22756,22757,22758,22759,22760,22761,22762,22763,22764,22765,22766,22767,22768,22769,22770,22771,22772,22773,22774,22775,22776,22777,22778,22779,22780,22781,22782,22783,22784,22785,22786,22787,22788,22789,22790,22791,22792,22793,22794,22795,22796,22797,22798,22799,22800,22801,22802,22803,22804,22805,22806,22807,22808,22809,22810,22811,22812,22813,22814,22815,22816,22817,22818,22819,22820,22821,22822,22823,22824,22825,22826,22827,22828,22829,22830,22831,22832,22833,22834,22835,22836,22837,22838,22839,22840,22841,22842,22843,22844,22845,22846,22847,22848,22849,22850,22851,22852,22853,22854,22855,22856,22857,22858,22859,22860,22861,22862,22863,22864,22865,22866,22867,22868,22869,22870,22871,22872,22873,22874,22875,22876,22877,22878,22879,22880,22881,22882,22883,22884,22885,22886,22887,22888,22889,22890,22891,22892,22893,22894,22895,22896,22897,22898,22899,22900,22901,22902,22903,22904,22905,22906,22907,22908,22909,22910,22911,22912,22913,22914,22915,22916,22917,22918,22919,22920,22921,22922,22923,22924,22925,22926,22927,22928,22929,22930,22931,22932,22933,22934,22935,22936,22937,22938,22939,22940,22941,22942,22943,22944,22945,22946,22947,22948,22949,22950,22951,22952,22953,22954,22955,22956,22957,22958,22959,22960,22961,22962,22963,22964,22965,22966,22967,22968,22969,22970,22971,22972,22973,22974,22975,22976,22977,22978,22979,22980,22981,22982,22983,22984,22985,22986,22987,22988,22989,22990,22991,22992,22993,22994,22995,22996,22997,22998,22999,23000,23001,23002,23003,23004,23005,23006,23007,23008,23009,23010,23011,23012,23013,23014,23015,23016,23017,23018,23019,23020,23021,23022,23023,23024,23025,23026,23027,23028,23029,23030,23031,23032,23033,23034,23035,23036,23037,23038,23039,23040,23041,23042,23043,23044,23045,23046,23047,23048,23049,23050,23051,23052,23053,23054,23055,23056,23057,23058,23059,23060,23061,23062,23063,23064,23065,23066,23067,23068,23069,23070,23071,23072,23073,23074,23075,23076,23077,23078,23079,23080,23081,23082,23083,23084,23085,23086,23087,23088,23089,23090,23091,23092,23093,23094,23095,23096,23097,23098,23099,23100,23101,23102,23103,23104,23105,23106,23107,23108,23109,23110,23111,23112,23113,23114,23115,23116,23117,23118,23119,23120,23121,23122,23123,23124,23125,23126,23127,23128,23129,23130,23131,23132,23133,23134,23135,23136,23137,23138,23139,23140,23141,23142,23143,23144,23145,23146,23147,23148,23149,23150,23151,23152,23153,23154,23155,23156,23157,23158,23159,23160,23161,23162,23163,23164,23165,23166,23167,23168,23169,23170,23171,23172,23173,23174,23175,23176,23177,23178,23179,23180,23181,23182,23183,23184,23185,23186,23187,23188,23189,23190,23191,23192,23193,23194,23195,23196,23197,23198,23199,23200,23201,23202,23203,23204,23205,23206,23207,23208,23209,23210,23211,23212,23213,23214,23215,23216,23217,23218,23219,23220,23221,23222,23223,23224,23225,23226,23227,23228,23229,23230,23231,23232,23233,23234,23235,23236,23237,23238,23239,23240,23241,23242,23243,23244,23245,23246,23247,23248,23249,23250,23251,23252,23253,23254,23255,23256,23257,23258,23259,23260,23261,23262,23263,23264,23265,23266,23267,23268,23269,23270,23271,23272,23273,23274,23275,23276,23277,23278,23279,23280,23281,23282,23283,23284,23285,23286,23287,23288,23289,23290,23291,23292,23293,23294,23295,23296,23297,23298,23299,23300,23301,23302,23303,23304,23305,23306,23307,23308,23309,23310,23311,23312,23313,23314,23315,23316,23317,23318,23319,23320,23321,23322,23323,23324,23325,23326,23327,23328,23329,23330,23331,23332,23333,23334,23335,23336,23337,23338,23339,23340,23341,23342,23343,23344,23345,23346,23347,23348,23349,23350,23351,23352,23353,23354,23355,23356,23357,23358,23359,23360,23361,23362,23363,23364,23365,23366,23367,23368,23369,23370,23371,23372,23373,23374,23375,23376,23377,23378,23379,23380,23381,23382,23383,23384,23385,23386,23387,23388,23389,23390,23391,23392,23393,23394,23395,23396,23397,23398,23399,23400,23401,23402,23403,23404,23405,23406,23407,23408,23409,23410,23411,23412,23413,23414,23415,23416,23417,23418,23419,23420,23421,23422,23423,23424,23425,23426,23427,23428,23429,23430,23431,23432,23433,23434,23435,23436,23437,23438,23439,23440,23441,23442,23443,23444,23445,23446,23447,23448,23449,23450,23451,23452,23453,23454,23455,23456,23457,23458,23459,23460,23461,23462,23463,23464,23465,23466,23467,23468,23469,23470,23471,23472,23473,23474,23475,23476,23477,23478,23479,23480,23481,23482,23483,23484,23485,23486,23487,23488,23489,23490,23491,23492,23493,23494,23495,23496,23497,23498,23499,23500,23501,23502,23503,23504,23505,23506,23507,23508,23509,23510,23511,23512,23513,23514,23515,23516,23517,23518,23519,23520,23521,23522,23523,23524,23525,23526,23527,23528,23529,23530,23531,23532,23533,23534,23535,23536,23537,23538,23539,23540,23541,23542,23543,23544,23545,23546,23547,23548,23549,23550,23551,23552,23553,23554,23555,23556,23557,23558,23559,23560,23561,23562,23563,23564,23565,23566,23567,23568,23569,23570,23571,23572,23573,23574,23575,23576,23577,23578,23579,23580,23581,23582,23583,23584,23585,23586,23587,23588,23589,23590,23591,23592,23593,23594,23595,23596,23597,23598,23599,23600,23601,23602,23603,23604,23605,23606,23607,23608,23609,23610,23611,23612,23613,23614,23615,23616,23617,23618,23619,23620,23621,23622,23623,23624,23625,23626,23627,23628,23629,23630,23631,23632,23633,23634,23635,23636,23637,23638,23639,23640,23641,23642,23643,23644,23645,23646,23647,23648,23649,23650,23651,23652,23653,23654,23655,23656,23657,23658,23659,23660,23661,23662,23663,23664,23665,23666,23667,23668,23669,23670,23671,23672,23673,23674,23675,23676,23677,23678,23679,23680,23681,23682,23683,23684,23685,23686,23687,23688,23689,23690,23691,23692,23693,23694,23695,23696,23697,23698,23699,23700,23701,23702,23703,23704,23705,23706,23707,23708,23709,23710,23711,23712,23713,23714,23715,23716,23717,23718,23719,23720,23721,23722,23723,23724,23725,23726,23727,23728,23729,23730,23731,23732,23733,23734,23735,23736,23737,23738,23739,23740,23741,23742,23743,23744,23745,23746,23747,23748,23749,23750,23751,23752,23753,23754,23755,23756,23757,23758,23759,23760,23761,23762,23763,23764,23765,23766,23767,23768,23769,23770,23771,23772,23773,23774,23775,23776,23777,23778,23779,23780,23781,23782,23783,23784,23785,23786,23787,23788,23789,23790,23791,23792,23793,23794,23795,23796,23797,23798,23799,23800,23801,23802,23803,23804,23805,23806,23807,23808,23809,23810,23811,23812,23813,23814,23815,23816,23817,23818,23819,23820,23821,23822,23823,23824,23825,23826,23827,23828,23829,23830,23831,23832,23833,23834,23835,23836,23837,23838,23839,23840,23841,23842,23843,23844,23845,23846,23847,23848,23849,23850,23851,23852,23853,23854,23855,23856,23857,23858,23859,23860,23861,23862,23863,23864,23865,23866,23867,23868,23869,23870,23871,23872,23873,23874,23875,23876,23877,23878,23879,23880,23881,23882,23883,23884,23885,23886,23887,23888,23889,23890,23891,23892,23893,23894,23895,23896,23897,23898,23899,23900,23901,23902,23903,23904,23905,23906,23907,23908,23909,23910,23911,23912,23913,23914,23915,23916,23917,23918,23919,23920,23921,23922,23923,23924,23925,23926,23927,23928,23929,23930,23931,23932,23933,23934,23935,23936,23937,23938,23939,23940,23941,23942,23943,23944,23945,23946,23947,23948,23949,23950,23951,23952,23953,23954,23955,23956,23957,23958,23959,23960,23961,23962,23963,23964,23965,23966,23967,23968,23969,23970,23971,23972,23973,23974,23975,23976,23977,23978,23979,23980,23981,23982,23983,23984,23985,23986,23987,23988,23989,23990,23991,23992,23993,23994,23995,23996,23997,23998,23999,24000,24001,24002,24003,24004,24005,24006,24007,24008,24009,24010,24011,24012,24013,24014,24015,24016,24017,24018,24019,24020,24021,24022,24023,24024,24025,24026,24027,24028,24029,24030,24031,24032,24033,24034,24035,24036,24037,24038,24039,24040,24041,24042,24043,24044,24045,24046,24047,24048,24049,24050,24051,24052,24053,24054,24055,24056,24057,24058,24059,24060,24061,24062,24063,24064,24065,24066,24067,24068,24069,24070,24071,24072,24073,24074,24075,24076,24077,24078,24079,24080,24081,24082,24083,24084,24085,24086,24087,24088,24089,24090,24091,24092,24093,24094,24095,24096,24097,24098,24099,24100,24101,24102,24103,24104,24105,24106,24107,24108,24109,24110,24111,24112,24113,24114,24115,24116,24117,24118,24119,24120,24121,24122,24123,24124,24125,24126,24127,24128,24129,24130,24131,24132,24133,24134,24135,24136,24137,24138,24139,24140,24141,24142,24143,24144,24145,24146,24147,24148,24149,24150,24151,24152,24153,24154,24155,24156,24157,24158,24159,24160,24161,24162,24163,24164,24165,24166,24167,24168,24169,24170,24171,24172,24173,24174,24175,24176,24177,24178,24179,24180,24181,24182,24183,24184,24185,24186,24187,24188,24189,24190,24191,24192,24193,24194,24195,24196,24197,24198,24199,24200,24201,24202,24203,24204,24205,24206,24207,24208,24209,24210,24211,24212,24213,24214,24215,24216,24217,24218,24219,24220,24221,24222,24223,24224,24225,24226,24227,24228,24229,24230,24231,24232,24233,24234,24235,24236,24237,24238,24239,24240,24241,24242,24243,24244,24245,24246,24247,24248,24249,24250,24251,24252,24253,24254,24255,24256,24257,24258,24259,24260,24261,24262,24263,24264,24265,24266,24267,24268,24269,24270,24271,24272,24273,24274,24275,24276,24277,24278,24279,24280,24281,24282,24283,24284,24285,24286,24287,24288,24289,24290,24291,24292,24293,24294,24295,24296,24297,24298,24299,24300,24301,24302,24303,24304,24305,24306,24307,24308,24309,24310,24311,24312,24313,24314,24315,24316,24317,24318,24319,24320,24321,24322,24323,24324,24325,24326,24327,24328,24329,24330,24331,24332,24333,24334,24335,24336,24337,24338,24339,24340,24341,24342,24343,24344,24345,24346,24347,24348,24349,24350,24351,24352,24353,24354,24355,24356,24357,24358,24359,24360,24361,24362,24363,24364,24365,24366,24367,24368,24369,24370,24371,24372,24373,24374,24375,24376,24377,24378,24379,24380,24381,24382,24383,24384,24385,24386,24387,24388,24389,24390,24391,24392,24393,24394,24395,24396,24397,24398,24399,24400,24401,24402,24403,24404,24405,24406,24407,24408,24409,24410,24411,24412,24413,24414,24415,24416,24417,24418,24419,24420,24421,24422,24423,24424,24425,24426,24427,24428,24429,24430,24431,24432,24433,24434,24435,24436,24437,24438,24439,24440,24441,24442,24443,24444,24445,24446,24447,24448,24449,24450,24451,24452,24453,24454,24455,24456,24457,24458,24459,24460,24461,24462,24463,24464,24465,24466,24467,24468,24469,24470,24471,24472,24473,24474,24475,24476,24477,24478,24479,24480,24481,24482,24483,24484,24485,24486,24487,24488,24489,24490,24491,24492,24493,24494,24495,24496,24497,24498,24499,24500,24501,24502,24503,24504,24505,24506,24507,24508,24509,24510,24511,24512,24513,24514,24515,24516,24517,24518,24519,24520,24521,24522,24523,24524,24525,24526,24527,24528,24529,24530,24531,24532,24533,24534,24535,24536,24537,24538,24539,24540,24541,24542,24543,24544,24545,24546,24547,24548,24549,24550,24551,24552,24553,24554,24555,24556,24557,24558,24559,24560,24561,24562,24563,24564,24565,24566,24567,24568,24569,24570,24571,24572,24573,24574,24575,24576,24577,24578,24579,24580,24581,24582,24583,24584,24585,24586,24587,24588,24589,24590,24591,24592,24593,24594,24595,24596,24597,24598,24599,24600,24601,24602,24603,24604,24605,24606,24607,24608,24609,24610,24611,24612,24613,24614,24615,24616,24617,24618,24619,24620,24621,24622,24623,24624,24625,24626,24627,24628,24629,24630,24631,24632,24633,24634,24635,24636,24637,24638,24639,24640,24641,24642,24643,24644,24645,24646,24647,24648,24649,24650,24651,24652,24653,24654,24655,24656,24657,24658,24659,24660,24661,24662,24663,24664,24665,24666,24667,24668,24669,24670,24671,24672,24673,24674,24675,24676,24677,24678,24679,24680,24681,24682,24683,24684,24685,24686,24687,24688,24689,24690,24691,24692,24693,24694,24695,24696,24697,24698,24699,24700,24701,24702,24703,24704,24705,24706,24707,24708,24709,24710,24711,24712,24713,24714,24715,24716,24717,24718,24719,24720,24721,24722,24723,24724,24725,24726,24727,24728,24729,24730,24731,24732,24733,24734,24735,24736,24737,24738,24739,24740,24741,24742,24743,24744,24745,24746,24747,24748,24749,24750,24751,24752,24753,24754,24755,24756,24757,24758,24759,24760,24761,24762,24763,24764,24765,24766,24767,24768,24769,24770,24771,24772,24773,24774,24775,24776,24777,24778,24779,24780,24781,24782,24783,24784,24785,24786,24787,24788,24789,24790,24791,24792,24793,24794,24795,24796,24797,24798,24799,24800,24801,24802,24803,24804,24805,24806,24807,24808,24809,24810,24811,24812,24813,24814,24815,24816,24817,24818,24819,24820,24821,24822,24823,24824,24825,24826,24827,24828,24829,24830,24831,24832,24833,24834,24835,24836,24837,24838,24839,24840,24841,24842,24843,24844,24845,24846,24847,24848,24849,24850,24851,24852,24853,24854,24855,24856,24857,24858,24859,24860,24861,24862,24863,24864,24865,24866,24867,24868,24869,24870,24871,24872,24873,24874,24875,24876,24877,24878,24879,24880,24881,24882,24883,24884,24885,24886,24887,24888,24889,24890,24891,24892,24893,24894,24895,24896,24897,24898,24899,24900,24901,24902,24903,24904,24905,24906,24907,24908,24909,24910,24911,24912,24913,24914,24915,24916,24917,24918,24919,24920,24921,24922,24923,24924,24925,24926,24927,24928,24929,24930,24931,24932,24933,24934,24935,24936,24937,24938,24939,24940,24941,24942,24943,24944,24945,24946,24947,24948,24949,24950,24951,24952,24953,24954,24955,24956,24957,24958,24959,24960,24961,24962,24963,24964,24965,24966,24967,24968,24969,24970,24971,24972,24973,24974,24975,24976,24977,24978,24979,24980,24981,24982,24983,24984,24985,24986,24987,24988,24989,24990,24991,24992,24993,24994,24995,24996,24997,24998,24999,25000,25001,25002,25003,25004,25005,25006,25007,25008,25009,25010,25011,25012,25013,25014,25015,25016,25017,25018,25019,25020,25021,25022,25023,25024,25025,25026,25027,25028,25029,25030,25031,25032,25033,25034,25035,25036,25037,25038,25039,25040,25041,25042,25043,25044,25045,25046,25047,25048,25049,25050,25051,25052,25053,25054,25055,25056,25057,25058,25059,25060,25061,25062,25063,25064,25065,25066,25067,25068,25069,25070,25071,25072,25073,25074,25075,25076,25077,25078,25079,25080,25081,25082,25083,25084,25085,25086,25087,25088,25089,25090,25091,25092,25093,25094,25095,25096,25097,25098,25099,25100,25101,25102,25103,25104,25105,25106,25107,25108,25109,25110,25111,25112,25113,25114,25115,25116,25117,25118,25119,25120,25121,25122,25123,25124,25125,25126,25127,25128,25129,25130,25131,25132,25133,25134,25135,25136,25137,25138,25139,25140,25141,25142,25143,25144,25145,25146,25147,25148,25149,25150,25151,25152,25153,25154,25155,25156,25157,25158,25159,25160,25161,25162,25163,25164,25165,25166,25167,25168,25169,25170,25171,25172,25173,25174,25175,25176,25177,25178,25179,25180,25181,25182,25183,25184,25185,25186,25187,25188,25189,25190,25191,25192,25193,25194,25195,25196,25197,25198,25199,25200,25201,25202,25203,25204,25205,25206,25207,25208,25209,25210,25211,25212,25213,25214,25215,25216,25217,25218,25219,25220,25221,25222,25223,25224,25225,25226,25227,25228,25229,25230,25231,25232,25233,25234,25235,25236,25237,25238,25239,25240,25241,25242,25243,25244,25245,25246,25247,25248,25249,25250,25251,25252,25253,25254,25255,25256,25257,25258,25259,25260,25261,25262,25263,25264,25265,25266,25267,25268,25269,25270,25271,25272,25273,25274,25275,25276,25277,25278,25279,25280,25281,25282,25283,25284,25285,25286,25287,25288,25289,25290,25291,25292,25293,25294,25295,25296,25297,25298,25299,25300,25301,25302,25303,25304,25305,25306,25307,25308,25309,25310,25311,25312,25313,25314,25315,25316,25317,25318,25319,25320,25321,25322,25323,25324,25325,25326,25327,25328,25329,25330,25331,25332,25333,25334,25335,25336,25337,25338,25339,25340,25341,25342,25343,25344,25345,25346,25347,25348,25349,25350,25351,25352,25353,25354,25355,25356,25357,25358,25359,25360,25361,25362,25363,25364,25365,25366,25367,25368,25369,25370,25371,25372,25373,25374,25375,25376,25377,25378,25379,25380,25381,25382,25383,25384,25385,25386,25387,25388,25389,25390,25391,25392,25393,25394,25395,25396,25397,25398,25399,25400,25401,25402,25403,25404,25405,25406,25407,25408,25409,25410,25411,25412,25413,25414,25415,25416,25417,25418,25419,25420,25421,25422,25423,25424,25425,25426,25427,25428,25429,25430,25431,25432,25433,25434,25435,25436,25437,25438,25439,25440,25441,25442,25443,25444,25445,25446,25447,25448,25449,25450,25451,25452,25453,25454,25455,25456,25457,25458,25459,25460,25461,25462,25463,25464,25465,25466,25467,25468,25469,25470,25471,25472,25473,25474,25475,25476,25477,25478,25479,25480,25481,25482,25483,25484,25485,25486,25487,25488,25489,25490,25491,25492,25493,25494,25495,25496,25497,25498,25499,25500,25501,25502,25503,25504,25505,25506,25507,25508,25509,25510,25511,25512,25513,25514,25515,25516,25517,25518,25519,25520,25521,25522,25523,25524,25525,25526,25527,25528,25529,25530,25531,25532,25533,25534,25535,25536,25537,25538,25539,25540,25541,25542,25543,25544,25545,25546,25547,25548,25549,25550,25551,25552,25553,25554,25555,25556,25557,25558,25559,25560,25561,25562,25563,25564,25565,25566,25567,25568,25569,25570,25571,25572,25573,25574,25575,25576,25577,25578,25579,25580,25581,25582,25583,25584,25585,25586,25587,25588,25589,25590,25591,25592,25593,25594,25595,25596,25597,25598,25599,25600,25601,25602,25603,25604,25605,25606,25607,25608,25609,25610,25611,25612,25613,25614,25615,25616,25617,25618,25619,25620,25621,25622,25623,25624,25625,25626,25627,25628,25629,25630,25631,25632,25633,25634,25635,25636,25637,25638,25639,25640,25641,25642,25643,25644,25645,25646,25647,25648,25649,25650,25651,25652,25653,25654,25655,25656,25657,25658,25659,25660,25661,25662,25663,25664,25665,25666,25667,25668,25669,25670,25671,25672,25673,25674,25675,25676,25677,25678,25679,25680,25681,25682,25683,25684,25685,25686,25687,25688,25689,25690,25691,25692,25693,25694,25695,25696,25697,25698,25699,25700,25701,25702,25703,25704,25705,25706,25707,25708,25709,25710,25711,25712,25713,25714,25715,25716,25717,25718,25719,25720,25721,25722,25723,25724,25725,25726,25727,25728,25729,25730,25731,25732,25733,25734,25735,25736,25737,25738,25739,25740,25741,25742,25743,25744,25745,25746,25747,25748,25749,25750,25751,25752,25753,25754,25755,25756,25757,25758,25759,25760,25761,25762,25763,25764,25765,25766,25767,25768,25769,25770,25771,25772,25773,25774,25775,25776,25777,25778,25779,25780,25781,25782,25783,25784,25785,25786,25787,25788,25789,25790,25791,25792,25793,25794,25795,25796,25797,25798,25799,25800,25801,25802,25803,25804,25805,25806,25807,25808,25809,25810,25811,25812,25813,25814,25815,25816,25817,25818,25819,25820,25821,25822,25823,25824,25825,25826,25827,25828,25829,25830,25831,25832,25833,25834,25835,25836,25837,25838,25839,25840,25841,25842,25843,25844,25845,25846,25847,25848,25849,25850,25851,25852,25853,25854,25855,25856,25857,25858,25859,25860,25861,25862,25863,25864,25865,25866,25867,25868,25869,25870,25871,25872,25873,25874,25875,25876,25877,25878,25879,25880,25881,25882,25883,25884,25885,25886,25887,25888,25889,25890,25891,25892,25893,25894,25895,25896,25897,25898,25899,25900,25901,25902,25903,25904,25905,25906,25907,25908,25909,25910,25911,25912,25913,25914,25915,25916,25917,25918,25919,25920,25921,25922,25923,25924,25925,25926,25927,25928,25929,25930,25931,25932,25933,25934,25935,25936,25937,25938,25939,25940,25941,25942,25943,25944,25945,25946,25947,25948,25949,25950,25951,25952,25953,25954,25955,25956,25957,25958,25959,25960,25961,25962,25963,25964,25965,25966,25967,25968,25969,25970,25971,25972,25973,25974,25975,25976,25977,25978,25979,25980,25981,25982,25983,25984,25985,25986,25987,25988,25989,25990,25991,25992,25993,25994,25995,25996,25997,25998,25999,26000,26001,26002,26003,26004,26005,26006,26007,26008,26009,26010,26011,26012,26013,26014,26015,26016,26017,26018,26019,26020,26021,26022,26023,26024,26025,26026,26027,26028,26029,26030,26031,26032,26033,26034,26035,26036,26037,26038,26039,26040,26041,26042,26043,26044,26045,26046,26047,26048,26049,26050,26051,26052,26053,26054,26055,26056,26057,26058,26059,26060,26061,26062,26063,26064,26065,26066,26067,26068,26069,26070,26071,26072,26073,26074,26075,26076,26077,26078,26079,26080,26081,26082,26083,26084,26085,26086,26087,26088,26089,26090,26091,26092,26093,26094,26095,26096,26097,26098,26099,26100,26101,26102,26103,26104,26105,26106,26107,26108,26109,26110,26111,26112,26113,26114,26115,26116,26117,26118,26119,26120,26121,26122,26123,26124,26125,26126,26127,26128,26129,26130,26131,26132,26133,26134,26135,26136,26137,26138,26139,26140,26141,26142,26143,26144,26145,26146,26147,26148,26149,26150,26151,26152,26153,26154,26155,26156,26157,26158,26159,26160,26161,26162,26163,26164,26165,26166,26167,26168,26169,26170,26171,26172,26173,26174,26175,26176,26177,26178,26179,26180,26181,26182,26183,26184,26185,26186,26187,26188,26189,26190,26191,26192,26193,26194,26195,26196,26197,26198,26199,26200,26201,26202,26203,26204,26205,26206,26207,26208,26209,26210,26211,26212,26213,26214,26215,26216,26217,26218,26219,26220,26221,26222,26223,26224,26225,26226,26227,26228,26229,26230,26231,26232,26233,26234,26235,26236,26237,26238,26239,26240,26241,26242,26243,26244,26245,26246,26247,26248,26249,26250,26251,26252,26253,26254,26255,26256,26257,26258,26259,26260,26261,26262,26263,26264,26265,26266,26267,26268,26269,26270,26271,26272,26273,26274,26275,26276,26277,26278,26279,26280,26281,26282,26283,26284,26285,26286,26287,26288,26289,26290,26291,26292,26293,26294,26295,26296,26297,26298,26299,26300,26301,26302,26303,26304,26305,26306,26307,26308,26309,26310,26311,26312,26313,26314,26315,26316,26317,26318,26319,26320,26321,26322,26323,26324,26325,26326,26327,26328,26329,26330,26331,26332,26333,26334,26335,26336,26337,26338,26339,26340,26341,26342,26343,26344,26345,26346,26347,26348,26349,26350,26351,26352,26353,26354,26355,26356,26357,26358,26359,26360,26361,26362,26363,26364,26365,26366,26367,26368,26369,26370,26371,26372,26373,26374,26375,26376,26377,26378,26379,26380,26381,26382,26383,26384,26385,26386,26387,26388,26389,26390,26391,26392,26393,26394,26395,26396,26397,26398,26399,26400,26401,26402,26403,26404,26405,26406,26407,26408,26409,26410,26411,26412,26413,26414,26415,26416,26417,26418,26419,26420,26421,26422,26423,26424,26425,26426,26427,26428,26429,26430,26431,26432,26433,26434,26435,26436,26437,26438,26439,26440,26441,26442,26443,26444,26445,26446,26447,26448,26449,26450,26451,26452,26453,26454,26455,26456,26457,26458,26459,26460,26461,26462,26463,26464,26465,26466,26467,26468,26469,26470,26471,26472,26473,26474,26475,26476,26477,26478,26479,26480,26481,26482,26483,26484,26485,26486,26487,26488,26489,26490,26491,26492,26493,26494,26495,26496,26497,26498,26499,26500,26501,26502,26503,26504,26505,26506,26507,26508,26509,26510,26511,26512,26513,26514,26515,26516,26517,26518,26519,26520,26521,26522,26523,26524,26525,26526,26527,26528,26529,26530,26531,26532,26533,26534,26535,26536,26537,26538,26539,26540,26541,26542,26543,26544,26545,26546,26547,26548,26549,26550,26551,26552,26553,26554,26555,26556,26557,26558,26559,26560,26561,26562,26563,26564,26565,26566,26567,26568,26569,26570,26571,26572,26573,26574,26575,26576,26577,26578,26579,26580,26581,26582,26583,26584,26585,26586,26587,26588,26589,26590,26591,26592,26593,26594,26595,26596,26597,26598,26599,26600,26601,26602,26603,26604,26605,26606,26607,26608,26609,26610,26611,26612,26613,26614,26615,26616,26617,26618,26619,26620,26621,26622,26623,26624,26625,26626,26627,26628,26629,26630,26631,26632,26633,26634,26635,26636,26637,26638,26639,26640,26641,26642,26643,26644,26645,26646,26647,26648,26649,26650,26651,26652,26653,26654,26655,26656,26657,26658,26659,26660,26661,26662,26663,26664,26665,26666,26667,26668,26669,26670,26671,26672,26673,26674,26675,26676,26677,26678,26679,26680,26681,26682,26683,26684,26685,26686,26687,26688,26689,26690,26691,26692,26693,26694,26695,26696,26697,26698,26699,26700,26701,26702,26703,26704,26705,26706,26707,26708,26709,26710,26711,26712,26713,26714,26715,26716,26717,26718,26719,26720,26721,26722,26723,26724,26725,26726,26727,26728,26729,26730,26731,26732,26733,26734,26735,26736,26737,26738,26739,26740,26741,26742,26743,26744,26745,26746,26747,26748,26749,26750,26751,26752,26753,26754,26755,26756,26757,26758,26759,26760,26761,26762,26763,26764,26765,26766,26767,26768,26769,26770,26771,26772,26773,26774,26775,26776,26777,26778,26779,26780,26781,26782,26783,26784,26785,26786,26787,26788,26789,26790,26791,26792,26793,26794,26795,26796,26797,26798,26799,26800,26801,26802,26803,26804,26805,26806,26807,26808,26809,26810,26811,26812,26813,26814,26815,26816,26817,26818,26819,26820,26821,26822,26823,26824,26825,26826,26827,26828,26829,26830,26831,26832,26833,26834,26835,26836,26837,26838,26839,26840,26841,26842,26843,26844,26845,26846,26847,26848,26849,26850,26851,26852,26853,26854,26855,26856,26857,26858,26859,26860,26861,26862,26863,26864,26865,26866,26867,26868,26869,26870,26871,26872,26873,26874,26875,26876,26877,26878,26879,26880,26881,26882,26883,26884,26885,26886,26887,26888,26889,26890,26891,26892,26893,26894,26895,26896,26897,26898,26899,26900,26901,26902,26903,26904,26905,26906,26907,26908,26909,26910,26911,26912,26913,26914,26915,26916,26917,26918,26919,26920,26921,26922,26923,26924,26925,26926,26927,26928,26929,26930,26931,26932,26933,26934,26935,26936,26937,26938,26939,26940,26941,26942,26943,26944,26945,26946,26947,26948,26949,26950,26951,26952,26953,26954,26955,26956,26957,26958,26959,26960,26961,26962,26963,26964,26965,26966,26967,26968,26969,26970,26971,26972,26973,26974,26975,26976,26977,26978,26979,26980,26981,26982,26983,26984,26985,26986,26987,26988,26989,26990,26991,26992,26993,26994,26995,26996,26997,26998,26999,27000,27001,27002,27003,27004,27005,27006,27007,27008,27009,27010,27011,27012,27013,27014,27015,27016,27017,27018,27019,27020,27021,27022,27023,27024,27025,27026,27027,27028,27029,27030,27031,27032,27033,27034,27035,27036,27037,27038,27039,27040,27041,27042,27043,27044,27045,27046,27047,27048,27049,27050,27051,27052,27053,27054,27055,27056,27057,27058,27059,27060,27061,27062,27063,27064,27065,27066,27067,27068,27069,27070,27071,27072,27073,27074,27075,27076,27077,27078,27079,27080,27081,27082,27083,27084,27085,27086,27087,27088,27089,27090,27091,27092,27093,27094,27095,27096,27097,27098,27099,27100,27101,27102,27103,27104,27105,27106,27107,27108,27109,27110,27111,27112,27113,27114,27115,27116,27117,27118,27119,27120,27121,27122,27123,27124,27125,27126,27127,27128,27129,27130,27131,27132,27133,27134,27135,27136,27137,27138,27139,27140,27141,27142,27143,27144,27145,27146,27147,27148,27149,27150,27151,27152,27153,27154,27155,27156,27157,27158,27159,27160,27161,27162,27163,27164,27165,27166,27167,27168,27169,27170,27171,27172,27173,27174,27175,27176,27177,27178,27179,27180,27181,27182,27183,27184,27185,27186,27187,27188,27189,27190,27191,27192,27193,27194,27195,27196,27197,27198,27199,27200,27201,27202,27203,27204,27205,27206,27207,27208,27209,27210,27211,27212,27213,27214,27215,27216,27217,27218,27219,27220,27221,27222,27223,27224,27225,27226,27227,27228,27229,27230,27231,27232,27233,27234,27235,27236,27237,27238,27239,27240,27241,27242,27243,27244,27245,27246,27247,27248,27249,27250,27251,27252,27253,27254,27255,27256,27257,27258,27259,27260,27261,27262,27263,27264,27265,27266,27267,27268,27269,27270,27271,27272,27273,27274,27275,27276,27277,27278,27279,27280,27281,27282,27283,27284,27285,27286,27287,27288,27289,27290,27291,27292,27293,27294,27295,27296,27297,27298,27299,27300,27301,27302,27303,27304,27305,27306,27307,27308,27309,27310,27311,27312,27313,27314,27315,27316,27317,27318,27319,27320,27321,27322,27323,27324,27325,27326,27327,27328,27329,27330,27331,27332,27333,27334,27335,27336,27337,27338,27339,27340,27341,27342,27343,27344,27345,27346,27347,27348,27349,27350,27351,27352,27353,27354,27355,27356,27357,27358,27359,27360,27361,27362,27363,27364,27365,27366,27367,27368,27369,27370,27371,27372,27373,27374,27375,27376,27377,27378,27379,27380,27381,27382,27383,27384,27385,27386,27387,27388,27389,27390,27391,27392,27393,27394,27395,27396,27397,27398,27399,27400,27401,27402,27403,27404,27405,27406,27407,27408,27409,27410,27411,27412,27413,27414,27415,27416,27417,27418,27419,27420,27421,27422,27423,27424,27425,27426,27427,27428,27429,27430,27431,27432,27433,27434,27435,27436,27437,27438,27439,27440,27441,27442,27443,27444,27445,27446,27447,27448,27449,27450,27451,27452,27453,27454,27455,27456,27457,27458,27459,27460,27461,27462,27463,27464,27465,27466,27467,27468,27469,27470,27471,27472,27473,27474,27475,27476,27477,27478,27479,27480,27481,27482,27483,27484,27485,27486,27487,27488,27489,27490,27491,27492,27493,27494,27495,27496,27497,27498,27499,27500,27501,27502,27503,27504,27505,27506,27507,27508,27509,27510,27511,27512,27513,27514,27515,27516,27517,27518,27519,27520,27521,27522,27523,27524,27525,27526,27527,27528,27529,27530,27531,27532,27533,27534,27535,27536,27537,27538,27539,27540,27541,27542,27543,27544,27545,27546,27547,27548,27549,27550,27551,27552,27553,27554,27555,27556,27557,27558,27559,27560,27561,27562,27563,27564,27565,27566,27567,27568,27569,27570,27571,27572,27573,27574,27575,27576,27577,27578,27579,27580,27581,27582,27583,27584,27585,27586,27587,27588,27589,27590,27591,27592,27593,27594,27595,27596,27597,27598,27599,27600,27601,27602,27603,27604,27605,27606,27607,27608,27609,27610,27611,27612,27613,27614,27615,27616,27617,27618,27619,27620,27621,27622,27623,27624,27625,27626,27627,27628,27629,27630,27631,27632,27633,27634,27635,27636,27637,27638,27639,27640,27641,27642,27643,27644,27645,27646,27647,27648,27649,27650,27651,27652,27653,27654,27655,27656,27657,27658,27659,27660,27661,27662,27663,27664,27665,27666,27667,27668,27669,27670,27671,27672,27673,27674,27675,27676,27677,27678,27679,27680,27681,27682,27683,27684,27685,27686,27687,27688,27689,27690,27691,27692,27693,27694,27695,27696,27697,27698,27699,27700,27701,27702,27703,27704,27705,27706,27707,27708,27709,27710,27711,27712,27713,27714,27715,27716,27717,27718,27719,27720,27721,27722,27723,27724,27725,27726,27727,27728,27729,27730,27731,27732,27733,27734,27735,27736,27737,27738,27739,27740,27741,27742,27743,27744,27745,27746,27747,27748,27749,27750,27751,27752,27753,27754,27755,27756,27757,27758,27759,27760,27761,27762,27763,27764,27765,27766,27767,27768,27769,27770,27771,27772,27773,27774,27775,27776,27777,27778,27779,27780,27781,27782,27783,27784,27785,27786,27787,27788,27789,27790,27791,27792,27793,27794,27795,27796,27797,27798,27799,27800,27801,27802,27803,27804,27805,27806,27807,27808,27809,27810,27811,27812,27813,27814,27815,27816,27817,27818,27819,27820,27821,27822,27823,27824,27825,27826,27827,27828,27829,27830,27831,27832,27833,27834,27835,27836,27837,27838,27839,27840,27841,27842,27843,27844,27845,27846,27847,27848,27849,27850,27851,27852,27853,27854,27855,27856,27857,27858,27859,27860,27861,27862,27863,27864,27865,27866,27867,27868,27869,27870,27871,27872,27873,27874,27875,27876,27877,27878,27879,27880,27881,27882,27883,27884,27885,27886,27887,27888,27889,27890,27891,27892,27893,27894,27895,27896,27897,27898,27899,27900,27901,27902,27903,27904,27905,27906,27907,27908,27909,27910,27911,27912,27913,27914,27915,27916,27917,27918,27919,27920,27921,27922,27923,27924,27925,27926,27927,27928,27929,27930,27931,27932,27933,27934,27935,27936,27937,27938,27939,27940,27941,27942,27943,27944,27945,27946,27947,27948,27949,27950,27951,27952,27953,27954,27955,27956,27957,27958,27959,27960,27961,27962,27963,27964,27965,27966,27967,27968,27969,27970,27971,27972,27973,27974,27975,27976,27977,27978,27979,27980,27981,27982,27983,27984,27985,27986,27987,27988,27989,27990,27991,27992,27993,27994,27995,27996,27997,27998,27999,28000,28001,28002,28003,28004,28005,28006,28007,28008,28009,28010,28011,28012,28013,28014,28015,28016,28017,28018,28019,28020,28021,28022,28023,28024,28025,28026,28027,28028,28029,28030,28031,28032,28033,28034,28035,28036,28037,28038,28039,28040,28041,28042,28043,28044,28045,28046,28047,28048,28049,28050,28051,28052,28053,28054,28055,28056,28057,28058,28059,28060,28061,28062,28063,28064,28065,28066,28067,28068,28069,28070,28071,28072,28073,28074,28075,28076,28077,28078,28079,28080,28081,28082,28083,28084,28085,28086,28087,28088,28089,28090,28091,28092,28093,28094,28095,28096,28097,28098,28099,28100,28101,28102,28103,28104,28105,28106,28107,28108,28109,28110,28111,28112,28113,28114,28115,28116,28117,28118,28119,28120,28121,28122,28123,28124,28125,28126,28127,28128,28129,28130,28131,28132,28133,28134,28135,28136,28137,28138,28139,28140,28141,28142,28143,28144,28145,28146,28147,28148,28149,28150,28151,28152,28153,28154,28155,28156,28157,28158,28159,28160,28161,28162,28163,28164,28165,28166,28167,28168,28169,28170,28171,28172,28173,28174,28175,28176,28177,28178,28179,28180,28181,28182,28183,28184,28185,28186,28187,28188,28189,28190,28191,28192,28193,28194,28195,28196,28197,28198,28199,28200,28201,28202,28203,28204,28205,28206,28207,28208,28209,28210,28211,28212,28213,28214,28215,28216,28217,28218,28219,28220,28221,28222,28223,28224,28225,28226,28227,28228,28229,28230,28231,28232,28233,28234,28235,28236,28237,28238,28239,28240,28241,28242,28243,28244,28245,28246,28247,28248,28249,28250,28251,28252,28253,28254,28255,28256,28257,28258,28259,28260,28261,28262,28263,28264,28265,28266,28267,28268,28269,28270,28271,28272,28273,28274,28275,28276,28277,28278,28279,28280,28281,28282,28283,28284,28285,28286,28287,28288,28289,28290,28291,28292,28293,28294,28295,28296,28297,28298,28299,28300,28301,28302,28303,28304,28305,28306,28307,28308,28309,28310,28311,28312,28313,28314,28315,28316,28317,28318,28319,28320,28321,28322,28323,28324,28325,28326,28327,28328,28329,28330,28331,28332,28333,28334,28335,28336,28337,28338,28339,28340,28341,28342,28343,28344,28345,28346,28347,28348,28349,28350,28351,28352,28353,28354,28355,28356,28357,28358,28359,28360,28361,28362,28363,28364,28365,28366,28367,28368,28369,28370,28371,28372,28373,28374,28375,28376,28377,28378,28379,28380,28381,28382,28383,28384,28385,28386,28387,28388,28389,28390,28391,28392,28393,28394,28395,28396,28397,28398,28399,28400,28401,28402,28403,28404,28405,28406,28407,28408,28409,28410,28411,28412,28413,28414,28415,28416,28417,28418,28419,28420,28421,28422,28423,28424,28425,28426,28427,28428,28429,28430,28431,28432,28433,28434,28435,28436,28437,28438,28439,28440,28441,28442,28443,28444,28445,28446,28447,28448,28449,28450,28451,28452,28453,28454,28455,28456,28457,28458,28459,28460,28461,28462,28463,28464,28465,28466,28467,28468,28469,28470,28471,28472,28473,28474,28475,28476,28477,28478,28479,28480,28481,28482,28483,28484,28485,28486,28487,28488,28489,28490,28491,28492,28493,28494,28495,28496,28497,28498,28499,28500,28501,28502,28503,28504,28505,28506,28507,28508,28509,28510,28511,28512,28513,28514,28515,28516,28517,28518,28519,28520,28521,28522,28523,28524,28525,28526,28527,28528,28529,28530,28531,28532,28533,28534,28535,28536,28537,28538,28539,28540,28541,28542,28543,28544,28545,28546,28547,28548,28549,28550,28551,28552,28553,28554,28555,28556,28557,28558,28559,28560,28561,28562,28563,28564,28565,28566,28567,28568,28569,28570,28571,28572,28573,28574,28575,28576,28577,28578,28579,28580,28581,28582,28583,28584,28585,28586,28587,28588,28589,28590,28591,28592,28593,28594,28595,28596,28597,28598,28599,28600,28601,28602,28603,28604,28605,28606,28607,28608,28609,28610,28611,28612,28613,28614,28615,28616,28617,28618,28619,28620,28621,28622,28623,28624,28625,28626,28627,28628,28629,28630,28631,28632,28633,28634,28635,28636,28637,28638,28639,28640,28641,28642,28643,28644,28645,28646,28647,28648,28649,28650,28651,28652,28653,28654,28655,28656,28657,28658,28659,28660,28661,28662,28663,28664,28665,28666,28667,28668,28669,28670,28671,28672,28673,28674,28675,28676,28677,28678,28679,28680,28681,28682,28683,28684,28685,28686,28687,28688,28689,28690,28691,28692,28693,28694,28695,28696,28697,28698,28699,28700,28701,28702,28703,28704,28705,28706,28707,28708,28709,28710,28711,28712,28713,28714,28715,28716,28717,28718,28719,28720,28721,28722,28723,28724,28725,28726,28727,28728,28729,28730,28731,28732,28733,28734,28735,28736,28737,28738,28739,28740,28741,28742,28743,28744,28745,28746,28747,28748,28749,28750,28751,28752,28753,28754,28755,28756,28757,28758,28759,28760,28761,28762,28763,28764,28765,28766,28767,28768,28769,28770,28771,28772,28773,28774,28775,28776,28777,28778,28779,28780,28781,28782,28783,28784,28785,28786,28787,28788,28789,28790,28791,28792,28793,28794,28795,28796,28797,28798,28799,28800,28801,28802,28803,28804,28805,28806,28807,28808,28809,28810,28811,28812,28813,28814,28815,28816,28817,28818,28819,28820,28821,28822,28823,28824,28825,28826,28827,28828,28829,28830,28831,28832,28833,28834,28835,28836,28837,28838,28839,28840,28841,28842,28843,28844,28845,28846,28847,28848,28849,28850,28851,28852,28853,28854,28855,28856,28857,28858,28859,28860,28861,28862,28863,28864,28865,28866,28867,28868,28869,28870,28871,28872,28873,28874,28875,28876,28877,28878,28879,28880,28881,28882,28883,28884,28885,28886,28887,28888,28889,28890,28891,28892,28893,28894,28895,28896,28897,28898,28899,28900,28901,28902,28903,28904,28905,28906,28907,28908,28909,28910,28911,28912,28913,28914,28915,28916,28917,28918,28919,28920,28921,28922,28923,28924,28925,28926,28927,28928,28929,28930,28931,28932,28933,28934,28935,28936,28937,28938,28939,28940,28941,28942,28943,28944,28945,28946,28947,28948,28949,28950,28951,28952,28953,28954,28955,28956,28957,28958,28959,28960,28961,28962,28963,28964,28965,28966,28967,28968,28969,28970,28971,28972,28973,28974,28975,28976,28977,28978,28979,28980,28981,28982,28983,28984,28985,28986,28987,28988,28989,28990,28991,28992,28993,28994,28995,28996,28997,28998,28999,29000,29001,29002,29003,29004,29005,29006,29007,29008,29009,29010,29011,29012,29013,29014,29015,29016,29017,29018,29019,29020,29021,29022,29023,29024,29025,29026,29027,29028,29029,29030,29031,29032,29033,29034,29035,29036,29037,29038,29039,29040,29041,29042,29043,29044,29045,29046,29047,29048,29049,29050,29051,29052,29053,29054,29055,29056,29057,29058,29059,29060,29061,29062,29063,29064,29065,29066,29067,29068,29069,29070,29071,29072,29073,29074,29075,29076,29077,29078,29079,29080,29081,29082,29083,29084,29085,29086,29087,29088,29089,29090,29091,29092,29093,29094,29095,29096,29097,29098,29099,29100,29101,29102,29103,29104,29105,29106,29107,29108,29109,29110,29111,29112,29113,29114,29115,29116,29117,29118,29119,29120,29121,29122,29123,29124,29125,29126,29127,29128,29129,29130,29131,29132,29133,29134,29135,29136,29137,29138,29139,29140,29141,29142,29143,29144,29145,29146,29147,29148,29149,29150,29151,29152,29153,29154,29155,29156,29157,29158,29159,29160,29161,29162,29163,29164,29165,29166,29167,29168,29169,29170,29171,29172,29173,29174,29175,29176,29177,29178,29179,29180,29181,29182,29183,29184,29185,29186,29187,29188,29189,29190,29191,29192,29193,29194,29195,29196,29197,29198,29199,29200,29201,29202,29203,29204,29205,29206,29207,29208,29209,29210,29211,29212,29213,29214,29215,29216,29217,29218,29219,29220,29221,29222,29223,29224,29225,29226,29227,29228,29229,29230,29231,29232,29233,29234,29235,29236,29237,29238,29239,29240,29241,29242,29243,29244,29245,29246,29247,29248,29249,29250,29251,29252,29253,29254,29255,29256,29257,29258,29259,29260,29261,29262,29263,29264,29265,29266,29267,29268,29269,29270,29271,29272,29273,29274,29275,29276,29277,29278,29279,29280,29281,29282,29283,29284,29285,29286,29287,29288,29289,29290,29291,29292,29293,29294,29295,29296,29297,29298,29299,29300,29301,29302,29303,29304,29305,29306,29307,29308,29309,29310,29311,29312,29313,29314,29315,29316,29317,29318,29319,29320,29321,29322,29323,29324,29325,29326,29327,29328,29329,29330,29331,29332,29333,29334,29335,29336,29337,29338,29339,29340,29341,29342,29343,29344,29345,29346,29347,29348,29349,29350,29351,29352,29353,29354,29355,29356,29357,29358,29359,29360,29361,29362,29363,29364,29365,29366,29367,29368,29369,29370,29371,29372,29373,29374,29375,29376,29377,29378,29379,29380,29381,29382,29383,29384,29385,29386,29387,29388,29389,29390,29391,29392,29393,29394,29395,29396,29397,29398,29399,29400,29401,29402,29403,29404,29405,29406,29407,29408,29409,29410,29411,29412,29413,29414,29415,29416,29417,29418,29419,29420,29421,29422,29423,29424,29425,29426,29427,29428,29429,29430,29431,29432,29433,29434,29435,29436,29437,29438,29439,29440,29441,29442,29443,29444,29445,29446,29447,29448,29449,29450,29451,29452,29453,29454,29455,29456,29457,29458,29459,29460,29461,29462,29463,29464,29465,29466,29467,29468,29469,29470,29471,29472,29473,29474,29475,29476,29477,29478,29479,29480,29481,29482,29483,29484,29485,29486,29487,29488,29489,29490,29491,29492,29493,29494,29495,29496,29497,29498,29499,29500,29501,29502,29503,29504,29505,29506,29507,29508,29509,29510,29511,29512,29513,29514,29515,29516,29517,29518,29519,29520,29521,29522,29523,29524,29525,29526,29527,29528,29529,29530,29531,29532,29533,29534,29535,29536,29537,29538,29539,29540,29541,29542,29543,29544,29545,29546,29547,29548,29549,29550,29551,29552,29553,29554,29555,29556,29557,29558,29559,29560,29561,29562,29563,29564,29565,29566,29567,29568,29569,29570,29571,29572,29573,29574,29575,29576,29577,29578,29579,29580,29581,29582,29583,29584,29585,29586,29587,29588,29589,29590,29591,29592,29593,29594,29595,29596,29597,29598,29599,29600,29601,29602,29603,29604,29605,29606,29607,29608,29609,29610,29611,29612,29613,29614,29615,29616,29617,29618,29619,29620,29621,29622,29623,29624,29625,29626,29627,29628,29629,29630,29631,29632,29633,29634,29635,29636,29637,29638,29639,29640,29641,29642,29643,29644,29645,29646,29647,29648,29649,29650,29651,29652,29653,29654,29655,29656,29657,29658,29659,29660,29661,29662,29663,29664,29665,29666,29667,29668,29669,29670,29671,29672,29673,29674,29675,29676,29677,29678,29679,29680,29681,29682,29683,29684,29685,29686,29687,29688,29689,29690,29691,29692,29693,29694,29695,29696,29697,29698,29699,29700,29701,29702,29703,29704,29705,29706,29707,29708,29709,29710,29711,29712,29713,29714,29715,29716,29717,29718,29719,29720,29721,29722,29723,29724,29725,29726,29727,29728,29729,29730,29731,29732,29733,29734,29735,29736,29737,29738,29739,29740,29741,29742,29743,29744,29745,29746,29747,29748,29749,29750,29751,29752,29753,29754,29755,29756,29757,29758,29759,29760,29761,29762,29763,29764,29765,29766,29767,29768,29769,29770,29771,29772,29773,29774,29775,29776,29777,29778,29779,29780,29781,29782,29783,29784,29785,29786,29787,29788,29789,29790,29791,29792,29793,29794,29795,29796,29797,29798,29799,29800,29801,29802,29803,29804,29805,29806,29807,29808,29809,29810,29811,29812,29813,29814,29815,29816,29817,29818,29819,29820,29821,29822,29823,29824,29825,29826,29827,29828,29829,29830,29831,29832,29833,29834,29835,29836,29837,29838,29839,29840,29841,29842,29843,29844,29845,29846,29847,29848,29849,29850,29851,29852,29853,29854,29855,29856,29857,29858,29859,29860,29861,29862,29863,29864,29865,29866,29867,29868,29869,29870,29871,29872,29873,29874,29875,29876,29877,29878,29879,29880,29881,29882,29883,29884,29885,29886,29887,29888,29889,29890,29891,29892,29893,29894,29895,29896,29897,29898,29899,29900,29901,29902,29903,29904,29905,29906,29907,29908,29909,29910,29911,29912,29913,29914,29915,29916,29917,29918,29919,29920,29921,29922,29923,29924,29925,29926,29927,29928,29929,29930,29931,29932,29933,29934,29935,29936,29937,29938,29939,29940,29941,29942,29943,29944,29945,29946,29947,29948,29949,29950,29951,29952,29953,29954,29955,29956,29957,29958,29959,29960,29961,29962,29963,29964,29965,29966,29967,29968,29969,29970,29971,29972,29973,29974,29975,29976,29977,29978,29979,29980,29981,29982,29983,29984,29985,29986,29987,29988,29989,29990,29991,29992,29993,29994,29995,29996,29997,29998,29999,30000,30001,30002,30003,30004,30005,30006,30007,30008,30009,30010,30011,30012,30013,30014,30015,30016,30017,30018,30019,30020,30021,30022,30023,30024,30025,30026,30027,30028,30029,30030,30031,30032,30033,30034,30035,30036,30037,30038,30039,30040,30041,30042,30043,30044,30045,30046,30047,30048,30049,30050,30051,30052,30053,30054,30055,30056,30057,30058,30059,30060,30061,30062,30063,30064,30065,30066,30067,30068,30069,30070,30071,30072,30073,30074,30075,30076,30077,30078,30079,30080,30081,30082,30083,30084,30085,30086,30087,30088,30089,30090,30091,30092,30093,30094,30095,30096,30097,30098,30099,30100,30101,30102,30103,30104,30105,30106,30107,30108,30109,30110,30111,30112,30113,30114,30115,30116,30117,30118,30119,30120,30121,30122,30123,30124,30125,30126,30127,30128,30129,30130,30131,30132,30133,30134,30135,30136,30137,30138,30139,30140,30141,30142,30143,30144,30145,30146,30147,30148,30149,30150,30151,30152,30153,30154,30155,30156,30157,30158,30159,30160,30161,30162,30163,30164,30165,30166,30167,30168,30169,30170,30171,30172,30173,30174,30175,30176,30177,30178,30179,30180,30181,30182,30183,30184,30185,30186,30187,30188,30189,30190,30191,30192,30193,30194,30195,30196,30197,30198,30199,30200,30201,30202,30203,30204,30205,30206,30207,30208,30209,30210,30211,30212,30213,30214,30215,30216,30217,30218,30219,30220,30221,30222,30223,30224,30225,30226,30227,30228,30229,30230,30231,30232,30233,30234,30235,30236,30237,30238,30239,30240,30241,30242,30243,30244,30245,30246,30247,30248,30249,30250,30251,30252,30253,30254,30255,30256,30257,30258,30259,30260,30261,30262,30263,30264,30265,30266,30267,30268,30269,30270,30271,30272,30273,30274,30275,30276,30277,30278,30279,30280,30281,30282,30283,30284,30285,30286,30287,30288,30289,30290,30291,30292,30293,30294,30295,30296,30297,30298,30299,30300,30301,30302,30303,30304,30305,30306,30307,30308,30309,30310,30311,30312,30313,30314,30315,30316,30317,30318,30319,30320,30321,30322,30323,30324,30325,30326,30327,30328,30329,30330,30331,30332,30333,30334,30335,30336,30337,30338,30339,30340,30341,30342,30343,30344,30345,30346,30347,30348,30349,30350,30351,30352,30353,30354,30355,30356,30357,30358,30359,30360,30361,30362,30363,30364,30365,30366,30367,30368,30369,30370,30371,30372,30373,30374,30375,30376,30377,30378,30379,30380,30381,30382,30383,30384,30385,30386,30387,30388,30389,30390,30391,30392,30393,30394,30395,30396,30397,30398,30399,30400,30401,30402,30403,30404,30405,30406,30407,30408,30409,30410,30411,30412,30413,30414,30415,30416,30417,30418,30419,30420,30421,30422,30423,30424,30425,30426,30427,30428,30429,30430,30431,30432,30433,30434,30435,30436,30437,30438,30439,30440,30441,30442,30443,30444,30445,30446,30447,30448,30449,30450,30451,30452,30453,30454,30455,30456,30457,30458,30459,30460,30461,30462,30463,30464,30465,30466,30467,30468,30469,30470,30471,30472,30473,30474,30475,30476,30477,30478,30479,30480,30481,30482,30483,30484,30485,30486,30487,30488,30489,30490,30491,30492,30493,30494,30495,30496,30497,30498,30499,30500,30501,30502,30503,30504,30505,30506,30507,30508,30509,30510,30511,30512,30513,30514,30515,30516,30517,30518,30519,30520,30521,30522,30523,30524,30525,30526,30527,30528,30529,30530,30531,30532,30533,30534,30535,30536,30537,30538,30539,30540,30541,30542,30543,30544,30545,30546,30547,30548,30549,30550,30551,30552,30553,30554,30555,30556,30557,30558,30559,30560,30561,30562,30563,30564,30565,30566,30567,30568,30569,30570,30571,30572,30573,30574,30575,30576,30577,30578,30579,30580,30581,30582,30583,30584,30585,30586,30587,30588,30589,30590,30591,30592,30593,30594,30595,30596,30597,30598,30599,30600,30601,30602,30603,30604,30605,30606,30607,30608,30609,30610,30611,30612,30613,30614,30615,30616,30617,30618,30619,30620,30621,30622,30623,30624,30625,30626,30627,30628,30629,30630,30631,30632,30633,30634,30635,30636,30637,30638,30639,30640,30641,30642,30643,30644,30645,30646,30647,30648,30649,30650,30651,30652,30653,30654,30655,30656,30657,30658,30659,30660,30661,30662,30663,30664,30665,30666,30667,30668,30669,30670,30671,30672,30673,30674,30675,30676,30677,30678,30679,30680,30681,30682,30683,30684,30685,30686,30687,30688,30689,30690,30691,30692,30693,30694,30695,30696,30697,30698,30699,30700,30701,30702,30703,30704,30705,30706,30707,30708,30709,30710,30711,30712,30713,30714,30715,30716,30717,30718,30719,30720,30721,30722,30723,30724,30725,30726,30727,30728,30729,30730,30731,30732,30733,30734,30735,30736,30737,30738,30739,30740,30741,30742,30743,30744,30745,30746,30747,30748,30749,30750,30751,30752,30753,30754,30755,30756,30757,30758,30759,30760,30761,30762,30763,30764,30765,30766,30767,30768,30769,30770,30771,30772,30773,30774,30775,30776,30777,30778,30779,30780,30781,30782,30783,30784,30785,30786,30787,30788,30789,30790,30791,30792,30793,30794,30795,30796,30797,30798,30799,30800,30801,30802,30803,30804,30805,30806,30807,30808,30809,30810,30811,30812,30813,30814,30815,30816,30817,30818,30819,30820,30821,30822,30823,30824,30825,30826,30827,30828,30829,30830,30831,30832,30833,30834,30835,30836,30837,30838,30839,30840,30841,30842,30843,30844,30845,30846,30847,30848,30849,30850,30851,30852,30853,30854,30855,30856,30857,30858,30859,30860,30861,30862,30863,30864,30865,30866,30867,30868,30869,30870,30871,30872,30873,30874,30875,30876,30877,30878,30879,30880,30881,30882,30883,30884,30885,30886,30887,30888,30889,30890,30891,30892,30893,30894,30895,30896,30897,30898,30899,30900,30901,30902,30903,30904,30905,30906,30907,30908,30909,30910,30911,30912,30913,30914,30915,30916,30917,30918,30919,30920,30921,30922,30923,30924,30925,30926,30927,30928,30929,30930,30931,30932,30933,30934,30935,30936,30937,30938,30939,30940,30941,30942,30943,30944,30945,30946,30947,30948,30949,30950,30951,30952,30953,30954,30955,30956,30957,30958,30959,30960,30961,30962,30963,30964,30965,30966,30967,30968,30969,30970,30971,30972,30973,30974,30975,30976,30977,30978,30979,30980,30981,30982,30983,30984,30985,30986,30987,30988,30989,30990,30991,30992,30993,30994,30995,30996,30997,30998,30999,31000,31001,31002,31003,31004,31005,31006,31007,31008,31009,31010,31011,31012,31013,31014,31015,31016,31017,31018,31019,31020,31021,31022,31023,31024,31025,31026,31027,31028,31029,31030,31031,31032,31033,31034,31035,31036,31037,31038,31039,31040,31041,31042,31043,31044,31045,31046,31047,31048,31049,31050,31051,31052,31053,31054,31055,31056,31057,31058,31059,31060,31061,31062,31063,31064,31065,31066,31067,31068,31069,31070,31071,31072,31073,31074,31075,31076,31077,31078,31079,31080,31081,31082,31083,31084,31085,31086,31087,31088,31089,31090,31091,31092,31093,31094,31095,31096,31097,31098,31099,31100,31101,31102,31103,31104,31105,31106,31107,31108,31109,31110,31111,31112,31113,31114,31115,31116,31117,31118,31119,31120,31121,31122,31123,31124,31125,31126,31127,31128,31129,31130,31131,31132,31133,31134,31135,31136,31137,31138,31139,31140,31141,31142,31143,31144,31145,31146,31147,31148,31149,31150,31151,31152,31153,31154,31155,31156,31157,31158,31159,31160,31161,31162,31163,31164,31165,31166,31167,31168,31169,31170,31171,31172,31173,31174,31175,31176,31177,31178,31179,31180,31181,31182,31183,31184,31185,31186,31187,31188,31189,31190,31191,31192,31193,31194,31195,31196,31197,31198,31199,31200,31201,31202,31203,31204,31205,31206,31207,31208,31209,31210,31211,31212,31213,31214,31215,31216,31217,31218,31219,31220,31221,31222,31223,31224,31225,31226,31227,31228,31229,31230,31231,31232,31233,31234,31235,31236,31237,31238,31239,31240,31241,31242,31243,31244,31245,31246,31247,31248,31249,31250,31251,31252,31253,31254,31255,31256,31257,31258,31259,31260,31261,31262,31263,31264,31265,31266,31267,31268,31269,31270,31271,31272,31273,31274,31275,31276,31277,31278,31279,31280,31281,31282,31283,31284,31285,31286,31287,31288,31289,31290,31291,31292,31293,31294,31295,31296,31297,31298,31299,31300,31301,31302,31303,31304,31305,31306,31307,31308,31309,31310,31311,31312,31313,31314,31315,31316,31317,31318,31319,31320,31321,31322,31323,31324,31325,31326,31327,31328,31329,31330,31331,31332,31333,31334,31335,31336,31337,31338,31339,31340,31341,31342,31343,31344,31345,31346,31347,31348,31349,31350,31351,31352,31353,31354,31355,31356,31357,31358,31359,31360,31361,31362,31363,31364,31365,31366,31367,31368,31369,31370,31371,31372,31373,31374,31375,31376,31377,31378,31379,31380,31381,31382,31383,31384,31385,31386,31387,31388,31389,31390,31391,31392,31393,31394,31395,31396,31397,31398,31399,31400,31401,31402,31403,31404,31405,31406,31407,31408,31409,31410,31411,31412,31413,31414,31415,31416,31417,31418,31419,31420,31421,31422,31423,31424,31425,31426,31427,31428,31429,31430,31431,31432,31433,31434,31435,31436,31437,31438,31439,31440,31441,31442,31443,31444,31445,31446,31447,31448,31449,31450,31451,31452,31453,31454,31455,31456,31457,31458,31459,31460,31461,31462,31463,31464,31465,31466,31467,31468,31469,31470,31471,31472,31473,31474,31475,31476,31477,31478,31479,31480,31481,31482,31483,31484,31485,31486,31487,31488,31489,31490,31491,31492,31493,31494,31495,31496,31497,31498,31499,31500,31501,31502,31503,31504,31505,31506,31507,31508,31509,31510,31511,31512,31513,31514,31515,31516,31517,31518,31519,31520,31521,31522,31523,31524,31525,31526,31527,31528,31529,31530,31531,31532,31533,31534,31535,31536,31537,31538,31539,31540,31541,31542,31543,31544,31545,31546,31547,31548,31549,31550,31551,31552,31553,31554,31555,31556,31557,31558,31559,31560,31561,31562,31563,31564,31565,31566,31567,31568,31569,31570,31571,31572,31573,31574,31575,31576,31577,31578,31579,31580,31581,31582,31583,31584,31585,31586,31587,31588,31589,31590,31591,31592,31593,31594,31595,31596,31597,31598,31599,31600,31601,31602,31603,31604,31605,31606,31607,31608,31609,31610,31611,31612,31613,31614,31615,31616,31617,31618,31619,31620,31621,31622,31623,31624,31625,31626,31627,31628,31629,31630,31631,31632,31633,31634,31635,31636,31637,31638,31639,31640,31641,31642,31643,31644,31645,31646,31647,31648,31649,31650,31651,31652,31653,31654,31655,31656,31657,31658,31659,31660,31661,31662,31663,31664,31665,31666,31667,31668,31669,31670,31671,31672,31673,31674,31675,31676,31677,31678,31679,31680,31681,31682,31683,31684,31685,31686,31687,31688,31689,31690,31691,31692,31693,31694,31695,31696,31697,31698,31699,31700,31701,31702,31703,31704,31705,31706,31707,31708,31709,31710,31711,31712,31713,31714,31715,31716,31717,31718,31719,31720,31721,31722,31723,31724,31725,31726,31727,31728,31729,31730,31731,31732,31733,31734,31735,31736,31737,31738,31739,31740,31741,31742,31743,31744,31745,31746,31747,31748,31749,31750,31751,31752,31753,31754,31755,31756,31757,31758,31759,31760,31761,31762,31763,31764,31765,31766,31767,31768,31769,31770,31771,31772,31773,31774,31775,31776,31777,31778,31779,31780,31781,31782,31783,31784,31785,31786,31787,31788,31789,31790,31791,31792,31793,31794,31795,31796,31797,31798,31799,31800,31801,31802,31803,31804,31805,31806,31807,31808,31809,31810,31811,31812,31813,31814,31815,31816,31817,31818,31819,31820,31821,31822,31823,31824,31825,31826,31827,31828,31829,31830,31831,31832,31833,31834,31835,31836,31837,31838,31839,31840,31841,31842,31843,31844,31845,31846,31847,31848,31849,31850,31851,31852,31853,31854,31855,31856,31857,31858,31859,31860,31861,31862,31863,31864,31865,31866,31867,31868,31869,31870,31871,31872,31873,31874,31875,31876,31877,31878,31879,31880,31881,31882,31883,31884,31885,31886,31887,31888,31889,31890,31891,31892,31893,31894,31895,31896,31897,31898,31899,31900,31901,31902,31903,31904,31905,31906,31907,31908,31909,31910,31911,31912,31913,31914,31915,31916,31917,31918,31919,31920,31921,31922,31923,31924,31925,31926,31927,31928,31929,31930,31931,31932,31933,31934,31935,31936,31937,31938,31939,31940,31941,31942,31943,31944,31945,31946,31947,31948,31949,31950,31951,31952,31953,31954,31955,31956,31957,31958,31959,31960,31961,31962,31963,31964,31965,31966,31967,31968,31969,31970,31971,31972,31973,31974,31975,31976,31977,31978,31979,31980,31981,31982,31983,31984,31985,31986,31987,31988,31989,31990,31991,31992,31993,31994,31995,31996,31997,31998,31999,32000,32001,32002,32003,32004,32005,32006,32007,32008,32009,32010,32011,32012,32013,32014,32015,32016,32017,32018,32019,32020,32021,32022,32023,32024,32025,32026,32027,32028,32029,32030,32031,32032,32033,32034,32035,32036,32037,32038,32039,32040,32041,32042,32043,32044,32045,32046,32047,32048,32049,32050,32051,32052,32053,32054,32055,32056,32057,32058,32059,32060,32061,32062,32063,32064,32065,32066,32067,32068,32069,32070,32071,32072,32073,32074,32075,32076,32077,32078,32079,32080,32081,32082,32083,32084,32085,32086,32087,32088,32089,32090,32091,32092,32093,32094,32095,32096,32097,32098,32099,32100,32101,32102,32103,32104,32105,32106,32107,32108,32109,32110,32111,32112,32113,32114,32115,32116,32117,32118,32119,32120,32121,32122,32123,32124,32125,32126,32127,32128,32129,32130,32131,32132,32133,32134,32135,32136,32137,32138,32139,32140,32141,32142,32143,32144,32145,32146,32147,32148,32149,32150,32151,32152,32153,32154,32155,32156,32157,32158,32159,32160,32161,32162,32163,32164,32165,32166,32167,32168,32169,32170,32171,32172,32173,32174,32175,32176,32177,32178,32179,32180,32181,32182,32183,32184,32185,32186,32187,32188,32189,32190,32191,32192,32193,32194,32195,32196,32197,32198,32199,32200,32201,32202,32203,32204,32205,32206,32207,32208,32209,32210,32211,32212,32213,32214,32215,32216,32217,32218,32219,32220,32221,32222,32223,32224,32225,32226,32227,32228,32229,32230,32231,32232,32233,32234,32235,32236,32237,32238,32239,32240,32241,32242,32243,32244,32245,32246,32247,32248,32249,32250,32251,32252,32253,32254,32255,32256,32257,32258,32259,32260,32261,32262,32263,32264,32265,32266,32267,32268,32269,32270,32271,32272,32273,32274,32275,32276,32277,32278,32279,32280,32281,32282,32283,32284,32285,32286,32287,32288,32289,32290,32291,32292,32293,32294,32295,32296,32297,32298,32299,32300,32301,32302,32303,32304,32305,32306,32307,32308,32309,32310,32311,32312,32313,32314,32315,32316,32317,32318,32319,32320,32321,32322,32323,32324,32325,32326,32327,32328,32329,32330,32331,32332,32333,32334,32335,32336,32337,32338,32339,32340,32341,32342,32343,32344,32345,32346,32347,32348,32349,32350,32351,32352,32353,32354,32355,32356,32357,32358,32359,32360,32361,32362,32363,32364,32365,32366,32367,32368,32369,32370,32371,32372,32373,32374,32375,32376,32377,32378,32379,32380,32381,32382,32383,32384,32385,32386,32387,32388,32389,32390,32391,32392,32393,32394,32395,32396,32397,32398,32399,32400,32401,32402,32403,32404,32405,32406,32407,32408,32409,32410,32411,32412,32413,32414,32415,32416,32417,32418,32419,32420,32421,32422,32423,32424,32425,32426,32427,32428,32429,32430,32431,32432,32433,32434,32435,32436,32437,32438,32439,32440,32441,32442,32443,32444,32445,32446,32447,32448,32449,32450,32451,32452,32453,32454,32455,32456,32457,32458,32459,32460,32461,32462,32463,32464,32465,32466,32467,32468,32469,32470,32471,32472,32473,32474,32475,32476,32477,32478,32479,32480,32481,32482,32483,32484,32485,32486,32487,32488,32489,32490,32491,32492,32493,32494,32495,32496,32497,32498,32499,32500,32501,32502,32503,32504,32505,32506,32507,32508,32509,32510,32511,32512,32513,32514,32515,32516,32517,32518,32519,32520,32521,32522,32523,32524,32525,32526,32527,32528,32529,32530,32531,32532,32533,32534,32535,32536,32537,32538,32539,32540,32541,32542,32543,32544,32545,32546,32547,32548,32549,32550,32551,32552,32553,32554,32555,32556,32557,32558,32559,32560,32561,32562,32563,32564,32565,32566,32567,32568,32569,32570,32571,32572,32573,32574,32575,32576,32577,32578,32579,32580,32581,32582,32583,32584,32585,32586,32587,32588,32589,32590,32591,32592,32593,32594,32595,32596,32597,32598,32599,32600,32601,32602,32603,32604,32605,32606,32607,32608,32609,32610,32611,32612,32613,32614,32615,32616,32617,32618,32619,32620,32621,32622,32623,32624,32625,32626,32627,32628,32629,32630,32631,32632,32633,32634,32635,32636,32637,32638,32639,32640,32641,32642,32643,32644,32645,32646,32647,32648,32649,32650,32651,32652,32653,32654,32655,32656,32657,32658,32659,32660,32661,32662,32663,32664,32665,32666,32667,32668,32669,32670,32671,32672,32673,32674,32675,32676,32677,32678,32679,32680,32681,32682,32683,32684,32685,32686,32687,32688,32689,32690,32691,32692,32693,32694,32695,32696,32697,32698,32699,32700,32701,32702,32703,32704,32705,32706,32707,32708,32709,32710,32711,32712,32713,32714,32715,32716,32717,32718,32719,32720,32721,32722,32723,32724,32725,32726,32727,32728,32729,32730,32731,32732,32733,32734,32735,32736,32737,32738,32739,32740,32741,32742,32743,32744,32745,32746,32747,32748,32749,32750,32751,32752,32753,32754,32755,32756,32757,32758,32759,32760,32761,32762,32763,32764,32765,32766,32767,32768,32769,32770,32771,32772,32773,32774,32775,32776,32777,32778,32779,32780,32781,32782,32783,32784,32785,32786,32787,32788,32789,32790,32791,32792,32793,32794,32795,32796,32797,32798,32799,32800,32801,32802,32803,32804,32805,32806,32807,32808,32809,32810,32811,32812,32813,32814,32815,32816,32817,32818,32819,32820,32821,32822,32823,32824,32825,32826,32827,32828,32829,32830,32831,32832,32833,32834,32835,32836,32837,32838,32839,32840,32841,32842,32843,32844,32845,32846,32847,32848,32849,32850,32851,32852,32853,32854,32855,32856,32857,32858,32859,32860,32861,32862,32863,32864,32865,32866,32867,32868,32869,32870,32871,32872,32873,32874,32875,32876,32877,32878,32879,32880,32881,32882,32883,32884,32885,32886,32887,32888,32889,32890,32891,32892,32893,32894,32895,32896,32897,32898,32899,32900,32901,32902,32903,32904,32905,32906,32907,32908,32909,32910,32911,32912,32913,32914,32915,32916,32917,32918,32919,32920,32921,32922,32923,32924,32925,32926,32927,32928,32929,32930,32931,32932,32933,32934,32935,32936,32937,32938,32939,32940,32941,32942,32943,32944,32945,32946,32947,32948,32949,32950,32951,32952,32953,32954,32955,32956,32957,32958,32959,32960,32961,32962,32963,32964,32965,32966,32967,32968,32969,32970,32971,32972,32973,32974,32975,32976,32977,32978,32979,32980,32981,32982,32983,32984,32985,32986,32987,32988,32989,32990,32991,32992,32993,32994,32995,32996,32997,32998,32999,33000,33001,33002,33003,33004,33005,33006,33007,33008,33009,33010,33011,33012,33013,33014,33015,33016,33017,33018,33019,33020,33021,33022,33023,33024,33025,33026,33027,33028,33029,33030,33031,33032,33033,33034,33035,33036,33037,33038,33039,33040,33041,33042,33043,33044,33045,33046,33047,33048,33049,33050,33051,33052,33053,33054,33055,33056,33057,33058,33059,33060,33061,33062,33063,33064,33065,33066,33067,33068,33069,33070,33071,33072,33073,33074,33075,33076,33077,33078,33079,33080,33081,33082,33083,33084,33085,33086,33087,33088,33089,33090,33091,33092,33093,33094,33095,33096,33097,33098,33099,33100,33101,33102,33103,33104,33105,33106,33107,33108,33109,33110,33111,33112,33113,33114,33115,33116,33117,33118,33119,33120,33121,33122,33123,33124,33125,33126,33127,33128,33129,33130,33131,33132,33133,33134,33135,33136,33137,33138,33139,33140,33141,33142,33143,33144,33145,33146,33147,33148,33149,33150,33151,33152,33153,33154,33155,33156,33157,33158,33159,33160,33161,33162,33163,33164,33165,33166,33167,33168,33169,33170,33171,33172,33173,33174,33175,33176,33177,33178,33179,33180,33181,33182,33183,33184,33185,33186,33187,33188,33189,33190,33191,33192,33193,33194,33195,33196,33197,33198,33199,33200,33201,33202,33203,33204,33205,33206,33207,33208,33209,33210,33211,33212,33213,33214,33215,33216,33217,33218,33219,33220,33221,33222,33223,33224,33225,33226,33227,33228,33229,33230,33231,33232,33233,33234,33235,33236,33237,33238,33239,33240,33241,33242,33243,33244,33245,33246,33247,33248,33249,33250,33251,33252,33253,33254,33255,33256,33257,33258,33259,33260,33261,33262,33263,33264,33265,33266,33267,33268,33269,33270,33271,33272,33273,33274,33275,33276,33277,33278,33279,33280,33281,33282,33283,33284,33285,33286,33287,33288,33289,33290,33291,33292,33293,33294,33295,33296,33297,33298,33299,33300,33301,33302,33303,33304,33305,33306,33307,33308,33309,33310,33311,33312,33313,33314,33315,33316,33317,33318,33319,33320,33321,33322,33323,33324,33325,33326,33327,33328,33329,33330,33331,33332,33333,33334,33335,33336,33337,33338,33339,33340,33341,33342,33343,33344,33345,33346,33347,33348,33349,33350,33351,33352,33353,33354,33355,33356,33357,33358,33359,33360,33361,33362,33363,33364,33365,33366,33367,33368,33369,33370,33371,33372,33373,33374,33375,33376,33377,33378,33379,33380,33381,33382,33383,33384,33385,33386,33387,33388,33389,33390,33391,33392,33393,33394,33395,33396,33397,33398,33399,33400,33401,33402,33403,33404,33405,33406,33407,33408,33409,33410,33411,33412,33413,33414,33415,33416,33417,33418,33419,33420,33421,33422,33423,33424,33425,33426,33427,33428,33429,33430,33431,33432,33433,33434,33435,33436,33437,33438,33439,33440,33441,33442,33443,33444,33445,33446,33447,33448,33449,33450,33451,33452,33453,33454,33455,33456,33457,33458,33459,33460,33461,33462,33463,33464,33465,33466,33467,33468,33469,33470,33471,33472,33473,33474,33475,33476,33477,33478,33479,33480,33481,33482,33483,33484,33485,33486,33487,33488,33489,33490,33491,33492,33493,33494,33495,33496,33497,33498,33499,33500,33501,33502,33503,33504,33505,33506,33507,33508,33509,33510,33511,33512,33513,33514,33515,33516,33517,33518,33519,33520,33521,33522,33523,33524,33525,33526,33527,33528,33529,33530,33531,33532,33533,33534,33535,33536,33537,33538,33539,33540,33541,33542,33543,33544,33545,33546,33547,33548,33549,33550,33551,33552,33553,33554,33555,33556,33557,33558,33559,33560,33561,33562,33563,33564,33565,33566,33567,33568,33569,33570,33571,33572,33573,33574,33575,33576,33577,33578,33579,33580,33581,33582,33583,33584,33585,33586,33587,33588,33589,33590,33591,33592,33593,33594,33595,33596,33597,33598,33599,33600,33601,33602,33603,33604,33605,33606,33607,33608,33609,33610,33611,33612,33613,33614,33615,33616,33617,33618,33619,33620,33621,33622,33623,33624,33625,33626,33627,33628,33629,33630,33631,33632,33633,33634,33635,33636,33637,33638,33639,33640,33641,33642,33643,33644,33645,33646,33647,33648,33649,33650,33651,33652,33653,33654,33655,33656,33657,33658,33659,33660,33661,33662,33663,33664,33665,33666,33667,33668,33669,33670,33671,33672,33673,33674,33675,33676,33677,33678,33679,33680,33681,33682,33683,33684,33685,33686,33687,33688,33689,33690,33691,33692,33693,33694,33695,33696,33697,33698,33699,33700,33701,33702,33703,33704,33705,33706,33707,33708,33709,33710,33711,33712,33713,33714,33715,33716,33717,33718,33719,33720,33721,33722,33723,33724,33725,33726,33727,33728,33729,33730,33731,33732,33733,33734,33735,33736,33737,33738,33739,33740,33741,33742,33743,33744,33745,33746,33747,33748,33749,33750,33751,33752,33753,33754,33755,33756,33757,33758,33759,33760,33761,33762,33763,33764,33765,33766,33767,33768,33769,33770,33771,33772,33773,33774,33775,33776,33777,33778,33779,33780,33781,33782,33783,33784,33785,33786,33787,33788,33789,33790,33791,33792,33793,33794,33795,33796,33797,33798,33799,33800,33801,33802,33803,33804,33805,33806,33807,33808,33809,33810,33811,33812,33813,33814,33815,33816,33817,33818,33819,33820,33821,33822,33823,33824,33825,33826,33827,33828,33829,33830,33831,33832,33833,33834,33835,33836,33837,33838,33839,33840,33841,33842,33843,33844,33845,33846,33847,33848,33849,33850,33851,33852,33853,33854,33855,33856,33857,33858,33859,33860,33861,33862,33863,33864,33865,33866,33867,33868,33869,33870,33871,33872,33873,33874,33875,33876,33877,33878,33879,33880,33881,33882,33883,33884,33885,33886,33887,33888,33889,33890,33891,33892,33893,33894,33895,33896,33897,33898,33899,33900,33901,33902,33903,33904,33905,33906,33907,33908,33909,33910,33911,33912,33913,33914,33915,33916,33917,33918,33919,33920,33921,33922,33923,33924,33925,33926,33927,33928,33929,33930,33931,33932,33933,33934,33935,33936,33937,33938,33939,33940,33941,33942,33943,33944,33945,33946,33947,33948,33949,33950,33951,33952,33953,33954,33955,33956,33957,33958,33959,33960,33961,33962,33963,33964,33965,33966,33967,33968,33969,33970,33971,33972,33973,33974,33975,33976,33977,33978,33979,33980,33981,33982,33983,33984,33985,33986,33987,33988,33989,33990,33991,33992,33993,33994,33995,33996,33997,33998,33999,34000,34001,34002,34003,34004,34005,34006,34007,34008,34009,34010,34011,34012,34013,34014,34015,34016,34017,34018,34019,34020,34021,34022,34023,34024,34025,34026,34027,34028,34029,34030,34031,34032,34033,34034,34035,34036,34037,34038,34039,34040,34041,34042,34043,34044,34045,34046,34047,34048,34049,34050,34051,34052,34053,34054,34055,34056,34057,34058,34059,34060,34061,34062,34063,34064,34065,34066,34067,34068,34069,34070,34071,34072,34073,34074,34075,34076,34077,34078,34079,34080,34081,34082,34083,34084,34085,34086,34087,34088,34089,34090,34091,34092,34093,34094,34095,34096,34097,34098,34099,34100,34101,34102,34103,34104,34105,34106,34107,34108,34109,34110,34111,34112,34113,34114,34115,34116,34117,34118,34119,34120,34121,34122,34123,34124,34125,34126,34127,34128,34129,34130,34131,34132,34133,34134,34135,34136,34137,34138,34139,34140,34141,34142,34143,34144,34145,34146,34147,34148,34149,34150,34151,34152,34153,34154,34155,34156,34157,34158,34159,34160,34161,34162,34163,34164,34165,34166,34167,34168,34169,34170,34171,34172,34173,34174,34175,34176,34177,34178,34179,34180,34181,34182,34183,34184,34185,34186,34187,34188,34189,34190,34191,34192,34193,34194,34195,34196,34197,34198,34199,34200,34201,34202,34203,34204,34205,34206,34207,34208,34209,34210,34211,34212,34213,34214,34215,34216,34217,34218,34219,34220,34221,34222,34223,34224,34225,34226,34227,34228,34229,34230,34231,34232,34233,34234,34235,34236,34237,34238,34239,34240,34241,34242,34243,34244,34245,34246,34247,34248,34249,34250,34251,34252,34253,34254,34255,34256,34257,34258,34259,34260,34261,34262,34263,34264,34265,34266,34267,34268,34269,34270,34271,34272,34273,34274,34275,34276,34277,34278,34279,34280,34281,34282,34283,34284,34285,34286,34287,34288,34289,34290,34291,34292,34293,34294,34295,34296,34297,34298,34299,34300,34301,34302,34303,34304,34305,34306,34307,34308,34309,34310,34311,34312,34313,34314,34315,34316,34317,34318,34319,34320,34321,34322,34323,34324,34325,34326,34327,34328,34329,34330,34331,34332,34333,34334,34335,34336,34337,34338,34339,34340,34341,34342,34343,34344,34345,34346,34347,34348,34349,34350,34351,34352,34353,34354,34355,34356,34357,34358,34359,34360,34361,34362,34363,34364,34365,34366,34367,34368,34369,34370,34371,34372,34373,34374,34375,34376,34377,34378,34379,34380,34381,34382,34383,34384,34385,34386,34387,34388,34389,34390,34391,34392,34393,34394,34395,34396,34397,34398,34399,34400,34401,34402,34403,34404,34405,34406,34407,34408,34409,34410,34411,34412,34413,34414,34415,34416,34417,34418,34419,34420,34421,34422,34423,34424,34425,34426,34427,34428,34429,34430,34431,34432,34433,34434,34435,34436,34437,34438,34439,34440,34441,34442,34443,34444,34445,34446,34447,34448,34449,34450,34451,34452,34453,34454,34455,34456,34457,34458,34459,34460,34461,34462,34463,34464,34465,34466,34467,34468,34469,34470,34471,34472,34473,34474,34475,34476,34477,34478,34479,34480,34481,34482,34483,34484,34485,34486,34487,34488,34489,34490,34491,34492,34493,34494,34495,34496,34497,34498,34499,34500,34501,34502,34503,34504,34505,34506,34507,34508,34509,34510,34511,34512,34513,34514,34515,34516,34517,34518,34519,34520,34521,34522,34523,34524,34525,34526,34527,34528,34529,34530,34531,34532,34533,34534,34535,34536,34537,34538,34539,34540,34541,34542,34543,34544,34545,34546,34547,34548,34549,34550,34551,34552,34553,34554,34555,34556,34557,34558,34559,34560,34561,34562,34563,34564,34565,34566,34567,34568,34569,34570,34571,34572,34573,34574,34575,34576,34577,34578,34579,34580,34581,34582,34583,34584,34585,34586,34587,34588,34589,34590,34591,34592,34593,34594,34595,34596,34597,34598,34599,34600,34601,34602,34603,34604,34605,34606,34607,34608,34609,34610,34611,34612,34613,34614,34615,34616,34617,34618,34619,34620,34621,34622,34623,34624,34625,34626,34627,34628,34629,34630,34631,34632,34633,34634,34635,34636,34637,34638,34639,34640,34641,34642,34643,34644,34645,34646,34647,34648,34649,34650,34651,34652,34653,34654,34655,34656,34657,34658,34659,34660,34661,34662,34663,34664,34665,34666,34667,34668,34669,34670,34671,34672,34673,34674,34675,34676,34677,34678,34679,34680,34681,34682,34683,34684,34685,34686,34687,34688,34689,34690,34691,34692,34693,34694,34695,34696,34697,34698,34699,34700,34701,34702,34703,34704,34705,34706,34707,34708,34709,34710,34711,34712,34713,34714,34715,34716,34717,34718,34719,34720,34721,34722,34723,34724,34725,34726,34727,34728,34729,34730,34731,34732,34733,34734,34735,34736,34737,34738,34739,34740,34741,34742,34743,34744,34745,34746,34747,34748,34749,34750,34751,34752,34753,34754,34755,34756,34757,34758,34759,34760,34761,34762,34763,34764,34765,34766,34767,34768,34769,34770,34771,34772,34773,34774,34775,34776,34777,34778,34779,34780,34781,34782,34783,34784,34785,34786,34787,34788,34789,34790,34791,34792,34793,34794,34795,34796,34797,34798,34799,34800,34801,34802,34803,34804,34805,34806,34807,34808,34809,34810,34811,34812,34813,34814,34815,34816,34817,34818,34819,34820,34821,34822,34823,34824,34825,34826,34827,34828,34829,34830,34831,34832,34833,34834,34835,34836,34837,34838,34839,34840,34841,34842,34843,34844,34845,34846,34847,34848,34849,34850,34851,34852,34853,34854,34855,34856,34857,34858,34859,34860,34861,34862,34863,34864,34865,34866,34867,34868,34869,34870,34871,34872,34873,34874,34875,34876,34877,34878,34879,34880,34881,34882,34883,34884,34885,34886,34887,34888,34889,34890,34891,34892,34893,34894,34895,34896,34897,34898,34899,34900,34901,34902,34903,34904,34905,34906,34907,34908,34909,34910,34911,34912,34913,34914,34915,34916,34917,34918,34919,34920,34921,34922,34923,34924,34925,34926,34927,34928,34929,34930,34931,34932,34933,34934,34935,34936,34937,34938,34939,34940,34941,34942,34943,34944,34945,34946,34947,34948,34949,34950,34951,34952,34953,34954,34955,34956,34957,34958,34959,34960,34961,34962,34963,34964,34965,34966,34967,34968,34969,34970,34971,34972,34973,34974,34975,34976,34977,34978,34979,34980,34981,34982,34983,34984,34985,34986,34987,34988,34989,34990,34991,34992,34993,34994,34995,34996,34997,34998,34999,35000,35001,35002,35003,35004,35005,35006,35007,35008,35009,35010,35011,35012,35013,35014,35015,35016,35017,35018,35019,35020,35021,35022,35023,35024,35025,35026,35027,35028,35029,35030,35031,35032,35033,35034,35035,35036,35037,35038,35039,35040,35041,35042,35043,35044,35045,35046,35047,35048,35049,35050,35051,35052,35053,35054,35055,35056,35057,35058,35059,35060,35061,35062,35063,35064,35065,35066,35067,35068,35069,35070,35071,35072,35073,35074,35075,35076,35077,35078,35079,35080,35081,35082,35083,35084,35085,35086,35087,35088,35089,35090,35091,35092,35093,35094,35095,35096,35097,35098,35099,35100,35101,35102,35103,35104,35105,35106,35107,35108,35109,35110,35111,35112,35113,35114,35115,35116,35117,35118,35119,35120,35121,35122,35123,35124,35125,35126,35127,35128,35129,35130,35131,35132,35133,35134,35135,35136,35137,35138,35139,35140,35141,35142,35143,35144,35145,35146,35147,35148,35149,35150,35151,35152,35153,35154,35155,35156,35157,35158,35159,35160,35161,35162,35163,35164,35165,35166,35167,35168,35169,35170,35171,35172,35173,35174,35175,35176,35177,35178,35179,35180,35181,35182,35183,35184,35185,35186,35187,35188,35189,35190,35191,35192,35193,35194,35195,35196,35197,35198,35199,35200,35201,35202,35203,35204,35205,35206,35207,35208,35209,35210,35211,35212,35213,35214,35215,35216,35217,35218,35219,35220,35221,35222,35223,35224,35225,35226,35227,35228,35229,35230,35231,35232,35233,35234,35235,35236,35237,35238,35239,35240,35241,35242,35243,35244,35245,35246,35247,35248,35249,35250,35251,35252,35253,35254,35255,35256,35257,35258,35259,35260,35261,35262,35263,35264,35265,35266,35267,35268,35269,35270,35271,35272,35273,35274,35275,35276,35277,35278,35279,35280,35281,35282,35283,35284,35285,35286,35287,35288,35289,35290,35291,35292,35293,35294,35295,35296,35297,35298,35299,35300,35301,35302,35303,35304,35305,35306,35307,35308,35309,35310,35311,35312,35313,35314,35315,35316,35317,35318,35319,35320,35321,35322,35323,35324,35325,35326,35327,35328,35329,35330,35331,35332,35333,35334,35335,35336,35337,35338,35339,35340,35341,35342,35343,35344,35345,35346,35347,35348,35349,35350,35351,35352,35353,35354,35355,35356,35357,35358,35359,35360,35361,35362,35363,35364,35365,35366,35367,35368,35369,35370,35371,35372,35373,35374,35375,35376,35377,35378,35379,35380,35381,35382,35383,35384,35385,35386,35387,35388,35389,35390,35391,35392,35393,35394,35395,35396,35397,35398,35399,35400,35401,35402,35403,35404,35405,35406,35407,35408,35409,35410,35411,35412,35413,35414,35415,35416,35417,35418,35419,35420,35421,35422,35423,35424,35425,35426,35427,35428,35429,35430,35431,35432,35433,35434,35435,35436,35437,35438,35439,35440,35441,35442,35443,35444,35445,35446,35447,35448,35449,35450,35451,35452,35453,35454,35455,35456,35457,35458,35459,35460,35461,35462,35463,35464,35465,35466,35467,35468,35469,35470,35471,35472,35473,35474,35475,35476,35477,35478,35479,35480,35481,35482,35483,35484,35485,35486,35487,35488,35489,35490,35491,35492,35493,35494,35495,35496,35497,35498,35499,35500,35501,35502,35503,35504,35505,35506,35507,35508,35509,35510,35511,35512,35513,35514,35515,35516,35517,35518,35519,35520,35521,35522,35523,35524,35525,35526,35527,35528,35529,35530,35531,35532,35533,35534,35535,35536,35537,35538,35539,35540,35541,35542,35543,35544,35545,35546,35547,35548,35549,35550,35551,35552,35553,35554,35555,35556,35557,35558,35559,35560,35561,35562,35563,35564,35565,35566,35567,35568,35569,35570,35571,35572,35573,35574,35575,35576,35577,35578,35579,35580,35581,35582,35583,35584,35585,35586,35587,35588,35589,35590,35591,35592,35593,35594,35595,35596,35597,35598,35599,35600,35601,35602,35603,35604,35605,35606,35607,35608,35609,35610,35611,35612,35613,35614,35615,35616,35617,35618,35619,35620,35621,35622,35623,35624,35625,35626,35627,35628,35629,35630,35631,35632,35633,35634,35635,35636,35637,35638,35639,35640,35641,35642,35643,35644,35645,35646,35647,35648,35649,35650,35651,35652,35653,35654,35655,35656,35657,35658,35659,35660,35661,35662,35663,35664,35665,35666,35667,35668,35669,35670,35671,35672,35673,35674,35675,35676,35677,35678,35679,35680,35681,35682,35683,35684,35685,35686,35687,35688,35689,35690,35691,35692,35693,35694,35695,35696,35697,35698,35699,35700,35701,35702,35703,35704,35705,35706,35707,35708,35709,35710,35711,35712,35713,35714,35715,35716,35717,35718,35719,35720,35721,35722,35723,35724,35725,35726,35727,35728,35729,35730,35731,35732,35733,35734,35735,35736,35737,35738,35739,35740,35741,35742,35743,35744,35745,35746,35747,35748,35749,35750,35751,35752,35753,35754,35755,35756,35757,35758,35759,35760,35761,35762,35763,35764,35765,35766,35767,35768,35769,35770,35771,35772,35773,35774,35775,35776,35777,35778,35779,35780,35781,35782,35783,35784,35785,35786,35787,35788,35789,35790,35791,35792,35793,35794,35795,35796,35797,35798,35799,35800,35801,35802,35803,35804,35805,35806,35807,35808,35809,35810,35811,35812,35813,35814,35815,35816,35817,35818,35819,35820,35821,35822,35823,35824,35825,35826,35827,35828,35829,35830,35831,35832,35833,35834,35835,35836,35837,35838,35839,35840,35841,35842,35843,35844,35845,35846,35847,35848,35849,35850,35851,35852,35853,35854,35855,35856,35857,35858,35859,35860,35861,35862,35863,35864,35865,35866,35867,35868,35869,35870,35871,35872,35873,35874,35875,35876,35877,35878,35879,35880,35881,35882,35883,35884,35885,35886,35887,35888,35889,35890,35891,35892,35893,35894,35895,35896,35897,35898,35899,35900,35901,35902,35903,35904,35905,35906,35907,35908,35909,35910,35911,35912,35913,35914,35915,35916,35917,35918,35919,35920,35921,35922,35923,35924,35925,35926,35927,35928,35929,35930,35931,35932,35933,35934,35935,35936,35937,35938,35939,35940,35941,35942,35943,35944,35945,35946,35947,35948,35949,35950,35951,35952,35953,35954,35955,35956,35957,35958,35959,35960,35961,35962,35963,35964,35965,35966,35967,35968,35969,35970,35971,35972,35973,35974,35975,35976,35977,35978,35979,35980,35981,35982,35983,35984,35985,35986,35987,35988,35989,35990,35991,35992,35993,35994,35995,35996,35997,35998,35999,36000,36001,36002,36003,36004,36005,36006,36007,36008,36009,36010,36011,36012,36013,36014,36015,36016,36017,36018,36019,36020,36021,36022,36023,36024,36025,36026,36027,36028,36029,36030,36031,36032,36033,36034,36035,36036,36037,36038,36039,36040,36041,36042,36043,36044,36045,36046,36047,36048,36049,36050,36051,36052,36053,36054,36055,36056,36057,36058,36059,36060,36061,36062,36063,36064,36065,36066,36067,36068,36069,36070,36071,36072,36073,36074,36075,36076,36077,36078,36079,36080,36081,36082,36083,36084,36085,36086,36087,36088,36089,36090,36091,36092,36093,36094,36095,36096,36097,36098,36099,36100,36101,36102,36103,36104,36105,36106,36107,36108,36109,36110,36111,36112,36113,36114,36115,36116,36117,36118,36119,36120,36121,36122,36123,36124,36125,36126,36127,36128,36129,36130,36131,36132,36133,36134,36135,36136,36137,36138,36139,36140,36141,36142,36143,36144,36145,36146,36147,36148,36149,36150,36151,36152,36153,36154,36155,36156,36157,36158,36159,36160,36161,36162,36163,36164,36165,36166,36167,36168,36169,36170,36171,36172,36173,36174,36175,36176,36177,36178,36179,36180,36181,36182,36183,36184,36185,36186,36187,36188,36189,36190,36191,36192,36193,36194,36195,36196,36197,36198,36199,36200,36201,36202,36203,36204,36205,36206,36207,36208,36209,36210,36211,36212,36213,36214,36215,36216,36217,36218,36219,36220,36221,36222,36223,36224,36225,36226,36227,36228,36229,36230,36231,36232,36233,36234,36235,36236,36237,36238,36239,36240,36241,36242,36243,36244,36245,36246,36247,36248,36249,36250,36251,36252,36253,36254,36255,36256,36257,36258,36259,36260,36261,36262,36263,36264,36265,36266,36267,36268,36269,36270,36271,36272,36273,36274,36275,36276,36277,36278,36279,36280,36281,36282,36283,36284,36285,36286,36287,36288,36289,36290,36291,36292,36293,36294,36295,36296,36297,36298,36299,36300,36301,36302,36303,36304,36305,36306,36307,36308,36309,36310,36311,36312,36313,36314,36315,36316,36317,36318,36319,36320,36321,36322,36323,36324,36325,36326,36327,36328,36329,36330,36331,36332,36333,36334,36335,36336,36337,36338,36339,36340,36341,36342,36343,36344,36345,36346,36347,36348,36349,36350,36351,36352,36353,36354,36355,36356,36357,36358,36359,36360,36361,36362,36363,36364,36365,36366,36367,36368,36369,36370,36371,36372,36373,36374,36375,36376,36377,36378,36379,36380,36381,36382,36383,36384,36385,36386,36387,36388,36389,36390,36391,36392,36393,36394,36395,36396,36397,36398,36399,36400,36401,36402,36403,36404,36405,36406,36407,36408,36409,36410,36411,36412,36413,36414,36415,36416,36417,36418,36419,36420,36421,36422,36423,36424,36425,36426,36427,36428,36429,36430,36431,36432,36433,36434,36435,36436,36437,36438,36439,36440,36441,36442,36443,36444,36445,36446,36447,36448,36449,36450,36451,36452,36453,36454,36455,36456,36457,36458,36459,36460,36461,36462,36463,36464,36465,36466,36467,36468,36469,36470,36471,36472,36473,36474,36475,36476,36477,36478,36479,36480,36481,36482,36483,36484,36485,36486,36487,36488,36489,36490,36491,36492,36493,36494,36495,36496,36497,36498,36499,36500,36501,36502,36503,36504,36505,36506,36507,36508,36509,36510,36511,36512,36513,36514,36515,36516,36517,36518,36519,36520,36521,36522,36523,36524,36525,36526,36527,36528,36529,36530,36531,36532,36533,36534,36535,36536,36537,36538,36539,36540,36541,36542,36543,36544,36545,36546,36547,36548,36549,36550,36551,36552,36553,36554,36555,36556,36557,36558,36559,36560,36561,36562,36563,36564,36565,36566,36567,36568,36569,36570,36571,36572,36573,36574,36575,36576,36577,36578,36579,36580,36581,36582,36583,36584,36585,36586,36587,36588,36589,36590,36591,36592,36593,36594,36595,36596,36597,36598,36599,36600,36601,36602,36603,36604,36605,36606,36607,36608,36609,36610,36611,36612,36613,36614,36615,36616,36617,36618,36619,36620,36621,36622,36623,36624,36625,36626,36627,36628,36629,36630,36631,36632,36633,36634,36635,36636,36637,36638,36639,36640,36641,36642,36643,36644,36645,36646,36647,36648,36649,36650,36651,36652,36653,36654,36655,36656,36657,36658,36659,36660,36661,36662,36663,36664,36665,36666,36667,36668,36669,36670,36671,36672,36673,36674,36675,36676,36677,36678,36679,36680,36681,36682,36683,36684,36685,36686,36687,36688,36689,36690,36691,36692,36693,36694,36695,36696,36697,36698,36699,36700,36701,36702,36703,36704,36705,36706,36707,36708,36709,36710,36711,36712,36713,36714,36715,36716,36717,36718,36719,36720,36721,36722,36723,36724,36725,36726,36727,36728,36729,36730,36731,36732,36733,36734,36735,36736,36737,36738,36739,36740,36741,36742,36743,36744,36745,36746,36747,36748,36749,36750,36751,36752,36753,36754,36755,36756,36757,36758,36759,36760,36761,36762,36763,36764,36765,36766,36767,36768,36769,36770,36771,36772,36773,36774,36775,36776,36777,36778,36779,36780,36781,36782,36783,36784,36785,36786,36787,36788,36789,36790,36791,36792,36793,36794,36795,36796,36797,36798,36799,36800,36801,36802,36803,36804,36805,36806,36807,36808,36809,36810,36811,36812,36813,36814,36815,36816,36817,36818,36819,36820,36821,36822,36823,36824,36825,36826,36827,36828,36829,36830,36831,36832,36833,36834,36835,36836,36837,36838,36839,36840,36841,36842,36843,36844,36845,36846,36847,36848,36849,36850,36851,36852,36853,36854,36855,36856,36857,36858,36859,36860,36861,36862,36863,36864,36865,36866,36867,36868,36869,36870,36871,36872,36873,36874,36875,36876,36877,36878,36879,36880,36881,36882,36883,36884,36885,36886,36887,36888,36889,36890,36891,36892,36893,36894,36895,36896,36897,36898,36899,36900,36901,36902,36903,36904,36905,36906,36907,36908,36909,36910,36911,36912,36913,36914,36915,36916,36917,36918,36919,36920,36921,36922,36923,36924,36925,36926,36927,36928,36929,36930,36931,36932,36933,36934,36935,36936,36937,36938,36939,36940,36941,36942,36943,36944,36945,36946,36947,36948,36949,36950,36951,36952,36953,36954,36955,36956,36957,36958,36959,36960,36961,36962,36963,36964,36965,36966,36967,36968,36969,36970,36971,36972,36973,36974,36975,36976,36977,36978,36979,36980,36981,36982,36983,36984,36985,36986,36987,36988,36989,36990,36991,36992,36993,36994,36995,36996,36997,36998,36999,37000,37001,37002,37003,37004,37005,37006,37007,37008,37009,37010,37011,37012,37013,37014,37015,37016,37017,37018,37019,37020,37021,37022,37023,37024,37025,37026,37027,37028,37029,37030,37031,37032,37033,37034,37035,37036,37037,37038,37039,37040,37041,37042,37043,37044,37045,37046,37047,37048,37049,37050,37051,37052,37053,37054,37055,37056,37057,37058,37059,37060,37061,37062,37063,37064,37065,37066,37067,37068,37069,37070,37071,37072,37073,37074,37075,37076,37077,37078,37079,37080,37081,37082,37083,37084,37085,37086,37087,37088,37089,37090,37091,37092,37093,37094,37095,37096,37097,37098,37099,37100,37101,37102,37103,37104,37105,37106,37107,37108,37109,37110,37111,37112,37113,37114,37115,37116,37117,37118,37119,37120,37121,37122,37123,37124,37125,37126,37127,37128,37129,37130,37131,37132,37133,37134,37135,37136,37137,37138,37139,37140,37141,37142,37143,37144,37145,37146,37147,37148,37149,37150,37151,37152,37153,37154,37155,37156,37157,37158,37159,37160,37161,37162,37163,37164,37165,37166,37167,37168,37169,37170,37171,37172,37173,37174,37175,37176,37177,37178,37179,37180,37181,37182,37183,37184,37185,37186,37187,37188,37189,37190,37191,37192,37193,37194,37195,37196,37197,37198,37199,37200,37201,37202,37203,37204,37205,37206,37207,37208,37209,37210,37211,37212,37213,37214,37215,37216,37217,37218,37219,37220,37221,37222,37223,37224,37225,37226,37227,37228,37229,37230,37231,37232,37233,37234,37235,37236,37237,37238,37239,37240,37241,37242,37243,37244,37245,37246,37247,37248,37249,37250,37251,37252,37253,37254,37255,37256,37257,37258,37259,37260,37261,37262,37263,37264,37265,37266,37267,37268,37269,37270,37271,37272,37273,37274,37275,37276,37277,37278,37279,37280,37281,37282,37283,37284,37285,37286,37287,37288,37289,37290,37291,37292,37293,37294,37295,37296,37297,37298,37299,37300,37301,37302,37303,37304,37305,37306,37307,37308,37309,37310,37311,37312,37313,37314,37315,37316,37317,37318,37319,37320,37321,37322,37323,37324,37325,37326,37327,37328,37329,37330,37331,37332,37333,37334,37335,37336,37337,37338,37339,37340,37341,37342,37343,37344,37345,37346,37347,37348,37349,37350,37351,37352,37353,37354,37355,37356,37357,37358,37359,37360,37361,37362,37363,37364,37365,37366,37367,37368,37369,37370,37371,37372,37373,37374,37375,37376,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37387,37388,37389,37390,37391,37392,37393,37394,37395,37396,37397,37398,37399,37400,37401,37402,37403,37404,37405,37406,37407,37408,37409,37410,37411,37412,37413,37414,37415,37416,37417,37418,37419,37420,37421,37422,37423,37424,37425,37426,37427,37428,37429,37430,37431,37432,37433,37434,37435,37436,37437,37438,37439,37440,37441,37442,37443,37444,37445,37446,37447,37448,37449,37450,37451,37452,37453,37454,37455,37456,37457,37458,37459,37460,37461,37462,37463,37464,37465,37466,37467,37468,37469,37470,37471,37472,37473,37474,37475,37476,37477,37478,37479,37480,37481,37482,37483,37484,37485,37486,37487,37488,37489,37490,37491,37492,37493,37494,37495,37496,37497,37498,37499,37500,37501,37502,37503,37504,37505,37506,37507,37508,37509,37510,37511,37512,37513,37514,37515,37516,37517,37518,37519,37520,37521,37522,37523,37524,37525,37526,37527,37528,37529,37530,37531,37532,37533,37534,37535,37536,37537,37538,37539,37540,37541,37542,37543,37544,37545,37546,37547,37548,37549,37550,37551,37552,37553,37554,37555,37556,37557,37558,37559,37560,37561,37562,37563,37564,37565,37566,37567,37568,37569,37570,37571,37572,37573,37574,37575,37576,37577,37578,37579,37580,37581,37582,37583,37584,37585,37586,37587,37588,37589,37590,37591,37592,37593,37594,37595,37596,37597,37598,37599,37600,37601,37602,37603,37604,37605,37606,37607,37608,37609,37610,37611,37612,37613,37614,37615,37616,37617,37618,37619,37620,37621,37622,37623,37624,37625,37626,37627,37628,37629,37630,37631,37632,37633,37634,37635,37636,37637,37638,37639,37640,37641,37642,37643,37644,37645,37646,37647,37648,37649,37650,37651,37652,37653,37654,37655,37656,37657,37658,37659,37660,37661,37662,37663,37664,37665,37666,37667,37668,37669,37670,37671,37672,37673,37674,37675,37676,37677,37678,37679,37680,37681,37682,37683,37684,37685,37686,37687,37688,37689,37690,37691,37692,37693,37694,37695,37696,37697,37698,37699,37700,37701,37702,37703,37704,37705,37706,37707,37708,37709,37710,37711,37712,37713,37714,37715,37716,37717,37718,37719,37720,37721,37722,37723,37724,37725,37726,37727,37728,37729,37730,37731,37732,37733,37734,37735,37736,37737,37738,37739,37740,37741,37742,37743,37744,37745,37746,37747,37748,37749,37750,37751,37752,37753,37754,37755,37756,37757,37758,37759,37760,37761,37762,37763,37764,37765,37766,37767,37768,37769,37770,37771,37772,37773,37774,37775,37776,37777,37778,37779,37780,37781,37782,37783,37784,37785,37786,37787,37788,37789,37790,37791,37792,37793,37794,37795,37796,37797,37798,37799,37800,37801,37802,37803,37804,37805,37806,37807,37808,37809,37810,37811,37812,37813,37814,37815,37816,37817,37818,37819,37820,37821,37822,37823,37824,37825,37826,37827,37828,37829,37830,37831,37832,37833,37834,37835,37836,37837,37838,37839,37840,37841,37842,37843,37844,37845,37846,37847,37848,37849,37850,37851,37852,37853,37854,37855,37856,37857,37858,37859,37860,37861,37862,37863,37864,37865,37866,37867,37868,37869,37870,37871,37872,37873,37874,37875,37876,37877,37878,37879,37880,37881,37882,37883,37884,37885,37886,37887,37888,37889,37890,37891,37892,37893,37894,37895,37896,37897,37898,37899,37900,37901,37902,37903,37904,37905,37906,37907,37908,37909,37910,37911,37912,37913,37914,37915,37916,37917,37918,37919,37920,37921,37922,37923,37924,37925,37926,37927,37928,37929,37930,37931,37932,37933,37934,37935,37936,37937,37938,37939,37940,37941,37942,37943,37944,37945,37946,37947,37948,37949,37950,37951,37952,37953,37954,37955,37956,37957,37958,37959,37960,37961,37962,37963,37964,37965,37966,37967,37968,37969,37970,37971,37972,37973,37974,37975,37976,37977,37978,37979,37980,37981,37982,37983,37984,37985,37986,37987,37988,37989,37990,37991,37992,37993,37994,37995,37996,37997,37998,37999,38000,38001,38002,38003,38004,38005,38006,38007,38008,38009,38010,38011,38012,38013,38014,38015,38016,38017,38018,38019,38020,38021,38022,38023,38024,38025,38026,38027,38028,38029,38030,38031,38032,38033,38034,38035,38036,38037,38038,38039,38040,38041,38042,38043,38044,38045,38046,38047,38048,38049,38050,38051,38052,38053,38054,38055,38056,38057,38058,38059,38060,38061,38062,38063,38064,38065,38066,38067,38068,38069,38070,38071,38072,38073,38074,38075,38076,38077,38078,38079,38080,38081,38082,38083,38084,38085,38086,38087,38088,38089,38090,38091,38092,38093,38094,38095,38096,38097,38098,38099,38100,38101,38102,38103,38104,38105,38106,38107,38108,38109,38110,38111,38112,38113,38114,38115,38116,38117,38118,38119,38120,38121,38122,38123,38124,38125,38126,38127,38128,38129,38130,38131,38132,38133,38134,38135,38136,38137,38138,38139,38140,38141,38142,38143,38144,38145,38146,38147,38148,38149,38150,38151,38152,38153,38154,38155,38156,38157,38158,38159,38160,38161,38162,38163,38164,38165,38166,38167,38168,38169,38170,38171,38172,38173,38174,38175,38176,38177,38178,38179,38180,38181,38182,38183,38184,38185,38186,38187,38188,38189,38190,38191,38192,38193,38194,38195,38196,38197,38198,38199,38200,38201,38202,38203,38204,38205,38206,38207,38208,38209,38210,38211,38212,38213,38214,38215,38216,38217,38218,38219,38220,38221,38222,38223,38224,38225,38226,38227,38228,38229,38230,38231,38232,38233,38234,38235,38236,38237,38238,38239,38240,38241,38242,38243,38244,38245,38246,38247,38248,38249,38250,38251,38252,38253,38254,38255,38256,38257,38258,38259,38260,38261,38262,38263,38264,38265,38266,38267,38268,38269,38270,38271,38272,38273,38274,38275,38276,38277,38278,38279,38280,38281,38282,38283,38284,38285,38286,38287,38288,38289,38290,38291,38292,38293,38294,38295,38296,38297,38298,38299,38300,38301,38302,38303,38304,38305,38306,38307,38308,38309,38310,38311,38312,38313,38314,38315,38316,38317,38318,38319,38320,38321,38322,38323,38324,38325,38326,38327,38328,38329,38330,38331,38332,38333,38334,38335,38336,38337,38338,38339,38340,38341,38342,38343,38344,38345,38346,38347,38348,38349,38350,38351,38352,38353,38354,38355,38356,38357,38358,38359,38360,38361,38362,38363,38364,38365,38366,38367,38368,38369,38370,38371,38372,38373,38374,38375,38376,38377,38378,38379,38380,38381,38382,38383,38384,38385,38386,38387,38388,38389,38390,38391,38392,38393,38394,38395,38396,38397,38398,38399,38400,38401,38402,38403,38404,38405,38406,38407,38408,38409,38410,38411,38412,38413,38414,38415,38416,38417,38418,38419,38420,38421,38422,38423,38424,38425,38426,38427,38428,38429,38430,38431,38432,38433,38434,38435,38436,38437,38438,38439,38440,38441,38442,38443,38444,38445,38446,38447,38448,38449,38450,38451,38452,38453,38454,38455,38456,38457,38458,38459,38460,38461,38462,38463,38464,38465,38466,38467,38468,38469,38470,38471,38472,38473,38474,38475,38476,38477,38478,38479,38480,38481,38482,38483,38484,38485,38486,38487,38488,38489,38490,38491,38492,38493,38494,38495,38496,38497,38498,38499,38500,38501,38502,38503,38504,38505,38506,38507,38508,38509,38510,38511,38512,38513,38514,38515,38516,38517,38518,38519,38520,38521,38522,38523,38524,38525,38526,38527,38528,38529,38530,38531,38532,38533,38534,38535,38536,38537,38538,38539,38540,38541,38542,38543,38544,38545,38546,38547,38548,38549,38550,38551,38552,38553,38554,38555,38556,38557,38558,38559,38560,38561,38562,38563,38564,38565,38566,38567,38568,38569,38570,38571,38572,38573,38574,38575,38576,38577,38578,38579,38580,38581,38582,38583,38584,38585,38586,38587,38588,38589,38590,38591,38592,38593,38594,38595,38596,38597,38598,38599,38600,38601,38602,38603,38604,38605,38606,38607,38608,38609,38610,38611,38612,38613,38614,38615,38616,38617,38618,38619,38620,38621,38622,38623,38624,38625,38626,38627,38628,38629,38630,38631,38632,38633,38634,38635,38636,38637,38638,38639,38640,38641,38642,38643,38644,38645,38646,38647,38648,38649,38650,38651,38652,38653,38654,38655,38656,38657,38658,38659,38660,38661,38662,38663,38664,38665,38666,38667,38668,38669,38670,38671,38672,38673,38674,38675,38676,38677,38678,38679,38680,38681,38682,38683,38684,38685,38686,38687,38688,38689,38690,38691,38692,38693,38694,38695,38696,38697,38698,38699,38700,38701,38702,38703,38704,38705,38706,38707,38708,38709,38710,38711,38712,38713,38714,38715,38716,38717,38718,38719,38720,38721,38722,38723,38724,38725,38726,38727,38728,38729,38730,38731,38732,38733,38734,38735,38736,38737,38738,38739,38740,38741,38742,38743,38744,38745,38746,38747,38748,38749,38750,38751,38752,38753,38754,38755,38756,38757,38758,38759,38760,38761,38762,38763,38764,38765,38766,38767,38768,38769,38770,38771,38772,38773,38774,38775,38776,38777,38778,38779,38780,38781,38782,38783,38784,38785,38786,38787,38788,38789,38790,38791,38792,38793,38794,38795,38796,38797,38798,38799,38800,38801,38802,38803,38804,38805,38806,38807,38808,38809,38810,38811,38812,38813,38814,38815,38816,38817,38818,38819,38820,38821,38822,38823,38824,38825,38826,38827,38828,38829,38830,38831,38832,38833,38834,38835,38836,38837,38838,38839,38840,38841,38842,38843,38844,38845,38846,38847,38848,38849,38850,38851,38852,38853,38854,38855,38856,38857,38858,38859,38860,38861,38862,38863,38864,38865,38866,38867,38868,38869,38870,38871,38872,38873,38874,38875,38876,38877,38878,38879,38880,38881,38882,38883,38884,38885,38886,38887,38888,38889,38890,38891,38892,38893,38894,38895,38896,38897,38898,38899,38900,38901,38902,38903,38904,38905,38906,38907,38908,38909,38910,38911,38912,38913,38914,38915,38916,38917,38918,38919,38920,38921,38922,38923,38924,38925,38926,38927,38928,38929,38930,38931,38932,38933,38934,38935,38936,38937,38938,38939,38940,38941,38942,38943,38944,38945,38946,38947,38948,38949,38950,38951,38952,38953,38954,38955,38956,38957,38958,38959,38960,38961,38962,38963,38964,38965,38966,38967,38968,38969,38970,38971,38972,38973,38974,38975,38976,38977,38978,38979,38980,38981,38982,38983,38984,38985,38986,38987,38988,38989,38990,38991,38992,38993,38994,38995,38996,38997,38998,38999,39000,39001,39002,39003,39004,39005,39006,39007,39008,39009,39010,39011,39012,39013,39014,39015,39016,39017,39018,39019,39020,39021,39022,39023,39024,39025,39026,39027,39028,39029,39030,39031,39032,39033,39034,39035,39036,39037,39038,39039,39040,39041,39042,39043,39044,39045,39046,39047,39048,39049,39050,39051,39052,39053,39054,39055,39056,39057,39058,39059,39060,39061,39062,39063,39064,39065,39066,39067,39068,39069,39070,39071,39072,39073,39074,39075,39076,39077,39078,39079,39080,39081,39082,39083,39084,39085,39086,39087,39088,39089,39090,39091,39092,39093,39094,39095,39096,39097,39098,39099,39100,39101,39102,39103,39104,39105,39106,39107,39108,39109,39110,39111,39112,39113,39114,39115,39116,39117,39118,39119,39120,39121,39122,39123,39124,39125,39126,39127,39128,39129,39130,39131,39132,39133,39134,39135,39136,39137,39138,39139,39140,39141,39142,39143,39144,39145,39146,39147,39148,39149,39150,39151,39152,39153,39154,39155,39156,39157,39158,39159,39160,39161,39162,39163,39164,39165,39166,39167,39168,39169,39170,39171,39172,39173,39174,39175,39176,39177,39178,39179,39180,39181,39182,39183,39184,39185,39186,39187,39188,39189,39190,39191,39192,39193,39194,39195,39196,39197,39198,39199,39200,39201,39202,39203,39204,39205,39206,39207,39208,39209,39210,39211,39212,39213,39214,39215,39216,39217,39218,39219,39220,39221,39222,39223,39224,39225,39226,39227,39228,39229,39230,39231,39232,39233,39234,39235,39236,39237,39238,39239,39240,39241,39242,39243,39244,39245,39246,39247,39248,39249,39250,39251,39252,39253,39254,39255,39256,39257,39258,39259,39260,39261,39262,39263,39264,39265,39266,39267,39268,39269,39270,39271,39272,39273,39274,39275,39276,39277,39278,39279,39280,39281,39282,39283,39284,39285,39286,39287,39288,39289,39290,39291,39292,39293,39294,39295,39296,39297,39298,39299,39300,39301,39302,39303,39304,39305,39306,39307,39308,39309,39310,39311,39312,39313,39314,39315,39316,39317,39318,39319,39320,39321,39322,39323,39324,39325,39326,39327,39328,39329,39330,39331,39332,39333,39334,39335,39336,39337,39338,39339,39340,39341,39342,39343,39344,39345,39346,39347,39348,39349,39350,39351,39352,39353,39354,39355,39356,39357,39358,39359,39360,39361,39362,39363,39364,39365,39366,39367,39368,39369,39370,39371,39372,39373,39374,39375,39376,39377,39378,39379,39380,39381,39382,39383,39384,39385,39386,39387,39388,39389,39390,39391,39392,39393,39394,39395,39396,39397,39398,39399,39400,39401,39402,39403,39404,39405,39406,39407,39408,39409,39410,39411,39412,39413,39414,39415,39416,39417,39418,39419,39420,39421,39422,39423,39424,39425,39426,39427,39428,39429,39430,39431,39432,39433,39434,39435,39436,39437,39438,39439,39440,39441,39442,39443,39444,39445,39446,39447,39448,39449,39450,39451,39452,39453,39454,39455,39456,39457,39458,39459,39460,39461,39462,39463,39464,39465,39466,39467,39468,39469,39470,39471,39472,39473,39474,39475,39476,39477,39478,39479,39480,39481,39482,39483,39484,39485,39486,39487,39488,39489,39490,39491,39492,39493,39494,39495,39496,39497,39498,39499,39500,39501,39502,39503,39504,39505,39506,39507,39508,39509,39510,39511,39512,39513,39514,39515,39516,39517,39518,39519,39520,39521,39522,39523,39524,39525,39526,39527,39528,39529,39530,39531,39532,39533,39534,39535,39536,39537,39538,39539,39540,39541,39542,39543,39544,39545,39546,39547,39548,39549,39550,39551,39552,39553,39554,39555,39556,39557,39558,39559,39560,39561,39562,39563,39564,39565,39566,39567,39568,39569,39570,39571,39572,39573,39574,39575,39576,39577,39578,39579,39580,39581,39582,39583,39584,39585,39586,39587,39588,39589,39590,39591,39592,39593,39594,39595,39596,39597,39598,39599,39600,39601,39602,39603,39604,39605,39606,39607,39608,39609,39610,39611,39612,39613,39614,39615,39616,39617,39618,39619,39620,39621,39622,39623,39624,39625,39626,39627,39628,39629,39630,39631,39632,39633,39634,39635,39636,39637,39638,39639,39640,39641,39642,39643,39644,39645,39646,39647,39648,39649,39650,39651,39652,39653,39654,39655,39656,39657,39658,39659,39660,39661,39662,39663,39664,39665,39666,39667,39668,39669,39670,39671,39672,39673,39674,39675,39676,39677,39678,39679,39680,39681,39682,39683,39684,39685,39686,39687,39688,39689,39690,39691,39692,39693,39694,39695,39696,39697,39698,39699,39700,39701,39702,39703,39704,39705,39706,39707,39708,39709,39710,39711,39712,39713,39714,39715,39716,39717,39718,39719,39720,39721,39722,39723,39724,39725,39726,39727,39728,39729,39730,39731,39732,39733,39734,39735,39736,39737,39738,39739,39740,39741,39742,39743,39744,39745,39746,39747,39748,39749,39750,39751,39752,39753,39754,39755,39756,39757,39758,39759,39760,39761,39762,39763,39764,39765,39766,39767,39768,39769,39770,39771,39772,39773,39774,39775,39776,39777,39778,39779,39780,39781,39782,39783,39784,39785,39786,39787,39788,39789,39790,39791,39792,39793,39794,39795,39796,39797,39798,39799,39800,39801,39802,39803,39804,39805,39806,39807,39808,39809,39810,39811,39812,39813,39814,39815,39816,39817,39818,39819,39820,39821,39822,39823,39824,39825,39826,39827,39828,39829,39830,39831,39832,39833,39834,39835,39836,39837,39838,39839,39840,39841,39842,39843,39844,39845,39846,39847,39848,39849,39850,39851,39852,39853,39854,39855,39856,39857,39858,39859,39860,39861,39862,39863,39864,39865,39866,39867,39868,39869,39870,39871,39872,39873,39874,39875,39876,39877,39878,39879,39880,39881,39882,39883,39884,39885,39886,39887,39888,39889,39890,39891,39892,39893,39894,39895,39896,39897,39898,39899,39900,39901,39902,39903,39904,39905,39906,39907,39908,39909,39910,39911,39912,39913,39914,39915,39916,39917,39918,39919,39920,39921,39922,39923,39924,39925,39926,39927,39928,39929,39930,39931,39932,39933,39934,39935,39936,39937,39938,39939,39940,39941,39942,39943,39944,39945,39946,39947,39948,39949,39950,39951,39952,39953,39954,39955,39956,39957,39958,39959,39960,39961,39962,39963,39964,39965,39966,39967,39968,39969,39970,39971,39972,39973,39974,39975,39976,39977,39978,39979,39980,39981,39982,39983,39984,39985,39986,39987,39988,39989,39990,39991,39992,39993,39994,39995,39996,39997,39998,39999,40000,40001,40002,40003,40004,40005,40006,40007,40008,40009,40010,40011,40012,40013,40014,40015,40016,40017,40018,40019,40020,40021,40022,40023,40024,40025,40026,40027,40028,40029,40030,40031,40032,40033,40034,40035,40036,40037,40038,40039,40040,40041,40042,40043,40044,40045,40046,40047,40048,40049,40050,40051,40052,40053,40054,40055,40056,40057,40058,40059,40060,40061,40062,40063,40064,40065,40066,40067,40068,40069,40070,40071,40072,40073,40074,40075,40076,40077,40078,40079,40080,40081,40082,40083,40084,40085,40086,40087,40088,40089,40090,40091,40092,40093,40094,40095,40096,40097,40098,40099,40100,40101,40102,40103,40104,40105,40106,40107,40108,40109,40110,40111,40112,40113,40114,40115,40116,40117,40118,40119,40120,40121,40122,40123,40124,40125,40126,40127,40128,40129,40130,40131,40132,40133,40134,40135,40136,40137,40138,40139,40140,40141,40142,40143,40144,40145,40146,40147,40148,40149,40150,40151,40152,40153,40154,40155,40156,40157,40158,40159,40160,40161,40162,40163,40164,40165,40166,40167,40168,40169,40170,40171,40172,40173,40174,40175,40176,40177,40178,40179,40180,40181,40182,40183,40184,40185,40186,40187,40188,40189,40190,40191,40192,40193,40194,40195,40196,40197,40198,40199,40200,40201,40202,40203,40204,40205,40206,40207,40208,40209,40210,40211,40212,40213,40214,40215,40216,40217,40218,40219,40220,40221,40222,40223,40224,40225,40226,40227,40228,40229,40230,40231,40232,40233,40234,40235,40236,40237,40238,40239,40240,40241,40242,40243,40244,40245,40246,40247,40248,40249,40250,40251,40252,40253,40254,40255,40256,40257,40258,40259,40260,40261,40262,40263,40264,40265,40266,40267,40268,40269,40270,40271,40272,40273,40274,40275,40276,40277,40278,40279,40280,40281,40282,40283,40284,40285,40286,40287,40288,40289,40290,40291,40292,40293,40294,40295,40296,40297,40298,40299,40300,40301,40302,40303,40304,40305,40306,40307,40308,40309,40310,40311,40312,40313,40314,40315,40316,40317,40318,40319,40320,40321,40322,40323,40324,40325,40326,40327,40328,40329,40330,40331,40332,40333,40334,40335,40336,40337,40338,40339,40340,40341,40342,40343,40344,40345,40346,40347,40348,40349,40350,40351,40352,40353,40354,40355,40356,40357,40358,40359,40360,40361,40362,40363,40364,40365,40366,40367,40368,40369,40370,40371,40372,40373,40374,40375,40376,40377,40378,40379,40380,40381,40382,40383,40384,40385,40386,40387,40388,40389,40390,40391,40392,40393,40394,40395,40396,40397,40398,40399,40400,40401,40402,40403,40404,40405,40406,40407,40408,40409,40410,40411,40412,40413,40414,40415,40416,40417,40418,40419,40420,40421,40422,40423,40424,40425,40426,40427,40428,40429,40430,40431,40432,40433,40434,40435,40436,40437,40438,40439,40440,40441,40442,40443,40444,40445,40446,40447,40448,40449,40450,40451,40452,40453,40454,40455,40456,40457,40458,40459,40460,40461,40462,40463,40464,40465,40466,40467,40468,40469,40470,40471,40472,40473,40474,40475,40476,40477,40478,40479,40480,40481,40482,40483,40484,40485,40486,40487,40488,40489,40490,40491,40492,40493,40494,40495,40496,40497,40498,40499,40500,40501,40502,40503,40504,40505,40506,40507,40508,40509,40510,40511,40512,40513,40514,40515,40516,40517,40518,40519,40520,40521,40522,40523,40524,40525,40526,40527,40528,40529,40530,40531,40532,40533,40534,40535,40536,40537,40538,40539,40540,40541,40542,40543,40544,40545,40546,40547,40548,40549,40550,40551,40552,40553,40554,40555,40556,40557,40558,40559,40560,40561,40562,40563,40564,40565,40566,40567,40568,40569,40570,40571,40572,40573,40574,40575,40576,40577,40578,40579,40580,40581,40582,40583,40584,40585,40586,40587,40588,40589,40590,40591,40592,40593,40594,40595,40596,40597,40598,40599,40600,40601,40602,40603,40604,40605,40606,40607,40608,40609,40610,40611,40612,40613,40614,40615,40616,40617,40618,40619,40620,40621,40622,40623,40624,40625,40626,40627,40628,40629,40630,40631,40632,40633,40634,40635,40636,40637,40638,40639,40640,40641,40642,40643,40644,40645,40646,40647,40648,40649,40650,40651,40652,40653,40654,40655,40656,40657,40658,40659,40660,40661,40662,40663,40664,40665,40666,40667,40668,40669,40670,40671,40672,40673,40674,40675,40676,40677,40678,40679,40680,40681,40682,40683,40684,40685,40686,40687,40688,40689,40690,40691,40692,40693,40694,40695,40696,40697,40698,40699,40700,40701,40702,40703,40704,40705,40706,40707,40708,40709,40710,40711,40712,40713,40714,40715,40716,40717,40718,40719,40720,40721,40722,40723,40724,40725,40726,40727,40728,40729,40730,40731,40732,40733,40734,40735,40736,40737,40738,40739,40740,40741,40742,40743,40744,40745,40746,40747,40748,40749,40750,40751,40752,40753,40754,40755,40756,40757,40758,40759,40760,40761,40762,40763,40764,40765,40766,40767,40768,40769,40770,40771,40772,40773,40774,40775,40776,40777,40778,40779,40780,40781,40782,40783,40784,40785,40786,40787,40788,40789,40790,40791,40792,40793,40794,40795,40796,40797,40798,40799,40800,40801,40802,40803,40804,40805,40806,40807,40808,40809,40810,40811,40812,40813,40814,40815,40816,40817,40818,40819,40820,40821,40822,40823,40824,40825,40826,40827,40828,40829,40830,40831,40832,40833,40834,40835,40836,40837,40838,40839,40840,40841,40842,40843,40844,40845,40846,40847,40848,40849,40850,40851,40852,40853,40854,40855,40856,40857,40858,40859,40860,40861,40862,40863,40864,40865,40866,40867,40868,40869,40870,40871,40872,40873,40874,40875,40876,40877,40878,40879,40880,40881,40882,40883,40884,40885,40886,40887,40888,40889,40890,40891,40892,40893,40894,40895,40896,40897,40898,40899,40900,40901,40902,40903,40904,40905,40906,40907,40908,40909,40910,40911,40912,40913,40914,40915,40916,40917,40918,40919,40920,40921,40922,40923,40924,40925,40926,40927,40928,40929,40930,40931,40932,40933,40934,40935,40936,40937,40938,40939,40940,40941,40942,40943,40960,40961,40962,40963,40964,40965,40966,40967,40968,40969,40970,40971,40972,40973,40974,40975,40976,40977,40978,40979,40980,40981,40982,40983,40984,40985,40986,40987,40988,40989,40990,40991,40992,40993,40994,40995,40996,40997,40998,40999,41000,41001,41002,41003,41004,41005,41006,41007,41008,41009,41010,41011,41012,41013,41014,41015,41016,41017,41018,41019,41020,41021,41022,41023,41024,41025,41026,41027,41028,41029,41030,41031,41032,41033,41034,41035,41036,41037,41038,41039,41040,41041,41042,41043,41044,41045,41046,41047,41048,41049,41050,41051,41052,41053,41054,41055,41056,41057,41058,41059,41060,41061,41062,41063,41064,41065,41066,41067,41068,41069,41070,41071,41072,41073,41074,41075,41076,41077,41078,41079,41080,41081,41082,41083,41084,41085,41086,41087,41088,41089,41090,41091,41092,41093,41094,41095,41096,41097,41098,41099,41100,41101,41102,41103,41104,41105,41106,41107,41108,41109,41110,41111,41112,41113,41114,41115,41116,41117,41118,41119,41120,41121,41122,41123,41124,41125,41126,41127,41128,41129,41130,41131,41132,41133,41134,41135,41136,41137,41138,41139,41140,41141,41142,41143,41144,41145,41146,41147,41148,41149,41150,41151,41152,41153,41154,41155,41156,41157,41158,41159,41160,41161,41162,41163,41164,41165,41166,41167,41168,41169,41170,41171,41172,41173,41174,41175,41176,41177,41178,41179,41180,41181,41182,41183,41184,41185,41186,41187,41188,41189,41190,41191,41192,41193,41194,41195,41196,41197,41198,41199,41200,41201,41202,41203,41204,41205,41206,41207,41208,41209,41210,41211,41212,41213,41214,41215,41216,41217,41218,41219,41220,41221,41222,41223,41224,41225,41226,41227,41228,41229,41230,41231,41232,41233,41234,41235,41236,41237,41238,41239,41240,41241,41242,41243,41244,41245,41246,41247,41248,41249,41250,41251,41252,41253,41254,41255,41256,41257,41258,41259,41260,41261,41262,41263,41264,41265,41266,41267,41268,41269,41270,41271,41272,41273,41274,41275,41276,41277,41278,41279,41280,41281,41282,41283,41284,41285,41286,41287,41288,41289,41290,41291,41292,41293,41294,41295,41296,41297,41298,41299,41300,41301,41302,41303,41304,41305,41306,41307,41308,41309,41310,41311,41312,41313,41314,41315,41316,41317,41318,41319,41320,41321,41322,41323,41324,41325,41326,41327,41328,41329,41330,41331,41332,41333,41334,41335,41336,41337,41338,41339,41340,41341,41342,41343,41344,41345,41346,41347,41348,41349,41350,41351,41352,41353,41354,41355,41356,41357,41358,41359,41360,41361,41362,41363,41364,41365,41366,41367,41368,41369,41370,41371,41372,41373,41374,41375,41376,41377,41378,41379,41380,41381,41382,41383,41384,41385,41386,41387,41388,41389,41390,41391,41392,41393,41394,41395,41396,41397,41398,41399,41400,41401,41402,41403,41404,41405,41406,41407,41408,41409,41410,41411,41412,41413,41414,41415,41416,41417,41418,41419,41420,41421,41422,41423,41424,41425,41426,41427,41428,41429,41430,41431,41432,41433,41434,41435,41436,41437,41438,41439,41440,41441,41442,41443,41444,41445,41446,41447,41448,41449,41450,41451,41452,41453,41454,41455,41456,41457,41458,41459,41460,41461,41462,41463,41464,41465,41466,41467,41468,41469,41470,41471,41472,41473,41474,41475,41476,41477,41478,41479,41480,41481,41482,41483,41484,41485,41486,41487,41488,41489,41490,41491,41492,41493,41494,41495,41496,41497,41498,41499,41500,41501,41502,41503,41504,41505,41506,41507,41508,41509,41510,41511,41512,41513,41514,41515,41516,41517,41518,41519,41520,41521,41522,41523,41524,41525,41526,41527,41528,41529,41530,41531,41532,41533,41534,41535,41536,41537,41538,41539,41540,41541,41542,41543,41544,41545,41546,41547,41548,41549,41550,41551,41552,41553,41554,41555,41556,41557,41558,41559,41560,41561,41562,41563,41564,41565,41566,41567,41568,41569,41570,41571,41572,41573,41574,41575,41576,41577,41578,41579,41580,41581,41582,41583,41584,41585,41586,41587,41588,41589,41590,41591,41592,41593,41594,41595,41596,41597,41598,41599,41600,41601,41602,41603,41604,41605,41606,41607,41608,41609,41610,41611,41612,41613,41614,41615,41616,41617,41618,41619,41620,41621,41622,41623,41624,41625,41626,41627,41628,41629,41630,41631,41632,41633,41634,41635,41636,41637,41638,41639,41640,41641,41642,41643,41644,41645,41646,41647,41648,41649,41650,41651,41652,41653,41654,41655,41656,41657,41658,41659,41660,41661,41662,41663,41664,41665,41666,41667,41668,41669,41670,41671,41672,41673,41674,41675,41676,41677,41678,41679,41680,41681,41682,41683,41684,41685,41686,41687,41688,41689,41690,41691,41692,41693,41694,41695,41696,41697,41698,41699,41700,41701,41702,41703,41704,41705,41706,41707,41708,41709,41710,41711,41712,41713,41714,41715,41716,41717,41718,41719,41720,41721,41722,41723,41724,41725,41726,41727,41728,41729,41730,41731,41732,41733,41734,41735,41736,41737,41738,41739,41740,41741,41742,41743,41744,41745,41746,41747,41748,41749,41750,41751,41752,41753,41754,41755,41756,41757,41758,41759,41760,41761,41762,41763,41764,41765,41766,41767,41768,41769,41770,41771,41772,41773,41774,41775,41776,41777,41778,41779,41780,41781,41782,41783,41784,41785,41786,41787,41788,41789,41790,41791,41792,41793,41794,41795,41796,41797,41798,41799,41800,41801,41802,41803,41804,41805,41806,41807,41808,41809,41810,41811,41812,41813,41814,41815,41816,41817,41818,41819,41820,41821,41822,41823,41824,41825,41826,41827,41828,41829,41830,41831,41832,41833,41834,41835,41836,41837,41838,41839,41840,41841,41842,41843,41844,41845,41846,41847,41848,41849,41850,41851,41852,41853,41854,41855,41856,41857,41858,41859,41860,41861,41862,41863,41864,41865,41866,41867,41868,41869,41870,41871,41872,41873,41874,41875,41876,41877,41878,41879,41880,41881,41882,41883,41884,41885,41886,41887,41888,41889,41890,41891,41892,41893,41894,41895,41896,41897,41898,41899,41900,41901,41902,41903,41904,41905,41906,41907,41908,41909,41910,41911,41912,41913,41914,41915,41916,41917,41918,41919,41920,41921,41922,41923,41924,41925,41926,41927,41928,41929,41930,41931,41932,41933,41934,41935,41936,41937,41938,41939,41940,41941,41942,41943,41944,41945,41946,41947,41948,41949,41950,41951,41952,41953,41954,41955,41956,41957,41958,41959,41960,41961,41962,41963,41964,41965,41966,41967,41968,41969,41970,41971,41972,41973,41974,41975,41976,41977,41978,41979,41980,41981,41982,41983,41984,41985,41986,41987,41988,41989,41990,41991,41992,41993,41994,41995,41996,41997,41998,41999,42000,42001,42002,42003,42004,42005,42006,42007,42008,42009,42010,42011,42012,42013,42014,42015,42016,42017,42018,42019,42020,42021,42022,42023,42024,42025,42026,42027,42028,42029,42030,42031,42032,42033,42034,42035,42036,42037,42038,42039,42040,42041,42042,42043,42044,42045,42046,42047,42048,42049,42050,42051,42052,42053,42054,42055,42056,42057,42058,42059,42060,42061,42062,42063,42064,42065,42066,42067,42068,42069,42070,42071,42072,42073,42074,42075,42076,42077,42078,42079,42080,42081,42082,42083,42084,42085,42086,42087,42088,42089,42090,42091,42092,42093,42094,42095,42096,42097,42098,42099,42100,42101,42102,42103,42104,42105,42106,42107,42108,42109,42110,42111,42112,42113,42114,42115,42116,42117,42118,42119,42120,42121,42122,42123,42124,42192,42193,42194,42195,42196,42197,42198,42199,42200,42201,42202,42203,42204,42205,42206,42207,42208,42209,42210,42211,42212,42213,42214,42215,42216,42217,42218,42219,42220,42221,42222,42223,42224,42225,42226,42227,42228,42229,42230,42231,42232,42233,42234,42235,42236,42237,42240,42241,42242,42243,42244,42245,42246,42247,42248,42249,42250,42251,42252,42253,42254,42255,42256,42257,42258,42259,42260,42261,42262,42263,42264,42265,42266,42267,42268,42269,42270,42271,42272,42273,42274,42275,42276,42277,42278,42279,42280,42281,42282,42283,42284,42285,42286,42287,42288,42289,42290,42291,42292,42293,42294,42295,42296,42297,42298,42299,42300,42301,42302,42303,42304,42305,42306,42307,42308,42309,42310,42311,42312,42313,42314,42315,42316,42317,42318,42319,42320,42321,42322,42323,42324,42325,42326,42327,42328,42329,42330,42331,42332,42333,42334,42335,42336,42337,42338,42339,42340,42341,42342,42343,42344,42345,42346,42347,42348,42349,42350,42351,42352,42353,42354,42355,42356,42357,42358,42359,42360,42361,42362,42363,42364,42365,42366,42367,42368,42369,42370,42371,42372,42373,42374,42375,42376,42377,42378,42379,42380,42381,42382,42383,42384,42385,42386,42387,42388,42389,42390,42391,42392,42393,42394,42395,42396,42397,42398,42399,42400,42401,42402,42403,42404,42405,42406,42407,42408,42409,42410,42411,42412,42413,42414,42415,42416,42417,42418,42419,42420,42421,42422,42423,42424,42425,42426,42427,42428,42429,42430,42431,42432,42433,42434,42435,42436,42437,42438,42439,42440,42441,42442,42443,42444,42445,42446,42447,42448,42449,42450,42451,42452,42453,42454,42455,42456,42457,42458,42459,42460,42461,42462,42463,42464,42465,42466,42467,42468,42469,42470,42471,42472,42473,42474,42475,42476,42477,42478,42479,42480,42481,42482,42483,42484,42485,42486,42487,42488,42489,42490,42491,42492,42493,42494,42495,42496,42497,42498,42499,42500,42501,42502,42503,42504,42505,42506,42507,42508,42512,42513,42514,42515,42516,42517,42518,42519,42520,42521,42522,42523,42524,42525,42526,42527,42538,42539,42560,42561,42562,42563,42564,42565,42566,42567,42568,42569,42570,42571,42572,42573,42574,42575,42576,42577,42578,42579,42580,42581,42582,42583,42584,42585,42586,42587,42588,42589,42590,42591,42592,42593,42594,42595,42596,42597,42598,42599,42600,42601,42602,42603,42604,42605,42606,42623,42624,42625,42626,42627,42628,42629,42630,42631,42632,42633,42634,42635,42636,42637,42638,42639,42640,42641,42642,42643,42644,42645,42646,42647,42648,42649,42650,42651,42652,42653,42656,42657,42658,42659,42660,42661,42662,42663,42664,42665,42666,42667,42668,42669,42670,42671,42672,42673,42674,42675,42676,42677,42678,42679,42680,42681,42682,42683,42684,42685,42686,42687,42688,42689,42690,42691,42692,42693,42694,42695,42696,42697,42698,42699,42700,42701,42702,42703,42704,42705,42706,42707,42708,42709,42710,42711,42712,42713,42714,42715,42716,42717,42718,42719,42720,42721,42722,42723,42724,42725,42726,42727,42728,42729,42730,42731,42732,42733,42734,42735,42775,42776,42777,42778,42779,42780,42781,42782,42783,42786,42787,42788,42789,42790,42791,42792,42793,42794,42795,42796,42797,42798,42799,42800,42801,42802,42803,42804,42805,42806,42807,42808,42809,42810,42811,42812,42813,42814,42815,42816,42817,42818,42819,42820,42821,42822,42823,42824,42825,42826,42827,42828,42829,42830,42831,42832,42833,42834,42835,42836,42837,42838,42839,42840,42841,42842,42843,42844,42845,42846,42847,42848,42849,42850,42851,42852,42853,42854,42855,42856,42857,42858,42859,42860,42861,42862,42863,42864,42865,42866,42867,42868,42869,42870,42871,42872,42873,42874,42875,42876,42877,42878,42879,42880,42881,42882,42883,42884,42885,42886,42887,42888,42891,42892,42893,42894,42895,42896,42897,42898,42899,42900,42901,42902,42903,42904,42905,42906,42907,42908,42909,42910,42911,42912,42913,42914,42915,42916,42917,42918,42919,42920,42921,42922,42923,42924,42925,42926,42927,42928,42929,42930,42931,42932,42933,42934,42935,42936,42937,42999,43000,43001,43002,43003,43004,43005,43006,43007,43008,43009,43011,43012,43013,43015,43016,43017,43018,43020,43021,43022,43023,43024,43025,43026,43027,43028,43029,43030,43031,43032,43033,43034,43035,43036,43037,43038,43039,43040,43041,43042,43072,43073,43074,43075,43076,43077,43078,43079,43080,43081,43082,43083,43084,43085,43086,43087,43088,43089,43090,43091,43092,43093,43094,43095,43096,43097,43098,43099,43100,43101,43102,43103,43104,43105,43106,43107,43108,43109,43110,43111,43112,43113,43114,43115,43116,43117,43118,43119,43120,43121,43122,43123,43138,43139,43140,43141,43142,43143,43144,43145,43146,43147,43148,43149,43150,43151,43152,43153,43154,43155,43156,43157,43158,43159,43160,43161,43162,43163,43164,43165,43166,43167,43168,43169,43170,43171,43172,43173,43174,43175,43176,43177,43178,43179,43180,43181,43182,43183,43184,43185,43186,43187,43250,43251,43252,43253,43254,43255,43259,43261,43262,43274,43275,43276,43277,43278,43279,43280,43281,43282,43283,43284,43285,43286,43287,43288,43289,43290,43291,43292,43293,43294,43295,43296,43297,43298,43299,43300,43301,43312,43313,43314,43315,43316,43317,43318,43319,43320,43321,43322,43323,43324,43325,43326,43327,43328,43329,43330,43331,43332,43333,43334,43360,43361,43362,43363,43364,43365,43366,43367,43368,43369,43370,43371,43372,43373,43374,43375,43376,43377,43378,43379,43380,43381,43382,43383,43384,43385,43386,43387,43388,43396,43397,43398,43399,43400,43401,43402,43403,43404,43405,43406,43407,43408,43409,43410,43411,43412,43413,43414,43415,43416,43417,43418,43419,43420,43421,43422,43423,43424,43425,43426,43427,43428,43429,43430,43431,43432,43433,43434,43435,43436,43437,43438,43439,43440,43441,43442,43471,43488,43489,43490,43491,43492,43494,43495,43496,43497,43498,43499,43500,43501,43502,43503,43514,43515,43516,43517,43518,43520,43521,43522,43523,43524,43525,43526,43527,43528,43529,43530,43531,43532,43533,43534,43535,43536,43537,43538,43539,43540,43541,43542,43543,43544,43545,43546,43547,43548,43549,43550,43551,43552,43553,43554,43555,43556,43557,43558,43559,43560,43584,43585,43586,43588,43589,43590,43591,43592,43593,43594,43595,43616,43617,43618,43619,43620,43621,43622,43623,43624,43625,43626,43627,43628,43629,43630,43631,43632,43633,43634,43635,43636,43637,43638,43642,43646,43647,43648,43649,43650,43651,43652,43653,43654,43655,43656,43657,43658,43659,43660,43661,43662,43663,43664,43665,43666,43667,43668,43669,43670,43671,43672,43673,43674,43675,43676,43677,43678,43679,43680,43681,43682,43683,43684,43685,43686,43687,43688,43689,43690,43691,43692,43693,43694,43695,43697,43701,43702,43705,43706,43707,43708,43709,43712,43714,43739,43740,43741,43744,43745,43746,43747,43748,43749,43750,43751,43752,43753,43754,43762,43763,43764,43777,43778,43779,43780,43781,43782,43785,43786,43787,43788,43789,43790,43793,43794,43795,43796,43797,43798,43808,43809,43810,43811,43812,43813,43814,43816,43817,43818,43819,43820,43821,43822,43824,43825,43826,43827,43828,43829,43830,43831,43832,43833,43834,43835,43836,43837,43838,43839,43840,43841,43842,43843,43844,43845,43846,43847,43848,43849,43850,43851,43852,43853,43854,43855,43856,43857,43858,43859,43860,43861,43862,43863,43864,43865,43866,43868,43869,43870,43871,43872,43873,43874,43875,43876,43877,43888,43889,43890,43891,43892,43893,43894,43895,43896,43897,43898,43899,43900,43901,43902,43903,43904,43905,43906,43907,43908,43909,43910,43911,43912,43913,43914,43915,43916,43917,43918,43919,43920,43921,43922,43923,43924,43925,43926,43927,43928,43929,43930,43931,43932,43933,43934,43935,43936,43937,43938,43939,43940,43941,43942,43943,43944,43945,43946,43947,43948,43949,43950,43951,43952,43953,43954,43955,43956,43957,43958,43959,43960,43961,43962,43963,43964,43965,43966,43967,43968,43969,43970,43971,43972,43973,43974,43975,43976,43977,43978,43979,43980,43981,43982,43983,43984,43985,43986,43987,43988,43989,43990,43991,43992,43993,43994,43995,43996,43997,43998,43999,44000,44001,44002,44032,44033,44034,44035,44036,44037,44038,44039,44040,44041,44042,44043,44044,44045,44046,44047,44048,44049,44050,44051,44052,44053,44054,44055,44056,44057,44058,44059,44060,44061,44062,44063,44064,44065,44066,44067,44068,44069,44070,44071,44072,44073,44074,44075,44076,44077,44078,44079,44080,44081,44082,44083,44084,44085,44086,44087,44088,44089,44090,44091,44092,44093,44094,44095,44096,44097,44098,44099,44100,44101,44102,44103,44104,44105,44106,44107,44108,44109,44110,44111,44112,44113,44114,44115,44116,44117,44118,44119,44120,44121,44122,44123,44124,44125,44126,44127,44128,44129,44130,44131,44132,44133,44134,44135,44136,44137,44138,44139,44140,44141,44142,44143,44144,44145,44146,44147,44148,44149,44150,44151,44152,44153,44154,44155,44156,44157,44158,44159,44160,44161,44162,44163,44164,44165,44166,44167,44168,44169,44170,44171,44172,44173,44174,44175,44176,44177,44178,44179,44180,44181,44182,44183,44184,44185,44186,44187,44188,44189,44190,44191,44192,44193,44194,44195,44196,44197,44198,44199,44200,44201,44202,44203,44204,44205,44206,44207,44208,44209,44210,44211,44212,44213,44214,44215,44216,44217,44218,44219,44220,44221,44222,44223,44224,44225,44226,44227,44228,44229,44230,44231,44232,44233,44234,44235,44236,44237,44238,44239,44240,44241,44242,44243,44244,44245,44246,44247,44248,44249,44250,44251,44252,44253,44254,44255,44256,44257,44258,44259,44260,44261,44262,44263,44264,44265,44266,44267,44268,44269,44270,44271,44272,44273,44274,44275,44276,44277,44278,44279,44280,44281,44282,44283,44284,44285,44286,44287,44288,44289,44290,44291,44292,44293,44294,44295,44296,44297,44298,44299,44300,44301,44302,44303,44304,44305,44306,44307,44308,44309,44310,44311,44312,44313,44314,44315,44316,44317,44318,44319,44320,44321,44322,44323,44324,44325,44326,44327,44328,44329,44330,44331,44332,44333,44334,44335,44336,44337,44338,44339,44340,44341,44342,44343,44344,44345,44346,44347,44348,44349,44350,44351,44352,44353,44354,44355,44356,44357,44358,44359,44360,44361,44362,44363,44364,44365,44366,44367,44368,44369,44370,44371,44372,44373,44374,44375,44376,44377,44378,44379,44380,44381,44382,44383,44384,44385,44386,44387,44388,44389,44390,44391,44392,44393,44394,44395,44396,44397,44398,44399,44400,44401,44402,44403,44404,44405,44406,44407,44408,44409,44410,44411,44412,44413,44414,44415,44416,44417,44418,44419,44420,44421,44422,44423,44424,44425,44426,44427,44428,44429,44430,44431,44432,44433,44434,44435,44436,44437,44438,44439,44440,44441,44442,44443,44444,44445,44446,44447,44448,44449,44450,44451,44452,44453,44454,44455,44456,44457,44458,44459,44460,44461,44462,44463,44464,44465,44466,44467,44468,44469,44470,44471,44472,44473,44474,44475,44476,44477,44478,44479,44480,44481,44482,44483,44484,44485,44486,44487,44488,44489,44490,44491,44492,44493,44494,44495,44496,44497,44498,44499,44500,44501,44502,44503,44504,44505,44506,44507,44508,44509,44510,44511,44512,44513,44514,44515,44516,44517,44518,44519,44520,44521,44522,44523,44524,44525,44526,44527,44528,44529,44530,44531,44532,44533,44534,44535,44536,44537,44538,44539,44540,44541,44542,44543,44544,44545,44546,44547,44548,44549,44550,44551,44552,44553,44554,44555,44556,44557,44558,44559,44560,44561,44562,44563,44564,44565,44566,44567,44568,44569,44570,44571,44572,44573,44574,44575,44576,44577,44578,44579,44580,44581,44582,44583,44584,44585,44586,44587,44588,44589,44590,44591,44592,44593,44594,44595,44596,44597,44598,44599,44600,44601,44602,44603,44604,44605,44606,44607,44608,44609,44610,44611,44612,44613,44614,44615,44616,44617,44618,44619,44620,44621,44622,44623,44624,44625,44626,44627,44628,44629,44630,44631,44632,44633,44634,44635,44636,44637,44638,44639,44640,44641,44642,44643,44644,44645,44646,44647,44648,44649,44650,44651,44652,44653,44654,44655,44656,44657,44658,44659,44660,44661,44662,44663,44664,44665,44666,44667,44668,44669,44670,44671,44672,44673,44674,44675,44676,44677,44678,44679,44680,44681,44682,44683,44684,44685,44686,44687,44688,44689,44690,44691,44692,44693,44694,44695,44696,44697,44698,44699,44700,44701,44702,44703,44704,44705,44706,44707,44708,44709,44710,44711,44712,44713,44714,44715,44716,44717,44718,44719,44720,44721,44722,44723,44724,44725,44726,44727,44728,44729,44730,44731,44732,44733,44734,44735,44736,44737,44738,44739,44740,44741,44742,44743,44744,44745,44746,44747,44748,44749,44750,44751,44752,44753,44754,44755,44756,44757,44758,44759,44760,44761,44762,44763,44764,44765,44766,44767,44768,44769,44770,44771,44772,44773,44774,44775,44776,44777,44778,44779,44780,44781,44782,44783,44784,44785,44786,44787,44788,44789,44790,44791,44792,44793,44794,44795,44796,44797,44798,44799,44800,44801,44802,44803,44804,44805,44806,44807,44808,44809,44810,44811,44812,44813,44814,44815,44816,44817,44818,44819,44820,44821,44822,44823,44824,44825,44826,44827,44828,44829,44830,44831,44832,44833,44834,44835,44836,44837,44838,44839,44840,44841,44842,44843,44844,44845,44846,44847,44848,44849,44850,44851,44852,44853,44854,44855,44856,44857,44858,44859,44860,44861,44862,44863,44864,44865,44866,44867,44868,44869,44870,44871,44872,44873,44874,44875,44876,44877,44878,44879,44880,44881,44882,44883,44884,44885,44886,44887,44888,44889,44890,44891,44892,44893,44894,44895,44896,44897,44898,44899,44900,44901,44902,44903,44904,44905,44906,44907,44908,44909,44910,44911,44912,44913,44914,44915,44916,44917,44918,44919,44920,44921,44922,44923,44924,44925,44926,44927,44928,44929,44930,44931,44932,44933,44934,44935,44936,44937,44938,44939,44940,44941,44942,44943,44944,44945,44946,44947,44948,44949,44950,44951,44952,44953,44954,44955,44956,44957,44958,44959,44960,44961,44962,44963,44964,44965,44966,44967,44968,44969,44970,44971,44972,44973,44974,44975,44976,44977,44978,44979,44980,44981,44982,44983,44984,44985,44986,44987,44988,44989,44990,44991,44992,44993,44994,44995,44996,44997,44998,44999,45000,45001,45002,45003,45004,45005,45006,45007,45008,45009,45010,45011,45012,45013,45014,45015,45016,45017,45018,45019,45020,45021,45022,45023,45024,45025,45026,45027,45028,45029,45030,45031,45032,45033,45034,45035,45036,45037,45038,45039,45040,45041,45042,45043,45044,45045,45046,45047,45048,45049,45050,45051,45052,45053,45054,45055,45056,45057,45058,45059,45060,45061,45062,45063,45064,45065,45066,45067,45068,45069,45070,45071,45072,45073,45074,45075,45076,45077,45078,45079,45080,45081,45082,45083,45084,45085,45086,45087,45088,45089,45090,45091,45092,45093,45094,45095,45096,45097,45098,45099,45100,45101,45102,45103,45104,45105,45106,45107,45108,45109,45110,45111,45112,45113,45114,45115,45116,45117,45118,45119,45120,45121,45122,45123,45124,45125,45126,45127,45128,45129,45130,45131,45132,45133,45134,45135,45136,45137,45138,45139,45140,45141,45142,45143,45144,45145,45146,45147,45148,45149,45150,45151,45152,45153,45154,45155,45156,45157,45158,45159,45160,45161,45162,45163,45164,45165,45166,45167,45168,45169,45170,45171,45172,45173,45174,45175,45176,45177,45178,45179,45180,45181,45182,45183,45184,45185,45186,45187,45188,45189,45190,45191,45192,45193,45194,45195,45196,45197,45198,45199,45200,45201,45202,45203,45204,45205,45206,45207,45208,45209,45210,45211,45212,45213,45214,45215,45216,45217,45218,45219,45220,45221,45222,45223,45224,45225,45226,45227,45228,45229,45230,45231,45232,45233,45234,45235,45236,45237,45238,45239,45240,45241,45242,45243,45244,45245,45246,45247,45248,45249,45250,45251,45252,45253,45254,45255,45256,45257,45258,45259,45260,45261,45262,45263,45264,45265,45266,45267,45268,45269,45270,45271,45272,45273,45274,45275,45276,45277,45278,45279,45280,45281,45282,45283,45284,45285,45286,45287,45288,45289,45290,45291,45292,45293,45294,45295,45296,45297,45298,45299,45300,45301,45302,45303,45304,45305,45306,45307,45308,45309,45310,45311,45312,45313,45314,45315,45316,45317,45318,45319,45320,45321,45322,45323,45324,45325,45326,45327,45328,45329,45330,45331,45332,45333,45334,45335,45336,45337,45338,45339,45340,45341,45342,45343,45344,45345,45346,45347,45348,45349,45350,45351,45352,45353,45354,45355,45356,45357,45358,45359,45360,45361,45362,45363,45364,45365,45366,45367,45368,45369,45370,45371,45372,45373,45374,45375,45376,45377,45378,45379,45380,45381,45382,45383,45384,45385,45386,45387,45388,45389,45390,45391,45392,45393,45394,45395,45396,45397,45398,45399,45400,45401,45402,45403,45404,45405,45406,45407,45408,45409,45410,45411,45412,45413,45414,45415,45416,45417,45418,45419,45420,45421,45422,45423,45424,45425,45426,45427,45428,45429,45430,45431,45432,45433,45434,45435,45436,45437,45438,45439,45440,45441,45442,45443,45444,45445,45446,45447,45448,45449,45450,45451,45452,45453,45454,45455,45456,45457,45458,45459,45460,45461,45462,45463,45464,45465,45466,45467,45468,45469,45470,45471,45472,45473,45474,45475,45476,45477,45478,45479,45480,45481,45482,45483,45484,45485,45486,45487,45488,45489,45490,45491,45492,45493,45494,45495,45496,45497,45498,45499,45500,45501,45502,45503,45504,45505,45506,45507,45508,45509,45510,45511,45512,45513,45514,45515,45516,45517,45518,45519,45520,45521,45522,45523,45524,45525,45526,45527,45528,45529,45530,45531,45532,45533,45534,45535,45536,45537,45538,45539,45540,45541,45542,45543,45544,45545,45546,45547,45548,45549,45550,45551,45552,45553,45554,45555,45556,45557,45558,45559,45560,45561,45562,45563,45564,45565,45566,45567,45568,45569,45570,45571,45572,45573,45574,45575,45576,45577,45578,45579,45580,45581,45582,45583,45584,45585,45586,45587,45588,45589,45590,45591,45592,45593,45594,45595,45596,45597,45598,45599,45600,45601,45602,45603,45604,45605,45606,45607,45608,45609,45610,45611,45612,45613,45614,45615,45616,45617,45618,45619,45620,45621,45622,45623,45624,45625,45626,45627,45628,45629,45630,45631,45632,45633,45634,45635,45636,45637,45638,45639,45640,45641,45642,45643,45644,45645,45646,45647,45648,45649,45650,45651,45652,45653,45654,45655,45656,45657,45658,45659,45660,45661,45662,45663,45664,45665,45666,45667,45668,45669,45670,45671,45672,45673,45674,45675,45676,45677,45678,45679,45680,45681,45682,45683,45684,45685,45686,45687,45688,45689,45690,45691,45692,45693,45694,45695,45696,45697,45698,45699,45700,45701,45702,45703,45704,45705,45706,45707,45708,45709,45710,45711,45712,45713,45714,45715,45716,45717,45718,45719,45720,45721,45722,45723,45724,45725,45726,45727,45728,45729,45730,45731,45732,45733,45734,45735,45736,45737,45738,45739,45740,45741,45742,45743,45744,45745,45746,45747,45748,45749,45750,45751,45752,45753,45754,45755,45756,45757,45758,45759,45760,45761,45762,45763,45764,45765,45766,45767,45768,45769,45770,45771,45772,45773,45774,45775,45776,45777,45778,45779,45780,45781,45782,45783,45784,45785,45786,45787,45788,45789,45790,45791,45792,45793,45794,45795,45796,45797,45798,45799,45800,45801,45802,45803,45804,45805,45806,45807,45808,45809,45810,45811,45812,45813,45814,45815,45816,45817,45818,45819,45820,45821,45822,45823,45824,45825,45826,45827,45828,45829,45830,45831,45832,45833,45834,45835,45836,45837,45838,45839,45840,45841,45842,45843,45844,45845,45846,45847,45848,45849,45850,45851,45852,45853,45854,45855,45856,45857,45858,45859,45860,45861,45862,45863,45864,45865,45866,45867,45868,45869,45870,45871,45872,45873,45874,45875,45876,45877,45878,45879,45880,45881,45882,45883,45884,45885,45886,45887,45888,45889,45890,45891,45892,45893,45894,45895,45896,45897,45898,45899,45900,45901,45902,45903,45904,45905,45906,45907,45908,45909,45910,45911,45912,45913,45914,45915,45916,45917,45918,45919,45920,45921,45922,45923,45924,45925,45926,45927,45928,45929,45930,45931,45932,45933,45934,45935,45936,45937,45938,45939,45940,45941,45942,45943,45944,45945,45946,45947,45948,45949,45950,45951,45952,45953,45954,45955,45956,45957,45958,45959,45960,45961,45962,45963,45964,45965,45966,45967,45968,45969,45970,45971,45972,45973,45974,45975,45976,45977,45978,45979,45980,45981,45982,45983,45984,45985,45986,45987,45988,45989,45990,45991,45992,45993,45994,45995,45996,45997,45998,45999,46000,46001,46002,46003,46004,46005,46006,46007,46008,46009,46010,46011,46012,46013,46014,46015,46016,46017,46018,46019,46020,46021,46022,46023,46024,46025,46026,46027,46028,46029,46030,46031,46032,46033,46034,46035,46036,46037,46038,46039,46040,46041,46042,46043,46044,46045,46046,46047,46048,46049,46050,46051,46052,46053,46054,46055,46056,46057,46058,46059,46060,46061,46062,46063,46064,46065,46066,46067,46068,46069,46070,46071,46072,46073,46074,46075,46076,46077,46078,46079,46080,46081,46082,46083,46084,46085,46086,46087,46088,46089,46090,46091,46092,46093,46094,46095,46096,46097,46098,46099,46100,46101,46102,46103,46104,46105,46106,46107,46108,46109,46110,46111,46112,46113,46114,46115,46116,46117,46118,46119,46120,46121,46122,46123,46124,46125,46126,46127,46128,46129,46130,46131,46132,46133,46134,46135,46136,46137,46138,46139,46140,46141,46142,46143,46144,46145,46146,46147,46148,46149,46150,46151,46152,46153,46154,46155,46156,46157,46158,46159,46160,46161,46162,46163,46164,46165,46166,46167,46168,46169,46170,46171,46172,46173,46174,46175,46176,46177,46178,46179,46180,46181,46182,46183,46184,46185,46186,46187,46188,46189,46190,46191,46192,46193,46194,46195,46196,46197,46198,46199,46200,46201,46202,46203,46204,46205,46206,46207,46208,46209,46210,46211,46212,46213,46214,46215,46216,46217,46218,46219,46220,46221,46222,46223,46224,46225,46226,46227,46228,46229,46230,46231,46232,46233,46234,46235,46236,46237,46238,46239,46240,46241,46242,46243,46244,46245,46246,46247,46248,46249,46250,46251,46252,46253,46254,46255,46256,46257,46258,46259,46260,46261,46262,46263,46264,46265,46266,46267,46268,46269,46270,46271,46272,46273,46274,46275,46276,46277,46278,46279,46280,46281,46282,46283,46284,46285,46286,46287,46288,46289,46290,46291,46292,46293,46294,46295,46296,46297,46298,46299,46300,46301,46302,46303,46304,46305,46306,46307,46308,46309,46310,46311,46312,46313,46314,46315,46316,46317,46318,46319,46320,46321,46322,46323,46324,46325,46326,46327,46328,46329,46330,46331,46332,46333,46334,46335,46336,46337,46338,46339,46340,46341,46342,46343,46344,46345,46346,46347,46348,46349,46350,46351,46352,46353,46354,46355,46356,46357,46358,46359,46360,46361,46362,46363,46364,46365,46366,46367,46368,46369,46370,46371,46372,46373,46374,46375,46376,46377,46378,46379,46380,46381,46382,46383,46384,46385,46386,46387,46388,46389,46390,46391,46392,46393,46394,46395,46396,46397,46398,46399,46400,46401,46402,46403,46404,46405,46406,46407,46408,46409,46410,46411,46412,46413,46414,46415,46416,46417,46418,46419,46420,46421,46422,46423,46424,46425,46426,46427,46428,46429,46430,46431,46432,46433,46434,46435,46436,46437,46438,46439,46440,46441,46442,46443,46444,46445,46446,46447,46448,46449,46450,46451,46452,46453,46454,46455,46456,46457,46458,46459,46460,46461,46462,46463,46464,46465,46466,46467,46468,46469,46470,46471,46472,46473,46474,46475,46476,46477,46478,46479,46480,46481,46482,46483,46484,46485,46486,46487,46488,46489,46490,46491,46492,46493,46494,46495,46496,46497,46498,46499,46500,46501,46502,46503,46504,46505,46506,46507,46508,46509,46510,46511,46512,46513,46514,46515,46516,46517,46518,46519,46520,46521,46522,46523,46524,46525,46526,46527,46528,46529,46530,46531,46532,46533,46534,46535,46536,46537,46538,46539,46540,46541,46542,46543,46544,46545,46546,46547,46548,46549,46550,46551,46552,46553,46554,46555,46556,46557,46558,46559,46560,46561,46562,46563,46564,46565,46566,46567,46568,46569,46570,46571,46572,46573,46574,46575,46576,46577,46578,46579,46580,46581,46582,46583,46584,46585,46586,46587,46588,46589,46590,46591,46592,46593,46594,46595,46596,46597,46598,46599,46600,46601,46602,46603,46604,46605,46606,46607,46608,46609,46610,46611,46612,46613,46614,46615,46616,46617,46618,46619,46620,46621,46622,46623,46624,46625,46626,46627,46628,46629,46630,46631,46632,46633,46634,46635,46636,46637,46638,46639,46640,46641,46642,46643,46644,46645,46646,46647,46648,46649,46650,46651,46652,46653,46654,46655,46656,46657,46658,46659,46660,46661,46662,46663,46664,46665,46666,46667,46668,46669,46670,46671,46672,46673,46674,46675,46676,46677,46678,46679,46680,46681,46682,46683,46684,46685,46686,46687,46688,46689,46690,46691,46692,46693,46694,46695,46696,46697,46698,46699,46700,46701,46702,46703,46704,46705,46706,46707,46708,46709,46710,46711,46712,46713,46714,46715,46716,46717,46718,46719,46720,46721,46722,46723,46724,46725,46726,46727,46728,46729,46730,46731,46732,46733,46734,46735,46736,46737,46738,46739,46740,46741,46742,46743,46744,46745,46746,46747,46748,46749,46750,46751,46752,46753,46754,46755,46756,46757,46758,46759,46760,46761,46762,46763,46764,46765,46766,46767,46768,46769,46770,46771,46772,46773,46774,46775,46776,46777,46778,46779,46780,46781,46782,46783,46784,46785,46786,46787,46788,46789,46790,46791,46792,46793,46794,46795,46796,46797,46798,46799,46800,46801,46802,46803,46804,46805,46806,46807,46808,46809,46810,46811,46812,46813,46814,46815,46816,46817,46818,46819,46820,46821,46822,46823,46824,46825,46826,46827,46828,46829,46830,46831,46832,46833,46834,46835,46836,46837,46838,46839,46840,46841,46842,46843,46844,46845,46846,46847,46848,46849,46850,46851,46852,46853,46854,46855,46856,46857,46858,46859,46860,46861,46862,46863,46864,46865,46866,46867,46868,46869,46870,46871,46872,46873,46874,46875,46876,46877,46878,46879,46880,46881,46882,46883,46884,46885,46886,46887,46888,46889,46890,46891,46892,46893,46894,46895,46896,46897,46898,46899,46900,46901,46902,46903,46904,46905,46906,46907,46908,46909,46910,46911,46912,46913,46914,46915,46916,46917,46918,46919,46920,46921,46922,46923,46924,46925,46926,46927,46928,46929,46930,46931,46932,46933,46934,46935,46936,46937,46938,46939,46940,46941,46942,46943,46944,46945,46946,46947,46948,46949,46950,46951,46952,46953,46954,46955,46956,46957,46958,46959,46960,46961,46962,46963,46964,46965,46966,46967,46968,46969,46970,46971,46972,46973,46974,46975,46976,46977,46978,46979,46980,46981,46982,46983,46984,46985,46986,46987,46988,46989,46990,46991,46992,46993,46994,46995,46996,46997,46998,46999,47000,47001,47002,47003,47004,47005,47006,47007,47008,47009,47010,47011,47012,47013,47014,47015,47016,47017,47018,47019,47020,47021,47022,47023,47024,47025,47026,47027,47028,47029,47030,47031,47032,47033,47034,47035,47036,47037,47038,47039,47040,47041,47042,47043,47044,47045,47046,47047,47048,47049,47050,47051,47052,47053,47054,47055,47056,47057,47058,47059,47060,47061,47062,47063,47064,47065,47066,47067,47068,47069,47070,47071,47072,47073,47074,47075,47076,47077,47078,47079,47080,47081,47082,47083,47084,47085,47086,47087,47088,47089,47090,47091,47092,47093,47094,47095,47096,47097,47098,47099,47100,47101,47102,47103,47104,47105,47106,47107,47108,47109,47110,47111,47112,47113,47114,47115,47116,47117,47118,47119,47120,47121,47122,47123,47124,47125,47126,47127,47128,47129,47130,47131,47132,47133,47134,47135,47136,47137,47138,47139,47140,47141,47142,47143,47144,47145,47146,47147,47148,47149,47150,47151,47152,47153,47154,47155,47156,47157,47158,47159,47160,47161,47162,47163,47164,47165,47166,47167,47168,47169,47170,47171,47172,47173,47174,47175,47176,47177,47178,47179,47180,47181,47182,47183,47184,47185,47186,47187,47188,47189,47190,47191,47192,47193,47194,47195,47196,47197,47198,47199,47200,47201,47202,47203,47204,47205,47206,47207,47208,47209,47210,47211,47212,47213,47214,47215,47216,47217,47218,47219,47220,47221,47222,47223,47224,47225,47226,47227,47228,47229,47230,47231,47232,47233,47234,47235,47236,47237,47238,47239,47240,47241,47242,47243,47244,47245,47246,47247,47248,47249,47250,47251,47252,47253,47254,47255,47256,47257,47258,47259,47260,47261,47262,47263,47264,47265,47266,47267,47268,47269,47270,47271,47272,47273,47274,47275,47276,47277,47278,47279,47280,47281,47282,47283,47284,47285,47286,47287,47288,47289,47290,47291,47292,47293,47294,47295,47296,47297,47298,47299,47300,47301,47302,47303,47304,47305,47306,47307,47308,47309,47310,47311,47312,47313,47314,47315,47316,47317,47318,47319,47320,47321,47322,47323,47324,47325,47326,47327,47328,47329,47330,47331,47332,47333,47334,47335,47336,47337,47338,47339,47340,47341,47342,47343,47344,47345,47346,47347,47348,47349,47350,47351,47352,47353,47354,47355,47356,47357,47358,47359,47360,47361,47362,47363,47364,47365,47366,47367,47368,47369,47370,47371,47372,47373,47374,47375,47376,47377,47378,47379,47380,47381,47382,47383,47384,47385,47386,47387,47388,47389,47390,47391,47392,47393,47394,47395,47396,47397,47398,47399,47400,47401,47402,47403,47404,47405,47406,47407,47408,47409,47410,47411,47412,47413,47414,47415,47416,47417,47418,47419,47420,47421,47422,47423,47424,47425,47426,47427,47428,47429,47430,47431,47432,47433,47434,47435,47436,47437,47438,47439,47440,47441,47442,47443,47444,47445,47446,47447,47448,47449,47450,47451,47452,47453,47454,47455,47456,47457,47458,47459,47460,47461,47462,47463,47464,47465,47466,47467,47468,47469,47470,47471,47472,47473,47474,47475,47476,47477,47478,47479,47480,47481,47482,47483,47484,47485,47486,47487,47488,47489,47490,47491,47492,47493,47494,47495,47496,47497,47498,47499,47500,47501,47502,47503,47504,47505,47506,47507,47508,47509,47510,47511,47512,47513,47514,47515,47516,47517,47518,47519,47520,47521,47522,47523,47524,47525,47526,47527,47528,47529,47530,47531,47532,47533,47534,47535,47536,47537,47538,47539,47540,47541,47542,47543,47544,47545,47546,47547,47548,47549,47550,47551,47552,47553,47554,47555,47556,47557,47558,47559,47560,47561,47562,47563,47564,47565,47566,47567,47568,47569,47570,47571,47572,47573,47574,47575,47576,47577,47578,47579,47580,47581,47582,47583,47584,47585,47586,47587,47588,47589,47590,47591,47592,47593,47594,47595,47596,47597,47598,47599,47600,47601,47602,47603,47604,47605,47606,47607,47608,47609,47610,47611,47612,47613,47614,47615,47616,47617,47618,47619,47620,47621,47622,47623,47624,47625,47626,47627,47628,47629,47630,47631,47632,47633,47634,47635,47636,47637,47638,47639,47640,47641,47642,47643,47644,47645,47646,47647,47648,47649,47650,47651,47652,47653,47654,47655,47656,47657,47658,47659,47660,47661,47662,47663,47664,47665,47666,47667,47668,47669,47670,47671,47672,47673,47674,47675,47676,47677,47678,47679,47680,47681,47682,47683,47684,47685,47686,47687,47688,47689,47690,47691,47692,47693,47694,47695,47696,47697,47698,47699,47700,47701,47702,47703,47704,47705,47706,47707,47708,47709,47710,47711,47712,47713,47714,47715,47716,47717,47718,47719,47720,47721,47722,47723,47724,47725,47726,47727,47728,47729,47730,47731,47732,47733,47734,47735,47736,47737,47738,47739,47740,47741,47742,47743,47744,47745,47746,47747,47748,47749,47750,47751,47752,47753,47754,47755,47756,47757,47758,47759,47760,47761,47762,47763,47764,47765,47766,47767,47768,47769,47770,47771,47772,47773,47774,47775,47776,47777,47778,47779,47780,47781,47782,47783,47784,47785,47786,47787,47788,47789,47790,47791,47792,47793,47794,47795,47796,47797,47798,47799,47800,47801,47802,47803,47804,47805,47806,47807,47808,47809,47810,47811,47812,47813,47814,47815,47816,47817,47818,47819,47820,47821,47822,47823,47824,47825,47826,47827,47828,47829,47830,47831,47832,47833,47834,47835,47836,47837,47838,47839,47840,47841,47842,47843,47844,47845,47846,47847,47848,47849,47850,47851,47852,47853,47854,47855,47856,47857,47858,47859,47860,47861,47862,47863,47864,47865,47866,47867,47868,47869,47870,47871,47872,47873,47874,47875,47876,47877,47878,47879,47880,47881,47882,47883,47884,47885,47886,47887,47888,47889,47890,47891,47892,47893,47894,47895,47896,47897,47898,47899,47900,47901,47902,47903,47904,47905,47906,47907,47908,47909,47910,47911,47912,47913,47914,47915,47916,47917,47918,47919,47920,47921,47922,47923,47924,47925,47926,47927,47928,47929,47930,47931,47932,47933,47934,47935,47936,47937,47938,47939,47940,47941,47942,47943,47944,47945,47946,47947,47948,47949,47950,47951,47952,47953,47954,47955,47956,47957,47958,47959,47960,47961,47962,47963,47964,47965,47966,47967,47968,47969,47970,47971,47972,47973,47974,47975,47976,47977,47978,47979,47980,47981,47982,47983,47984,47985,47986,47987,47988,47989,47990,47991,47992,47993,47994,47995,47996,47997,47998,47999,48000,48001,48002,48003,48004,48005,48006,48007,48008,48009,48010,48011,48012,48013,48014,48015,48016,48017,48018,48019,48020,48021,48022,48023,48024,48025,48026,48027,48028,48029,48030,48031,48032,48033,48034,48035,48036,48037,48038,48039,48040,48041,48042,48043,48044,48045,48046,48047,48048,48049,48050,48051,48052,48053,48054,48055,48056,48057,48058,48059,48060,48061,48062,48063,48064,48065,48066,48067,48068,48069,48070,48071,48072,48073,48074,48075,48076,48077,48078,48079,48080,48081,48082,48083,48084,48085,48086,48087,48088,48089,48090,48091,48092,48093,48094,48095,48096,48097,48098,48099,48100,48101,48102,48103,48104,48105,48106,48107,48108,48109,48110,48111,48112,48113,48114,48115,48116,48117,48118,48119,48120,48121,48122,48123,48124,48125,48126,48127,48128,48129,48130,48131,48132,48133,48134,48135,48136,48137,48138,48139,48140,48141,48142,48143,48144,48145,48146,48147,48148,48149,48150,48151,48152,48153,48154,48155,48156,48157,48158,48159,48160,48161,48162,48163,48164,48165,48166,48167,48168,48169,48170,48171,48172,48173,48174,48175,48176,48177,48178,48179,48180,48181,48182,48183,48184,48185,48186,48187,48188,48189,48190,48191,48192,48193,48194,48195,48196,48197,48198,48199,48200,48201,48202,48203,48204,48205,48206,48207,48208,48209,48210,48211,48212,48213,48214,48215,48216,48217,48218,48219,48220,48221,48222,48223,48224,48225,48226,48227,48228,48229,48230,48231,48232,48233,48234,48235,48236,48237,48238,48239,48240,48241,48242,48243,48244,48245,48246,48247,48248,48249,48250,48251,48252,48253,48254,48255,48256,48257,48258,48259,48260,48261,48262,48263,48264,48265,48266,48267,48268,48269,48270,48271,48272,48273,48274,48275,48276,48277,48278,48279,48280,48281,48282,48283,48284,48285,48286,48287,48288,48289,48290,48291,48292,48293,48294,48295,48296,48297,48298,48299,48300,48301,48302,48303,48304,48305,48306,48307,48308,48309,48310,48311,48312,48313,48314,48315,48316,48317,48318,48319,48320,48321,48322,48323,48324,48325,48326,48327,48328,48329,48330,48331,48332,48333,48334,48335,48336,48337,48338,48339,48340,48341,48342,48343,48344,48345,48346,48347,48348,48349,48350,48351,48352,48353,48354,48355,48356,48357,48358,48359,48360,48361,48362,48363,48364,48365,48366,48367,48368,48369,48370,48371,48372,48373,48374,48375,48376,48377,48378,48379,48380,48381,48382,48383,48384,48385,48386,48387,48388,48389,48390,48391,48392,48393,48394,48395,48396,48397,48398,48399,48400,48401,48402,48403,48404,48405,48406,48407,48408,48409,48410,48411,48412,48413,48414,48415,48416,48417,48418,48419,48420,48421,48422,48423,48424,48425,48426,48427,48428,48429,48430,48431,48432,48433,48434,48435,48436,48437,48438,48439,48440,48441,48442,48443,48444,48445,48446,48447,48448,48449,48450,48451,48452,48453,48454,48455,48456,48457,48458,48459,48460,48461,48462,48463,48464,48465,48466,48467,48468,48469,48470,48471,48472,48473,48474,48475,48476,48477,48478,48479,48480,48481,48482,48483,48484,48485,48486,48487,48488,48489,48490,48491,48492,48493,48494,48495,48496,48497,48498,48499,48500,48501,48502,48503,48504,48505,48506,48507,48508,48509,48510,48511,48512,48513,48514,48515,48516,48517,48518,48519,48520,48521,48522,48523,48524,48525,48526,48527,48528,48529,48530,48531,48532,48533,48534,48535,48536,48537,48538,48539,48540,48541,48542,48543,48544,48545,48546,48547,48548,48549,48550,48551,48552,48553,48554,48555,48556,48557,48558,48559,48560,48561,48562,48563,48564,48565,48566,48567,48568,48569,48570,48571,48572,48573,48574,48575,48576,48577,48578,48579,48580,48581,48582,48583,48584,48585,48586,48587,48588,48589,48590,48591,48592,48593,48594,48595,48596,48597,48598,48599,48600,48601,48602,48603,48604,48605,48606,48607,48608,48609,48610,48611,48612,48613,48614,48615,48616,48617,48618,48619,48620,48621,48622,48623,48624,48625,48626,48627,48628,48629,48630,48631,48632,48633,48634,48635,48636,48637,48638,48639,48640,48641,48642,48643,48644,48645,48646,48647,48648,48649,48650,48651,48652,48653,48654,48655,48656,48657,48658,48659,48660,48661,48662,48663,48664,48665,48666,48667,48668,48669,48670,48671,48672,48673,48674,48675,48676,48677,48678,48679,48680,48681,48682,48683,48684,48685,48686,48687,48688,48689,48690,48691,48692,48693,48694,48695,48696,48697,48698,48699,48700,48701,48702,48703,48704,48705,48706,48707,48708,48709,48710,48711,48712,48713,48714,48715,48716,48717,48718,48719,48720,48721,48722,48723,48724,48725,48726,48727,48728,48729,48730,48731,48732,48733,48734,48735,48736,48737,48738,48739,48740,48741,48742,48743,48744,48745,48746,48747,48748,48749,48750,48751,48752,48753,48754,48755,48756,48757,48758,48759,48760,48761,48762,48763,48764,48765,48766,48767,48768,48769,48770,48771,48772,48773,48774,48775,48776,48777,48778,48779,48780,48781,48782,48783,48784,48785,48786,48787,48788,48789,48790,48791,48792,48793,48794,48795,48796,48797,48798,48799,48800,48801,48802,48803,48804,48805,48806,48807,48808,48809,48810,48811,48812,48813,48814,48815,48816,48817,48818,48819,48820,48821,48822,48823,48824,48825,48826,48827,48828,48829,48830,48831,48832,48833,48834,48835,48836,48837,48838,48839,48840,48841,48842,48843,48844,48845,48846,48847,48848,48849,48850,48851,48852,48853,48854,48855,48856,48857,48858,48859,48860,48861,48862,48863,48864,48865,48866,48867,48868,48869,48870,48871,48872,48873,48874,48875,48876,48877,48878,48879,48880,48881,48882,48883,48884,48885,48886,48887,48888,48889,48890,48891,48892,48893,48894,48895,48896,48897,48898,48899,48900,48901,48902,48903,48904,48905,48906,48907,48908,48909,48910,48911,48912,48913,48914,48915,48916,48917,48918,48919,48920,48921,48922,48923,48924,48925,48926,48927,48928,48929,48930,48931,48932,48933,48934,48935,48936,48937,48938,48939,48940,48941,48942,48943,48944,48945,48946,48947,48948,48949,48950,48951,48952,48953,48954,48955,48956,48957,48958,48959,48960,48961,48962,48963,48964,48965,48966,48967,48968,48969,48970,48971,48972,48973,48974,48975,48976,48977,48978,48979,48980,48981,48982,48983,48984,48985,48986,48987,48988,48989,48990,48991,48992,48993,48994,48995,48996,48997,48998,48999,49000,49001,49002,49003,49004,49005,49006,49007,49008,49009,49010,49011,49012,49013,49014,49015,49016,49017,49018,49019,49020,49021,49022,49023,49024,49025,49026,49027,49028,49029,49030,49031,49032,49033,49034,49035,49036,49037,49038,49039,49040,49041,49042,49043,49044,49045,49046,49047,49048,49049,49050,49051,49052,49053,49054,49055,49056,49057,49058,49059,49060,49061,49062,49063,49064,49065,49066,49067,49068,49069,49070,49071,49072,49073,49074,49075,49076,49077,49078,49079,49080,49081,49082,49083,49084,49085,49086,49087,49088,49089,49090,49091,49092,49093,49094,49095,49096,49097,49098,49099,49100,49101,49102,49103,49104,49105,49106,49107,49108,49109,49110,49111,49112,49113,49114,49115,49116,49117,49118,49119,49120,49121,49122,49123,49124,49125,49126,49127,49128,49129,49130,49131,49132,49133,49134,49135,49136,49137,49138,49139,49140,49141,49142,49143,49144,49145,49146,49147,49148,49149,49150,49151,49152,49153,49154,49155,49156,49157,49158,49159,49160,49161,49162,49163,49164,49165,49166,49167,49168,49169,49170,49171,49172,49173,49174,49175,49176,49177,49178,49179,49180,49181,49182,49183,49184,49185,49186,49187,49188,49189,49190,49191,49192,49193,49194,49195,49196,49197,49198,49199,49200,49201,49202,49203,49204,49205,49206,49207,49208,49209,49210,49211,49212,49213,49214,49215,49216,49217,49218,49219,49220,49221,49222,49223,49224,49225,49226,49227,49228,49229,49230,49231,49232,49233,49234,49235,49236,49237,49238,49239,49240,49241,49242,49243,49244,49245,49246,49247,49248,49249,49250,49251,49252,49253,49254,49255,49256,49257,49258,49259,49260,49261,49262,49263,49264,49265,49266,49267,49268,49269,49270,49271,49272,49273,49274,49275,49276,49277,49278,49279,49280,49281,49282,49283,49284,49285,49286,49287,49288,49289,49290,49291,49292,49293,49294,49295,49296,49297,49298,49299,49300,49301,49302,49303,49304,49305,49306,49307,49308,49309,49310,49311,49312,49313,49314,49315,49316,49317,49318,49319,49320,49321,49322,49323,49324,49325,49326,49327,49328,49329,49330,49331,49332,49333,49334,49335,49336,49337,49338,49339,49340,49341,49342,49343,49344,49345,49346,49347,49348,49349,49350,49351,49352,49353,49354,49355,49356,49357,49358,49359,49360,49361,49362,49363,49364,49365,49366,49367,49368,49369,49370,49371,49372,49373,49374,49375,49376,49377,49378,49379,49380,49381,49382,49383,49384,49385,49386,49387,49388,49389,49390,49391,49392,49393,49394,49395,49396,49397,49398,49399,49400,49401,49402,49403,49404,49405,49406,49407,49408,49409,49410,49411,49412,49413,49414,49415,49416,49417,49418,49419,49420,49421,49422,49423,49424,49425,49426,49427,49428,49429,49430,49431,49432,49433,49434,49435,49436,49437,49438,49439,49440,49441,49442,49443,49444,49445,49446,49447,49448,49449,49450,49451,49452,49453,49454,49455,49456,49457,49458,49459,49460,49461,49462,49463,49464,49465,49466,49467,49468,49469,49470,49471,49472,49473,49474,49475,49476,49477,49478,49479,49480,49481,49482,49483,49484,49485,49486,49487,49488,49489,49490,49491,49492,49493,49494,49495,49496,49497,49498,49499,49500,49501,49502,49503,49504,49505,49506,49507,49508,49509,49510,49511,49512,49513,49514,49515,49516,49517,49518,49519,49520,49521,49522,49523,49524,49525,49526,49527,49528,49529,49530,49531,49532,49533,49534,49535,49536,49537,49538,49539,49540,49541,49542,49543,49544,49545,49546,49547,49548,49549,49550,49551,49552,49553,49554,49555,49556,49557,49558,49559,49560,49561,49562,49563,49564,49565,49566,49567,49568,49569,49570,49571,49572,49573,49574,49575,49576,49577,49578,49579,49580,49581,49582,49583,49584,49585,49586,49587,49588,49589,49590,49591,49592,49593,49594,49595,49596,49597,49598,49599,49600,49601,49602,49603,49604,49605,49606,49607,49608,49609,49610,49611,49612,49613,49614,49615,49616,49617,49618,49619,49620,49621,49622,49623,49624,49625,49626,49627,49628,49629,49630,49631,49632,49633,49634,49635,49636,49637,49638,49639,49640,49641,49642,49643,49644,49645,49646,49647,49648,49649,49650,49651,49652,49653,49654,49655,49656,49657,49658,49659,49660,49661,49662,49663,49664,49665,49666,49667,49668,49669,49670,49671,49672,49673,49674,49675,49676,49677,49678,49679,49680,49681,49682,49683,49684,49685,49686,49687,49688,49689,49690,49691,49692,49693,49694,49695,49696,49697,49698,49699,49700,49701,49702,49703,49704,49705,49706,49707,49708,49709,49710,49711,49712,49713,49714,49715,49716,49717,49718,49719,49720,49721,49722,49723,49724,49725,49726,49727,49728,49729,49730,49731,49732,49733,49734,49735,49736,49737,49738,49739,49740,49741,49742,49743,49744,49745,49746,49747,49748,49749,49750,49751,49752,49753,49754,49755,49756,49757,49758,49759,49760,49761,49762,49763,49764,49765,49766,49767,49768,49769,49770,49771,49772,49773,49774,49775,49776,49777,49778,49779,49780,49781,49782,49783,49784,49785,49786,49787,49788,49789,49790,49791,49792,49793,49794,49795,49796,49797,49798,49799,49800,49801,49802,49803,49804,49805,49806,49807,49808,49809,49810,49811,49812,49813,49814,49815,49816,49817,49818,49819,49820,49821,49822,49823,49824,49825,49826,49827,49828,49829,49830,49831,49832,49833,49834,49835,49836,49837,49838,49839,49840,49841,49842,49843,49844,49845,49846,49847,49848,49849,49850,49851,49852,49853,49854,49855,49856,49857,49858,49859,49860,49861,49862,49863,49864,49865,49866,49867,49868,49869,49870,49871,49872,49873,49874,49875,49876,49877,49878,49879,49880,49881,49882,49883,49884,49885,49886,49887,49888,49889,49890,49891,49892,49893,49894,49895,49896,49897,49898,49899,49900,49901,49902,49903,49904,49905,49906,49907,49908,49909,49910,49911,49912,49913,49914,49915,49916,49917,49918,49919,49920,49921,49922,49923,49924,49925,49926,49927,49928,49929,49930,49931,49932,49933,49934,49935,49936,49937,49938,49939,49940,49941,49942,49943,49944,49945,49946,49947,49948,49949,49950,49951,49952,49953,49954,49955,49956,49957,49958,49959,49960,49961,49962,49963,49964,49965,49966,49967,49968,49969,49970,49971,49972,49973,49974,49975,49976,49977,49978,49979,49980,49981,49982,49983,49984,49985,49986,49987,49988,49989,49990,49991,49992,49993,49994,49995,49996,49997,49998,49999,50000,50001,50002,50003,50004,50005,50006,50007,50008,50009,50010,50011,50012,50013,50014,50015,50016,50017,50018,50019,50020,50021,50022,50023,50024,50025,50026,50027,50028,50029,50030,50031,50032,50033,50034,50035,50036,50037,50038,50039,50040,50041,50042,50043,50044,50045,50046,50047,50048,50049,50050,50051,50052,50053,50054,50055,50056,50057,50058,50059,50060,50061,50062,50063,50064,50065,50066,50067,50068,50069,50070,50071,50072,50073,50074,50075,50076,50077,50078,50079,50080,50081,50082,50083,50084,50085,50086,50087,50088,50089,50090,50091,50092,50093,50094,50095,50096,50097,50098,50099,50100,50101,50102,50103,50104,50105,50106,50107,50108,50109,50110,50111,50112,50113,50114,50115,50116,50117,50118,50119,50120,50121,50122,50123,50124,50125,50126,50127,50128,50129,50130,50131,50132,50133,50134,50135,50136,50137,50138,50139,50140,50141,50142,50143,50144,50145,50146,50147,50148,50149,50150,50151,50152,50153,50154,50155,50156,50157,50158,50159,50160,50161,50162,50163,50164,50165,50166,50167,50168,50169,50170,50171,50172,50173,50174,50175,50176,50177,50178,50179,50180,50181,50182,50183,50184,50185,50186,50187,50188,50189,50190,50191,50192,50193,50194,50195,50196,50197,50198,50199,50200,50201,50202,50203,50204,50205,50206,50207,50208,50209,50210,50211,50212,50213,50214,50215,50216,50217,50218,50219,50220,50221,50222,50223,50224,50225,50226,50227,50228,50229,50230,50231,50232,50233,50234,50235,50236,50237,50238,50239,50240,50241,50242,50243,50244,50245,50246,50247,50248,50249,50250,50251,50252,50253,50254,50255,50256,50257,50258,50259,50260,50261,50262,50263,50264,50265,50266,50267,50268,50269,50270,50271,50272,50273,50274,50275,50276,50277,50278,50279,50280,50281,50282,50283,50284,50285,50286,50287,50288,50289,50290,50291,50292,50293,50294,50295,50296,50297,50298,50299,50300,50301,50302,50303,50304,50305,50306,50307,50308,50309,50310,50311,50312,50313,50314,50315,50316,50317,50318,50319,50320,50321,50322,50323,50324,50325,50326,50327,50328,50329,50330,50331,50332,50333,50334,50335,50336,50337,50338,50339,50340,50341,50342,50343,50344,50345,50346,50347,50348,50349,50350,50351,50352,50353,50354,50355,50356,50357,50358,50359,50360,50361,50362,50363,50364,50365,50366,50367,50368,50369,50370,50371,50372,50373,50374,50375,50376,50377,50378,50379,50380,50381,50382,50383,50384,50385,50386,50387,50388,50389,50390,50391,50392,50393,50394,50395,50396,50397,50398,50399,50400,50401,50402,50403,50404,50405,50406,50407,50408,50409,50410,50411,50412,50413,50414,50415,50416,50417,50418,50419,50420,50421,50422,50423,50424,50425,50426,50427,50428,50429,50430,50431,50432,50433,50434,50435,50436,50437,50438,50439,50440,50441,50442,50443,50444,50445,50446,50447,50448,50449,50450,50451,50452,50453,50454,50455,50456,50457,50458,50459,50460,50461,50462,50463,50464,50465,50466,50467,50468,50469,50470,50471,50472,50473,50474,50475,50476,50477,50478,50479,50480,50481,50482,50483,50484,50485,50486,50487,50488,50489,50490,50491,50492,50493,50494,50495,50496,50497,50498,50499,50500,50501,50502,50503,50504,50505,50506,50507,50508,50509,50510,50511,50512,50513,50514,50515,50516,50517,50518,50519,50520,50521,50522,50523,50524,50525,50526,50527,50528,50529,50530,50531,50532,50533,50534,50535,50536,50537,50538,50539,50540,50541,50542,50543,50544,50545,50546,50547,50548,50549,50550,50551,50552,50553,50554,50555,50556,50557,50558,50559,50560,50561,50562,50563,50564,50565,50566,50567,50568,50569,50570,50571,50572,50573,50574,50575,50576,50577,50578,50579,50580,50581,50582,50583,50584,50585,50586,50587,50588,50589,50590,50591,50592,50593,50594,50595,50596,50597,50598,50599,50600,50601,50602,50603,50604,50605,50606,50607,50608,50609,50610,50611,50612,50613,50614,50615,50616,50617,50618,50619,50620,50621,50622,50623,50624,50625,50626,50627,50628,50629,50630,50631,50632,50633,50634,50635,50636,50637,50638,50639,50640,50641,50642,50643,50644,50645,50646,50647,50648,50649,50650,50651,50652,50653,50654,50655,50656,50657,50658,50659,50660,50661,50662,50663,50664,50665,50666,50667,50668,50669,50670,50671,50672,50673,50674,50675,50676,50677,50678,50679,50680,50681,50682,50683,50684,50685,50686,50687,50688,50689,50690,50691,50692,50693,50694,50695,50696,50697,50698,50699,50700,50701,50702,50703,50704,50705,50706,50707,50708,50709,50710,50711,50712,50713,50714,50715,50716,50717,50718,50719,50720,50721,50722,50723,50724,50725,50726,50727,50728,50729,50730,50731,50732,50733,50734,50735,50736,50737,50738,50739,50740,50741,50742,50743,50744,50745,50746,50747,50748,50749,50750,50751,50752,50753,50754,50755,50756,50757,50758,50759,50760,50761,50762,50763,50764,50765,50766,50767,50768,50769,50770,50771,50772,50773,50774,50775,50776,50777,50778,50779,50780,50781,50782,50783,50784,50785,50786,50787,50788,50789,50790,50791,50792,50793,50794,50795,50796,50797,50798,50799,50800,50801,50802,50803,50804,50805,50806,50807,50808,50809,50810,50811,50812,50813,50814,50815,50816,50817,50818,50819,50820,50821,50822,50823,50824,50825,50826,50827,50828,50829,50830,50831,50832,50833,50834,50835,50836,50837,50838,50839,50840,50841,50842,50843,50844,50845,50846,50847,50848,50849,50850,50851,50852,50853,50854,50855,50856,50857,50858,50859,50860,50861,50862,50863,50864,50865,50866,50867,50868,50869,50870,50871,50872,50873,50874,50875,50876,50877,50878,50879,50880,50881,50882,50883,50884,50885,50886,50887,50888,50889,50890,50891,50892,50893,50894,50895,50896,50897,50898,50899,50900,50901,50902,50903,50904,50905,50906,50907,50908,50909,50910,50911,50912,50913,50914,50915,50916,50917,50918,50919,50920,50921,50922,50923,50924,50925,50926,50927,50928,50929,50930,50931,50932,50933,50934,50935,50936,50937,50938,50939,50940,50941,50942,50943,50944,50945,50946,50947,50948,50949,50950,50951,50952,50953,50954,50955,50956,50957,50958,50959,50960,50961,50962,50963,50964,50965,50966,50967,50968,50969,50970,50971,50972,50973,50974,50975,50976,50977,50978,50979,50980,50981,50982,50983,50984,50985,50986,50987,50988,50989,50990,50991,50992,50993,50994,50995,50996,50997,50998,50999,51000,51001,51002,51003,51004,51005,51006,51007,51008,51009,51010,51011,51012,51013,51014,51015,51016,51017,51018,51019,51020,51021,51022,51023,51024,51025,51026,51027,51028,51029,51030,51031,51032,51033,51034,51035,51036,51037,51038,51039,51040,51041,51042,51043,51044,51045,51046,51047,51048,51049,51050,51051,51052,51053,51054,51055,51056,51057,51058,51059,51060,51061,51062,51063,51064,51065,51066,51067,51068,51069,51070,51071,51072,51073,51074,51075,51076,51077,51078,51079,51080,51081,51082,51083,51084,51085,51086,51087,51088,51089,51090,51091,51092,51093,51094,51095,51096,51097,51098,51099,51100,51101,51102,51103,51104,51105,51106,51107,51108,51109,51110,51111,51112,51113,51114,51115,51116,51117,51118,51119,51120,51121,51122,51123,51124,51125,51126,51127,51128,51129,51130,51131,51132,51133,51134,51135,51136,51137,51138,51139,51140,51141,51142,51143,51144,51145,51146,51147,51148,51149,51150,51151,51152,51153,51154,51155,51156,51157,51158,51159,51160,51161,51162,51163,51164,51165,51166,51167,51168,51169,51170,51171,51172,51173,51174,51175,51176,51177,51178,51179,51180,51181,51182,51183,51184,51185,51186,51187,51188,51189,51190,51191,51192,51193,51194,51195,51196,51197,51198,51199,51200,51201,51202,51203,51204,51205,51206,51207,51208,51209,51210,51211,51212,51213,51214,51215,51216,51217,51218,51219,51220,51221,51222,51223,51224,51225,51226,51227,51228,51229,51230,51231,51232,51233,51234,51235,51236,51237,51238,51239,51240,51241,51242,51243,51244,51245,51246,51247,51248,51249,51250,51251,51252,51253,51254,51255,51256,51257,51258,51259,51260,51261,51262,51263,51264,51265,51266,51267,51268,51269,51270,51271,51272,51273,51274,51275,51276,51277,51278,51279,51280,51281,51282,51283,51284,51285,51286,51287,51288,51289,51290,51291,51292,51293,51294,51295,51296,51297,51298,51299,51300,51301,51302,51303,51304,51305,51306,51307,51308,51309,51310,51311,51312,51313,51314,51315,51316,51317,51318,51319,51320,51321,51322,51323,51324,51325,51326,51327,51328,51329,51330,51331,51332,51333,51334,51335,51336,51337,51338,51339,51340,51341,51342,51343,51344,51345,51346,51347,51348,51349,51350,51351,51352,51353,51354,51355,51356,51357,51358,51359,51360,51361,51362,51363,51364,51365,51366,51367,51368,51369,51370,51371,51372,51373,51374,51375,51376,51377,51378,51379,51380,51381,51382,51383,51384,51385,51386,51387,51388,51389,51390,51391,51392,51393,51394,51395,51396,51397,51398,51399,51400,51401,51402,51403,51404,51405,51406,51407,51408,51409,51410,51411,51412,51413,51414,51415,51416,51417,51418,51419,51420,51421,51422,51423,51424,51425,51426,51427,51428,51429,51430,51431,51432,51433,51434,51435,51436,51437,51438,51439,51440,51441,51442,51443,51444,51445,51446,51447,51448,51449,51450,51451,51452,51453,51454,51455,51456,51457,51458,51459,51460,51461,51462,51463,51464,51465,51466,51467,51468,51469,51470,51471,51472,51473,51474,51475,51476,51477,51478,51479,51480,51481,51482,51483,51484,51485,51486,51487,51488,51489,51490,51491,51492,51493,51494,51495,51496,51497,51498,51499,51500,51501,51502,51503,51504,51505,51506,51507,51508,51509,51510,51511,51512,51513,51514,51515,51516,51517,51518,51519,51520,51521,51522,51523,51524,51525,51526,51527,51528,51529,51530,51531,51532,51533,51534,51535,51536,51537,51538,51539,51540,51541,51542,51543,51544,51545,51546,51547,51548,51549,51550,51551,51552,51553,51554,51555,51556,51557,51558,51559,51560,51561,51562,51563,51564,51565,51566,51567,51568,51569,51570,51571,51572,51573,51574,51575,51576,51577,51578,51579,51580,51581,51582,51583,51584,51585,51586,51587,51588,51589,51590,51591,51592,51593,51594,51595,51596,51597,51598,51599,51600,51601,51602,51603,51604,51605,51606,51607,51608,51609,51610,51611,51612,51613,51614,51615,51616,51617,51618,51619,51620,51621,51622,51623,51624,51625,51626,51627,51628,51629,51630,51631,51632,51633,51634,51635,51636,51637,51638,51639,51640,51641,51642,51643,51644,51645,51646,51647,51648,51649,51650,51651,51652,51653,51654,51655,51656,51657,51658,51659,51660,51661,51662,51663,51664,51665,51666,51667,51668,51669,51670,51671,51672,51673,51674,51675,51676,51677,51678,51679,51680,51681,51682,51683,51684,51685,51686,51687,51688,51689,51690,51691,51692,51693,51694,51695,51696,51697,51698,51699,51700,51701,51702,51703,51704,51705,51706,51707,51708,51709,51710,51711,51712,51713,51714,51715,51716,51717,51718,51719,51720,51721,51722,51723,51724,51725,51726,51727,51728,51729,51730,51731,51732,51733,51734,51735,51736,51737,51738,51739,51740,51741,51742,51743,51744,51745,51746,51747,51748,51749,51750,51751,51752,51753,51754,51755,51756,51757,51758,51759,51760,51761,51762,51763,51764,51765,51766,51767,51768,51769,51770,51771,51772,51773,51774,51775,51776,51777,51778,51779,51780,51781,51782,51783,51784,51785,51786,51787,51788,51789,51790,51791,51792,51793,51794,51795,51796,51797,51798,51799,51800,51801,51802,51803,51804,51805,51806,51807,51808,51809,51810,51811,51812,51813,51814,51815,51816,51817,51818,51819,51820,51821,51822,51823,51824,51825,51826,51827,51828,51829,51830,51831,51832,51833,51834,51835,51836,51837,51838,51839,51840,51841,51842,51843,51844,51845,51846,51847,51848,51849,51850,51851,51852,51853,51854,51855,51856,51857,51858,51859,51860,51861,51862,51863,51864,51865,51866,51867,51868,51869,51870,51871,51872,51873,51874,51875,51876,51877,51878,51879,51880,51881,51882,51883,51884,51885,51886,51887,51888,51889,51890,51891,51892,51893,51894,51895,51896,51897,51898,51899,51900,51901,51902,51903,51904,51905,51906,51907,51908,51909,51910,51911,51912,51913,51914,51915,51916,51917,51918,51919,51920,51921,51922,51923,51924,51925,51926,51927,51928,51929,51930,51931,51932,51933,51934,51935,51936,51937,51938,51939,51940,51941,51942,51943,51944,51945,51946,51947,51948,51949,51950,51951,51952,51953,51954,51955,51956,51957,51958,51959,51960,51961,51962,51963,51964,51965,51966,51967,51968,51969,51970,51971,51972,51973,51974,51975,51976,51977,51978,51979,51980,51981,51982,51983,51984,51985,51986,51987,51988,51989,51990,51991,51992,51993,51994,51995,51996,51997,51998,51999,52000,52001,52002,52003,52004,52005,52006,52007,52008,52009,52010,52011,52012,52013,52014,52015,52016,52017,52018,52019,52020,52021,52022,52023,52024,52025,52026,52027,52028,52029,52030,52031,52032,52033,52034,52035,52036,52037,52038,52039,52040,52041,52042,52043,52044,52045,52046,52047,52048,52049,52050,52051,52052,52053,52054,52055,52056,52057,52058,52059,52060,52061,52062,52063,52064,52065,52066,52067,52068,52069,52070,52071,52072,52073,52074,52075,52076,52077,52078,52079,52080,52081,52082,52083,52084,52085,52086,52087,52088,52089,52090,52091,52092,52093,52094,52095,52096,52097,52098,52099,52100,52101,52102,52103,52104,52105,52106,52107,52108,52109,52110,52111,52112,52113,52114,52115,52116,52117,52118,52119,52120,52121,52122,52123,52124,52125,52126,52127,52128,52129,52130,52131,52132,52133,52134,52135,52136,52137,52138,52139,52140,52141,52142,52143,52144,52145,52146,52147,52148,52149,52150,52151,52152,52153,52154,52155,52156,52157,52158,52159,52160,52161,52162,52163,52164,52165,52166,52167,52168,52169,52170,52171,52172,52173,52174,52175,52176,52177,52178,52179,52180,52181,52182,52183,52184,52185,52186,52187,52188,52189,52190,52191,52192,52193,52194,52195,52196,52197,52198,52199,52200,52201,52202,52203,52204,52205,52206,52207,52208,52209,52210,52211,52212,52213,52214,52215,52216,52217,52218,52219,52220,52221,52222,52223,52224,52225,52226,52227,52228,52229,52230,52231,52232,52233,52234,52235,52236,52237,52238,52239,52240,52241,52242,52243,52244,52245,52246,52247,52248,52249,52250,52251,52252,52253,52254,52255,52256,52257,52258,52259,52260,52261,52262,52263,52264,52265,52266,52267,52268,52269,52270,52271,52272,52273,52274,52275,52276,52277,52278,52279,52280,52281,52282,52283,52284,52285,52286,52287,52288,52289,52290,52291,52292,52293,52294,52295,52296,52297,52298,52299,52300,52301,52302,52303,52304,52305,52306,52307,52308,52309,52310,52311,52312,52313,52314,52315,52316,52317,52318,52319,52320,52321,52322,52323,52324,52325,52326,52327,52328,52329,52330,52331,52332,52333,52334,52335,52336,52337,52338,52339,52340,52341,52342,52343,52344,52345,52346,52347,52348,52349,52350,52351,52352,52353,52354,52355,52356,52357,52358,52359,52360,52361,52362,52363,52364,52365,52366,52367,52368,52369,52370,52371,52372,52373,52374,52375,52376,52377,52378,52379,52380,52381,52382,52383,52384,52385,52386,52387,52388,52389,52390,52391,52392,52393,52394,52395,52396,52397,52398,52399,52400,52401,52402,52403,52404,52405,52406,52407,52408,52409,52410,52411,52412,52413,52414,52415,52416,52417,52418,52419,52420,52421,52422,52423,52424,52425,52426,52427,52428,52429,52430,52431,52432,52433,52434,52435,52436,52437,52438,52439,52440,52441,52442,52443,52444,52445,52446,52447,52448,52449,52450,52451,52452,52453,52454,52455,52456,52457,52458,52459,52460,52461,52462,52463,52464,52465,52466,52467,52468,52469,52470,52471,52472,52473,52474,52475,52476,52477,52478,52479,52480,52481,52482,52483,52484,52485,52486,52487,52488,52489,52490,52491,52492,52493,52494,52495,52496,52497,52498,52499,52500,52501,52502,52503,52504,52505,52506,52507,52508,52509,52510,52511,52512,52513,52514,52515,52516,52517,52518,52519,52520,52521,52522,52523,52524,52525,52526,52527,52528,52529,52530,52531,52532,52533,52534,52535,52536,52537,52538,52539,52540,52541,52542,52543,52544,52545,52546,52547,52548,52549,52550,52551,52552,52553,52554,52555,52556,52557,52558,52559,52560,52561,52562,52563,52564,52565,52566,52567,52568,52569,52570,52571,52572,52573,52574,52575,52576,52577,52578,52579,52580,52581,52582,52583,52584,52585,52586,52587,52588,52589,52590,52591,52592,52593,52594,52595,52596,52597,52598,52599,52600,52601,52602,52603,52604,52605,52606,52607,52608,52609,52610,52611,52612,52613,52614,52615,52616,52617,52618,52619,52620,52621,52622,52623,52624,52625,52626,52627,52628,52629,52630,52631,52632,52633,52634,52635,52636,52637,52638,52639,52640,52641,52642,52643,52644,52645,52646,52647,52648,52649,52650,52651,52652,52653,52654,52655,52656,52657,52658,52659,52660,52661,52662,52663,52664,52665,52666,52667,52668,52669,52670,52671,52672,52673,52674,52675,52676,52677,52678,52679,52680,52681,52682,52683,52684,52685,52686,52687,52688,52689,52690,52691,52692,52693,52694,52695,52696,52697,52698,52699,52700,52701,52702,52703,52704,52705,52706,52707,52708,52709,52710,52711,52712,52713,52714,52715,52716,52717,52718,52719,52720,52721,52722,52723,52724,52725,52726,52727,52728,52729,52730,52731,52732,52733,52734,52735,52736,52737,52738,52739,52740,52741,52742,52743,52744,52745,52746,52747,52748,52749,52750,52751,52752,52753,52754,52755,52756,52757,52758,52759,52760,52761,52762,52763,52764,52765,52766,52767,52768,52769,52770,52771,52772,52773,52774,52775,52776,52777,52778,52779,52780,52781,52782,52783,52784,52785,52786,52787,52788,52789,52790,52791,52792,52793,52794,52795,52796,52797,52798,52799,52800,52801,52802,52803,52804,52805,52806,52807,52808,52809,52810,52811,52812,52813,52814,52815,52816,52817,52818,52819,52820,52821,52822,52823,52824,52825,52826,52827,52828,52829,52830,52831,52832,52833,52834,52835,52836,52837,52838,52839,52840,52841,52842,52843,52844,52845,52846,52847,52848,52849,52850,52851,52852,52853,52854,52855,52856,52857,52858,52859,52860,52861,52862,52863,52864,52865,52866,52867,52868,52869,52870,52871,52872,52873,52874,52875,52876,52877,52878,52879,52880,52881,52882,52883,52884,52885,52886,52887,52888,52889,52890,52891,52892,52893,52894,52895,52896,52897,52898,52899,52900,52901,52902,52903,52904,52905,52906,52907,52908,52909,52910,52911,52912,52913,52914,52915,52916,52917,52918,52919,52920,52921,52922,52923,52924,52925,52926,52927,52928,52929,52930,52931,52932,52933,52934,52935,52936,52937,52938,52939,52940,52941,52942,52943,52944,52945,52946,52947,52948,52949,52950,52951,52952,52953,52954,52955,52956,52957,52958,52959,52960,52961,52962,52963,52964,52965,52966,52967,52968,52969,52970,52971,52972,52973,52974,52975,52976,52977,52978,52979,52980,52981,52982,52983,52984,52985,52986,52987,52988,52989,52990,52991,52992,52993,52994,52995,52996,52997,52998,52999,53000,53001,53002,53003,53004,53005,53006,53007,53008,53009,53010,53011,53012,53013,53014,53015,53016,53017,53018,53019,53020,53021,53022,53023,53024,53025,53026,53027,53028,53029,53030,53031,53032,53033,53034,53035,53036,53037,53038,53039,53040,53041,53042,53043,53044,53045,53046,53047,53048,53049,53050,53051,53052,53053,53054,53055,53056,53057,53058,53059,53060,53061,53062,53063,53064,53065,53066,53067,53068,53069,53070,53071,53072,53073,53074,53075,53076,53077,53078,53079,53080,53081,53082,53083,53084,53085,53086,53087,53088,53089,53090,53091,53092,53093,53094,53095,53096,53097,53098,53099,53100,53101,53102,53103,53104,53105,53106,53107,53108,53109,53110,53111,53112,53113,53114,53115,53116,53117,53118,53119,53120,53121,53122,53123,53124,53125,53126,53127,53128,53129,53130,53131,53132,53133,53134,53135,53136,53137,53138,53139,53140,53141,53142,53143,53144,53145,53146,53147,53148,53149,53150,53151,53152,53153,53154,53155,53156,53157,53158,53159,53160,53161,53162,53163,53164,53165,53166,53167,53168,53169,53170,53171,53172,53173,53174,53175,53176,53177,53178,53179,53180,53181,53182,53183,53184,53185,53186,53187,53188,53189,53190,53191,53192,53193,53194,53195,53196,53197,53198,53199,53200,53201,53202,53203,53204,53205,53206,53207,53208,53209,53210,53211,53212,53213,53214,53215,53216,53217,53218,53219,53220,53221,53222,53223,53224,53225,53226,53227,53228,53229,53230,53231,53232,53233,53234,53235,53236,53237,53238,53239,53240,53241,53242,53243,53244,53245,53246,53247,53248,53249,53250,53251,53252,53253,53254,53255,53256,53257,53258,53259,53260,53261,53262,53263,53264,53265,53266,53267,53268,53269,53270,53271,53272,53273,53274,53275,53276,53277,53278,53279,53280,53281,53282,53283,53284,53285,53286,53287,53288,53289,53290,53291,53292,53293,53294,53295,53296,53297,53298,53299,53300,53301,53302,53303,53304,53305,53306,53307,53308,53309,53310,53311,53312,53313,53314,53315,53316,53317,53318,53319,53320,53321,53322,53323,53324,53325,53326,53327,53328,53329,53330,53331,53332,53333,53334,53335,53336,53337,53338,53339,53340,53341,53342,53343,53344,53345,53346,53347,53348,53349,53350,53351,53352,53353,53354,53355,53356,53357,53358,53359,53360,53361,53362,53363,53364,53365,53366,53367,53368,53369,53370,53371,53372,53373,53374,53375,53376,53377,53378,53379,53380,53381,53382,53383,53384,53385,53386,53387,53388,53389,53390,53391,53392,53393,53394,53395,53396,53397,53398,53399,53400,53401,53402,53403,53404,53405,53406,53407,53408,53409,53410,53411,53412,53413,53414,53415,53416,53417,53418,53419,53420,53421,53422,53423,53424,53425,53426,53427,53428,53429,53430,53431,53432,53433,53434,53435,53436,53437,53438,53439,53440,53441,53442,53443,53444,53445,53446,53447,53448,53449,53450,53451,53452,53453,53454,53455,53456,53457,53458,53459,53460,53461,53462,53463,53464,53465,53466,53467,53468,53469,53470,53471,53472,53473,53474,53475,53476,53477,53478,53479,53480,53481,53482,53483,53484,53485,53486,53487,53488,53489,53490,53491,53492,53493,53494,53495,53496,53497,53498,53499,53500,53501,53502,53503,53504,53505,53506,53507,53508,53509,53510,53511,53512,53513,53514,53515,53516,53517,53518,53519,53520,53521,53522,53523,53524,53525,53526,53527,53528,53529,53530,53531,53532,53533,53534,53535,53536,53537,53538,53539,53540,53541,53542,53543,53544,53545,53546,53547,53548,53549,53550,53551,53552,53553,53554,53555,53556,53557,53558,53559,53560,53561,53562,53563,53564,53565,53566,53567,53568,53569,53570,53571,53572,53573,53574,53575,53576,53577,53578,53579,53580,53581,53582,53583,53584,53585,53586,53587,53588,53589,53590,53591,53592,53593,53594,53595,53596,53597,53598,53599,53600,53601,53602,53603,53604,53605,53606,53607,53608,53609,53610,53611,53612,53613,53614,53615,53616,53617,53618,53619,53620,53621,53622,53623,53624,53625,53626,53627,53628,53629,53630,53631,53632,53633,53634,53635,53636,53637,53638,53639,53640,53641,53642,53643,53644,53645,53646,53647,53648,53649,53650,53651,53652,53653,53654,53655,53656,53657,53658,53659,53660,53661,53662,53663,53664,53665,53666,53667,53668,53669,53670,53671,53672,53673,53674,53675,53676,53677,53678,53679,53680,53681,53682,53683,53684,53685,53686,53687,53688,53689,53690,53691,53692,53693,53694,53695,53696,53697,53698,53699,53700,53701,53702,53703,53704,53705,53706,53707,53708,53709,53710,53711,53712,53713,53714,53715,53716,53717,53718,53719,53720,53721,53722,53723,53724,53725,53726,53727,53728,53729,53730,53731,53732,53733,53734,53735,53736,53737,53738,53739,53740,53741,53742,53743,53744,53745,53746,53747,53748,53749,53750,53751,53752,53753,53754,53755,53756,53757,53758,53759,53760,53761,53762,53763,53764,53765,53766,53767,53768,53769,53770,53771,53772,53773,53774,53775,53776,53777,53778,53779,53780,53781,53782,53783,53784,53785,53786,53787,53788,53789,53790,53791,53792,53793,53794,53795,53796,53797,53798,53799,53800,53801,53802,53803,53804,53805,53806,53807,53808,53809,53810,53811,53812,53813,53814,53815,53816,53817,53818,53819,53820,53821,53822,53823,53824,53825,53826,53827,53828,53829,53830,53831,53832,53833,53834,53835,53836,53837,53838,53839,53840,53841,53842,53843,53844,53845,53846,53847,53848,53849,53850,53851,53852,53853,53854,53855,53856,53857,53858,53859,53860,53861,53862,53863,53864,53865,53866,53867,53868,53869,53870,53871,53872,53873,53874,53875,53876,53877,53878,53879,53880,53881,53882,53883,53884,53885,53886,53887,53888,53889,53890,53891,53892,53893,53894,53895,53896,53897,53898,53899,53900,53901,53902,53903,53904,53905,53906,53907,53908,53909,53910,53911,53912,53913,53914,53915,53916,53917,53918,53919,53920,53921,53922,53923,53924,53925,53926,53927,53928,53929,53930,53931,53932,53933,53934,53935,53936,53937,53938,53939,53940,53941,53942,53943,53944,53945,53946,53947,53948,53949,53950,53951,53952,53953,53954,53955,53956,53957,53958,53959,53960,53961,53962,53963,53964,53965,53966,53967,53968,53969,53970,53971,53972,53973,53974,53975,53976,53977,53978,53979,53980,53981,53982,53983,53984,53985,53986,53987,53988,53989,53990,53991,53992,53993,53994,53995,53996,53997,53998,53999,54000,54001,54002,54003,54004,54005,54006,54007,54008,54009,54010,54011,54012,54013,54014,54015,54016,54017,54018,54019,54020,54021,54022,54023,54024,54025,54026,54027,54028,54029,54030,54031,54032,54033,54034,54035,54036,54037,54038,54039,54040,54041,54042,54043,54044,54045,54046,54047,54048,54049,54050,54051,54052,54053,54054,54055,54056,54057,54058,54059,54060,54061,54062,54063,54064,54065,54066,54067,54068,54069,54070,54071,54072,54073,54074,54075,54076,54077,54078,54079,54080,54081,54082,54083,54084,54085,54086,54087,54088,54089,54090,54091,54092,54093,54094,54095,54096,54097,54098,54099,54100,54101,54102,54103,54104,54105,54106,54107,54108,54109,54110,54111,54112,54113,54114,54115,54116,54117,54118,54119,54120,54121,54122,54123,54124,54125,54126,54127,54128,54129,54130,54131,54132,54133,54134,54135,54136,54137,54138,54139,54140,54141,54142,54143,54144,54145,54146,54147,54148,54149,54150,54151,54152,54153,54154,54155,54156,54157,54158,54159,54160,54161,54162,54163,54164,54165,54166,54167,54168,54169,54170,54171,54172,54173,54174,54175,54176,54177,54178,54179,54180,54181,54182,54183,54184,54185,54186,54187,54188,54189,54190,54191,54192,54193,54194,54195,54196,54197,54198,54199,54200,54201,54202,54203,54204,54205,54206,54207,54208,54209,54210,54211,54212,54213,54214,54215,54216,54217,54218,54219,54220,54221,54222,54223,54224,54225,54226,54227,54228,54229,54230,54231,54232,54233,54234,54235,54236,54237,54238,54239,54240,54241,54242,54243,54244,54245,54246,54247,54248,54249,54250,54251,54252,54253,54254,54255,54256,54257,54258,54259,54260,54261,54262,54263,54264,54265,54266,54267,54268,54269,54270,54271,54272,54273,54274,54275,54276,54277,54278,54279,54280,54281,54282,54283,54284,54285,54286,54287,54288,54289,54290,54291,54292,54293,54294,54295,54296,54297,54298,54299,54300,54301,54302,54303,54304,54305,54306,54307,54308,54309,54310,54311,54312,54313,54314,54315,54316,54317,54318,54319,54320,54321,54322,54323,54324,54325,54326,54327,54328,54329,54330,54331,54332,54333,54334,54335,54336,54337,54338,54339,54340,54341,54342,54343,54344,54345,54346,54347,54348,54349,54350,54351,54352,54353,54354,54355,54356,54357,54358,54359,54360,54361,54362,54363,54364,54365,54366,54367,54368,54369,54370,54371,54372,54373,54374,54375,54376,54377,54378,54379,54380,54381,54382,54383,54384,54385,54386,54387,54388,54389,54390,54391,54392,54393,54394,54395,54396,54397,54398,54399,54400,54401,54402,54403,54404,54405,54406,54407,54408,54409,54410,54411,54412,54413,54414,54415,54416,54417,54418,54419,54420,54421,54422,54423,54424,54425,54426,54427,54428,54429,54430,54431,54432,54433,54434,54435,54436,54437,54438,54439,54440,54441,54442,54443,54444,54445,54446,54447,54448,54449,54450,54451,54452,54453,54454,54455,54456,54457,54458,54459,54460,54461,54462,54463,54464,54465,54466,54467,54468,54469,54470,54471,54472,54473,54474,54475,54476,54477,54478,54479,54480,54481,54482,54483,54484,54485,54486,54487,54488,54489,54490,54491,54492,54493,54494,54495,54496,54497,54498,54499,54500,54501,54502,54503,54504,54505,54506,54507,54508,54509,54510,54511,54512,54513,54514,54515,54516,54517,54518,54519,54520,54521,54522,54523,54524,54525,54526,54527,54528,54529,54530,54531,54532,54533,54534,54535,54536,54537,54538,54539,54540,54541,54542,54543,54544,54545,54546,54547,54548,54549,54550,54551,54552,54553,54554,54555,54556,54557,54558,54559,54560,54561,54562,54563,54564,54565,54566,54567,54568,54569,54570,54571,54572,54573,54574,54575,54576,54577,54578,54579,54580,54581,54582,54583,54584,54585,54586,54587,54588,54589,54590,54591,54592,54593,54594,54595,54596,54597,54598,54599,54600,54601,54602,54603,54604,54605,54606,54607,54608,54609,54610,54611,54612,54613,54614,54615,54616,54617,54618,54619,54620,54621,54622,54623,54624,54625,54626,54627,54628,54629,54630,54631,54632,54633,54634,54635,54636,54637,54638,54639,54640,54641,54642,54643,54644,54645,54646,54647,54648,54649,54650,54651,54652,54653,54654,54655,54656,54657,54658,54659,54660,54661,54662,54663,54664,54665,54666,54667,54668,54669,54670,54671,54672,54673,54674,54675,54676,54677,54678,54679,54680,54681,54682,54683,54684,54685,54686,54687,54688,54689,54690,54691,54692,54693,54694,54695,54696,54697,54698,54699,54700,54701,54702,54703,54704,54705,54706,54707,54708,54709,54710,54711,54712,54713,54714,54715,54716,54717,54718,54719,54720,54721,54722,54723,54724,54725,54726,54727,54728,54729,54730,54731,54732,54733,54734,54735,54736,54737,54738,54739,54740,54741,54742,54743,54744,54745,54746,54747,54748,54749,54750,54751,54752,54753,54754,54755,54756,54757,54758,54759,54760,54761,54762,54763,54764,54765,54766,54767,54768,54769,54770,54771,54772,54773,54774,54775,54776,54777,54778,54779,54780,54781,54782,54783,54784,54785,54786,54787,54788,54789,54790,54791,54792,54793,54794,54795,54796,54797,54798,54799,54800,54801,54802,54803,54804,54805,54806,54807,54808,54809,54810,54811,54812,54813,54814,54815,54816,54817,54818,54819,54820,54821,54822,54823,54824,54825,54826,54827,54828,54829,54830,54831,54832,54833,54834,54835,54836,54837,54838,54839,54840,54841,54842,54843,54844,54845,54846,54847,54848,54849,54850,54851,54852,54853,54854,54855,54856,54857,54858,54859,54860,54861,54862,54863,54864,54865,54866,54867,54868,54869,54870,54871,54872,54873,54874,54875,54876,54877,54878,54879,54880,54881,54882,54883,54884,54885,54886,54887,54888,54889,54890,54891,54892,54893,54894,54895,54896,54897,54898,54899,54900,54901,54902,54903,54904,54905,54906,54907,54908,54909,54910,54911,54912,54913,54914,54915,54916,54917,54918,54919,54920,54921,54922,54923,54924,54925,54926,54927,54928,54929,54930,54931,54932,54933,54934,54935,54936,54937,54938,54939,54940,54941,54942,54943,54944,54945,54946,54947,54948,54949,54950,54951,54952,54953,54954,54955,54956,54957,54958,54959,54960,54961,54962,54963,54964,54965,54966,54967,54968,54969,54970,54971,54972,54973,54974,54975,54976,54977,54978,54979,54980,54981,54982,54983,54984,54985,54986,54987,54988,54989,54990,54991,54992,54993,54994,54995,54996,54997,54998,54999,55000,55001,55002,55003,55004,55005,55006,55007,55008,55009,55010,55011,55012,55013,55014,55015,55016,55017,55018,55019,55020,55021,55022,55023,55024,55025,55026,55027,55028,55029,55030,55031,55032,55033,55034,55035,55036,55037,55038,55039,55040,55041,55042,55043,55044,55045,55046,55047,55048,55049,55050,55051,55052,55053,55054,55055,55056,55057,55058,55059,55060,55061,55062,55063,55064,55065,55066,55067,55068,55069,55070,55071,55072,55073,55074,55075,55076,55077,55078,55079,55080,55081,55082,55083,55084,55085,55086,55087,55088,55089,55090,55091,55092,55093,55094,55095,55096,55097,55098,55099,55100,55101,55102,55103,55104,55105,55106,55107,55108,55109,55110,55111,55112,55113,55114,55115,55116,55117,55118,55119,55120,55121,55122,55123,55124,55125,55126,55127,55128,55129,55130,55131,55132,55133,55134,55135,55136,55137,55138,55139,55140,55141,55142,55143,55144,55145,55146,55147,55148,55149,55150,55151,55152,55153,55154,55155,55156,55157,55158,55159,55160,55161,55162,55163,55164,55165,55166,55167,55168,55169,55170,55171,55172,55173,55174,55175,55176,55177,55178,55179,55180,55181,55182,55183,55184,55185,55186,55187,55188,55189,55190,55191,55192,55193,55194,55195,55196,55197,55198,55199,55200,55201,55202,55203,55216,55217,55218,55219,55220,55221,55222,55223,55224,55225,55226,55227,55228,55229,55230,55231,55232,55233,55234,55235,55236,55237,55238,55243,55244,55245,55246,55247,55248,55249,55250,55251,55252,55253,55254,55255,55256,55257,55258,55259,55260,55261,55262,55263,55264,55265,55266,55267,55268,55269,55270,55271,55272,55273,55274,55275,55276,55277,55278,55279,55280,55281,55282,55283,55284,55285,55286,55287,55288,55289,55290,55291,63744,63745,63746,63747,63748,63749,63750,63751,63752,63753,63754,63755,63756,63757,63758,63759,63760,63761,63762,63763,63764,63765,63766,63767,63768,63769,63770,63771,63772,63773,63774,63775,63776,63777,63778,63779,63780,63781,63782,63783,63784,63785,63786,63787,63788,63789,63790,63791,63792,63793,63794,63795,63796,63797,63798,63799,63800,63801,63802,63803,63804,63805,63806,63807,63808,63809,63810,63811,63812,63813,63814,63815,63816,63817,63818,63819,63820,63821,63822,63823,63824,63825,63826,63827,63828,63829,63830,63831,63832,63833,63834,63835,63836,63837,63838,63839,63840,63841,63842,63843,63844,63845,63846,63847,63848,63849,63850,63851,63852,63853,63854,63855,63856,63857,63858,63859,63860,63861,63862,63863,63864,63865,63866,63867,63868,63869,63870,63871,63872,63873,63874,63875,63876,63877,63878,63879,63880,63881,63882,63883,63884,63885,63886,63887,63888,63889,63890,63891,63892,63893,63894,63895,63896,63897,63898,63899,63900,63901,63902,63903,63904,63905,63906,63907,63908,63909,63910,63911,63912,63913,63914,63915,63916,63917,63918,63919,63920,63921,63922,63923,63924,63925,63926,63927,63928,63929,63930,63931,63932,63933,63934,63935,63936,63937,63938,63939,63940,63941,63942,63943,63944,63945,63946,63947,63948,63949,63950,63951,63952,63953,63954,63955,63956,63957,63958,63959,63960,63961,63962,63963,63964,63965,63966,63967,63968,63969,63970,63971,63972,63973,63974,63975,63976,63977,63978,63979,63980,63981,63982,63983,63984,63985,63986,63987,63988,63989,63990,63991,63992,63993,63994,63995,63996,63997,63998,63999,64000,64001,64002,64003,64004,64005,64006,64007,64008,64009,64010,64011,64012,64013,64014,64015,64016,64017,64018,64019,64020,64021,64022,64023,64024,64025,64026,64027,64028,64029,64030,64031,64032,64033,64034,64035,64036,64037,64038,64039,64040,64041,64042,64043,64044,64045,64046,64047,64048,64049,64050,64051,64052,64053,64054,64055,64056,64057,64058,64059,64060,64061,64062,64063,64064,64065,64066,64067,64068,64069,64070,64071,64072,64073,64074,64075,64076,64077,64078,64079,64080,64081,64082,64083,64084,64085,64086,64087,64088,64089,64090,64091,64092,64093,64094,64095,64096,64097,64098,64099,64100,64101,64102,64103,64104,64105,64106,64107,64108,64109,64112,64113,64114,64115,64116,64117,64118,64119,64120,64121,64122,64123,64124,64125,64126,64127,64128,64129,64130,64131,64132,64133,64134,64135,64136,64137,64138,64139,64140,64141,64142,64143,64144,64145,64146,64147,64148,64149,64150,64151,64152,64153,64154,64155,64156,64157,64158,64159,64160,64161,64162,64163,64164,64165,64166,64167,64168,64169,64170,64171,64172,64173,64174,64175,64176,64177,64178,64179,64180,64181,64182,64183,64184,64185,64186,64187,64188,64189,64190,64191,64192,64193,64194,64195,64196,64197,64198,64199,64200,64201,64202,64203,64204,64205,64206,64207,64208,64209,64210,64211,64212,64213,64214,64215,64216,64217,64256,64257,64258,64259,64260,64261,64262,64275,64276,64277,64278,64279,64285,64287,64288,64289,64290,64291,64292,64293,64294,64295,64296,64298,64299,64300,64301,64302,64303,64304,64305,64306,64307,64308,64309,64310,64312,64313,64314,64315,64316,64318,64320,64321,64323,64324,64326,64327,64328,64329,64330,64331,64332,64333,64334,64335,64336,64337,64338,64339,64340,64341,64342,64343,64344,64345,64346,64347,64348,64349,64350,64351,64352,64353,64354,64355,64356,64357,64358,64359,64360,64361,64362,64363,64364,64365,64366,64367,64368,64369,64370,64371,64372,64373,64374,64375,64376,64377,64378,64379,64380,64381,64382,64383,64384,64385,64386,64387,64388,64389,64390,64391,64392,64393,64394,64395,64396,64397,64398,64399,64400,64401,64402,64403,64404,64405,64406,64407,64408,64409,64410,64411,64412,64413,64414,64415,64416,64417,64418,64419,64420,64421,64422,64423,64424,64425,64426,64427,64428,64429,64430,64431,64432,64433,64467,64468,64469,64470,64471,64472,64473,64474,64475,64476,64477,64478,64479,64480,64481,64482,64483,64484,64485,64486,64487,64488,64489,64490,64491,64492,64493,64494,64495,64496,64497,64498,64499,64500,64501,64502,64503,64504,64505,64506,64507,64508,64509,64510,64511,64512,64513,64514,64515,64516,64517,64518,64519,64520,64521,64522,64523,64524,64525,64526,64527,64528,64529,64530,64531,64532,64533,64534,64535,64536,64537,64538,64539,64540,64541,64542,64543,64544,64545,64546,64547,64548,64549,64550,64551,64552,64553,64554,64555,64556,64557,64558,64559,64560,64561,64562,64563,64564,64565,64566,64567,64568,64569,64570,64571,64572,64573,64574,64575,64576,64577,64578,64579,64580,64581,64582,64583,64584,64585,64586,64587,64588,64589,64590,64591,64592,64593,64594,64595,64596,64597,64598,64599,64600,64601,64602,64603,64604,64605,64606,64607,64608,64609,64610,64611,64612,64613,64614,64615,64616,64617,64618,64619,64620,64621,64622,64623,64624,64625,64626,64627,64628,64629,64630,64631,64632,64633,64634,64635,64636,64637,64638,64639,64640,64641,64642,64643,64644,64645,64646,64647,64648,64649,64650,64651,64652,64653,64654,64655,64656,64657,64658,64659,64660,64661,64662,64663,64664,64665,64666,64667,64668,64669,64670,64671,64672,64673,64674,64675,64676,64677,64678,64679,64680,64681,64682,64683,64684,64685,64686,64687,64688,64689,64690,64691,64692,64693,64694,64695,64696,64697,64698,64699,64700,64701,64702,64703,64704,64705,64706,64707,64708,64709,64710,64711,64712,64713,64714,64715,64716,64717,64718,64719,64720,64721,64722,64723,64724,64725,64726,64727,64728,64729,64730,64731,64732,64733,64734,64735,64736,64737,64738,64739,64740,64741,64742,64743,64744,64745,64746,64747,64748,64749,64750,64751,64752,64753,64754,64755,64756,64757,64758,64759,64760,64761,64762,64763,64764,64765,64766,64767,64768,64769,64770,64771,64772,64773,64774,64775,64776,64777,64778,64779,64780,64781,64782,64783,64784,64785,64786,64787,64788,64789,64790,64791,64792,64793,64794,64795,64796,64797,64798,64799,64800,64801,64802,64803,64804,64805,64806,64807,64808,64809,64810,64811,64812,64813,64814,64815,64816,64817,64818,64819,64820,64821,64822,64823,64824,64825,64826,64827,64828,64829,64848,64849,64850,64851,64852,64853,64854,64855,64856,64857,64858,64859,64860,64861,64862,64863,64864,64865,64866,64867,64868,64869,64870,64871,64872,64873,64874,64875,64876,64877,64878,64879,64880,64881,64882,64883,64884,64885,64886,64887,64888,64889,64890,64891,64892,64893,64894,64895,64896,64897,64898,64899,64900,64901,64902,64903,64904,64905,64906,64907,64908,64909,64910,64911,64914,64915,64916,64917,64918,64919,64920,64921,64922,64923,64924,64925,64926,64927,64928,64929,64930,64931,64932,64933,64934,64935,64936,64937,64938,64939,64940,64941,64942,64943,64944,64945,64946,64947,64948,64949,64950,64951,64952,64953,64954,64955,64956,64957,64958,64959,64960,64961,64962,64963,64964,64965,64966,64967,65008,65009,65010,65011,65012,65013,65014,65015,65016,65017,65018,65019,65136,65137,65138,65139,65140,65142,65143,65144,65145,65146,65147,65148,65149,65150,65151,65152,65153,65154,65155,65156,65157,65158,65159,65160,65161,65162,65163,65164,65165,65166,65167,65168,65169,65170,65171,65172,65173,65174,65175,65176,65177,65178,65179,65180,65181,65182,65183,65184,65185,65186,65187,65188,65189,65190,65191,65192,65193,65194,65195,65196,65197,65198,65199,65200,65201,65202,65203,65204,65205,65206,65207,65208,65209,65210,65211,65212,65213,65214,65215,65216,65217,65218,65219,65220,65221,65222,65223,65224,65225,65226,65227,65228,65229,65230,65231,65232,65233,65234,65235,65236,65237,65238,65239,65240,65241,65242,65243,65244,65245,65246,65247,65248,65249,65250,65251,65252,65253,65254,65255,65256,65257,65258,65259,65260,65261,65262,65263,65264,65265,65266,65267,65268,65269,65270,65271,65272,65273,65274,65275,65276,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,65382,65383,65384,65385,65386,65387,65388,65389,65390,65391,65392,65393,65394,65395,65396,65397,65398,65399,65400,65401,65402,65403,65404,65405,65406,65407,65408,65409,65410,65411,65412,65413,65414,65415,65416,65417,65418,65419,65420,65421,65422,65423,65424,65425,65426,65427,65428,65429,65430,65431,65432,65433,65434,65435,65436,65437,65438,65439,65440,65441,65442,65443,65444,65445,65446,65447,65448,65449,65450,65451,65452,65453,65454,65455,65456,65457,65458,65459,65460,65461,65462,65463,65464,65465,65466,65467,65468,65469,65470,65474,65475,65476,65477,65478,65479,65482,65483,65484,65485,65486,65487,65490,65491,65492,65493,65494,65495,65498,65499,65500'; +var arr = str.split(',').map(function(code) { + return parseInt(code, 10); +}); +module.exports = arr; +},{}],"/../../jshint/node_modules/console-browserify/index.js":[function(_dereq_,module,exports){ +(function (global){ +var util = _dereq_("util") +var assert = _dereq_("assert") +var now = _dereq_("date-now") + +var slice = Array.prototype.slice +var console +var times = {} + +if (typeof global !== "undefined" && global.console) { + console = global.console +} else if (typeof window !== "undefined" && window.console) { + console = window.console +} else { + console = {} +} + +var functions = [ + [log, "log"], + [info, "info"], + [warn, "warn"], + [error, "error"], + [time, "time"], + [timeEnd, "timeEnd"], + [trace, "trace"], + [dir, "dir"], + [consoleAssert, "assert"] +] + +for (var i = 0; i < functions.length; i++) { + var tuple = functions[i] + var f = tuple[0] + var name = tuple[1] + + if (!console[name]) { + console[name] = f + } +} + +module.exports = console + +function log() {} + +function info() { + console.log.apply(console, arguments) +} + +function warn() { + console.log.apply(console, arguments) +} + +function error() { + console.warn.apply(console, arguments) +} + +function time(label) { + times[label] = now() +} + +function timeEnd(label) { + var time = times[label] + if (!time) { + throw new Error("No such label: " + label) + } + + var duration = now() - time + console.log(label + ": " + duration + "ms") +} + +function trace() { + var err = new Error() + err.name = "Trace" + err.message = util.format.apply(null, arguments) + console.error(err.stack) +} + +function dir(object) { + console.log(util.inspect(object) + "\n") +} + +function consoleAssert(expression) { + if (!expression) { + var arr = slice.call(arguments, 1) + assert.ok(false, util.format.apply(null, arr)) + } +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"assert":"/../node_modules/assert/assert.js","date-now":"/../../jshint/node_modules/date-now/index.js","util":"/../node_modules/util/util.js"}],"/../../jshint/node_modules/date-now/index.js":[function(_dereq_,module,exports){ +module.exports = now + +function now() { + return new Date().getTime() +} + +},{}],"/../../jshint/node_modules/lodash.clone/index.js":[function(_dereq_,module,exports){ +(function (global){ +var LARGE_ARRAY_SIZE = 200; +var HASH_UNDEFINED = '__lodash_hash_undefined__'; +var MAX_SAFE_INTEGER = 9007199254740991; +var argsTag = '[object Arguments]', + arrayTag = '[object Array]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + errorTag = '[object Error]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + mapTag = '[object Map]', + numberTag = '[object Number]', + objectTag = '[object Object]', + promiseTag = '[object Promise]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + symbolTag = '[object Symbol]', + weakMapTag = '[object WeakMap]'; + +var arrayBufferTag = '[object ArrayBuffer]', + dataViewTag = '[object DataView]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; +var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; +var reFlags = /\w*$/; +var reIsHostCtor = /^\[object .+?Constructor\]$/; +var reIsUint = /^(?:0|[1-9]\d*)$/; +var cloneableTags = {}; +cloneableTags[argsTag] = cloneableTags[arrayTag] = +cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = +cloneableTags[boolTag] = cloneableTags[dateTag] = +cloneableTags[float32Tag] = cloneableTags[float64Tag] = +cloneableTags[int8Tag] = cloneableTags[int16Tag] = +cloneableTags[int32Tag] = cloneableTags[mapTag] = +cloneableTags[numberTag] = cloneableTags[objectTag] = +cloneableTags[regexpTag] = cloneableTags[setTag] = +cloneableTags[stringTag] = cloneableTags[symbolTag] = +cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = +cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; +cloneableTags[errorTag] = cloneableTags[funcTag] = +cloneableTags[weakMapTag] = false; +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; +var root = freeGlobal || freeSelf || Function('return this')(); +var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports; +var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; +var moduleExports = freeModule && freeModule.exports === freeExports; +function addMapEntry(map, pair) { + map.set(pair[0], pair[1]); + return map; +} +function addSetEntry(set, value) { + set.add(value); + return set; +} +function arrayEach(array, iteratee) { + var index = -1, + length = array ? array.length : 0; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; +} +function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; +} +function arrayReduce(array, iteratee, accumulator, initAccum) { + var index = -1, + length = array ? array.length : 0; + + if (initAccum && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; +} +function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; +} +function getValue(object, key) { + return object == null ? undefined : object[key]; +} +function isHostObject(value) { + var result = false; + if (value != null && typeof value.toString != 'function') { + try { + result = !!(value + ''); + } catch (e) {} + } + return result; +} +function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; +} +function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; +} +function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; +} +var arrayProto = Array.prototype, + funcProto = Function.prototype, + objectProto = Object.prototype; +var coreJsData = root['__core-js_shared__']; +var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; +}()); +var funcToString = funcProto.toString; +var hasOwnProperty = objectProto.hasOwnProperty; +var objectToString = objectProto.toString; +var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' +); +var Buffer = moduleExports ? root.Buffer : undefined, + Symbol = root.Symbol, + Uint8Array = root.Uint8Array, + getPrototype = overArg(Object.getPrototypeOf, Object), + objectCreate = Object.create, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + splice = arrayProto.splice; +var nativeGetSymbols = Object.getOwnPropertySymbols, + nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined, + nativeKeys = overArg(Object.keys, Object); +var DataView = getNative(root, 'DataView'), + Map = getNative(root, 'Map'), + Promise = getNative(root, 'Promise'), + Set = getNative(root, 'Set'), + WeakMap = getNative(root, 'WeakMap'), + nativeCreate = getNative(Object, 'create'); +var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map), + promiseCtorString = toSource(Promise), + setCtorString = toSource(Set), + weakMapCtorString = toSource(WeakMap); +var symbolProto = Symbol ? Symbol.prototype : undefined, + symbolValueOf = symbolProto ? symbolProto.valueOf : undefined; +function Hash(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} +function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; +} +function hashDelete(key) { + return this.has(key) && delete this.__data__[key]; +} +function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; +} +function hashHas(key) { + var data = this.__data__; + return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key); +} +function hashSet(key, value) { + var data = this.__data__; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; +} +Hash.prototype.clear = hashClear; +Hash.prototype['delete'] = hashDelete; +Hash.prototype.get = hashGet; +Hash.prototype.has = hashHas; +Hash.prototype.set = hashSet; +function ListCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} +function listCacheClear() { + this.__data__ = []; +} +function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + return true; +} +function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; +} +function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; +} +function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; +} +ListCache.prototype.clear = listCacheClear; +ListCache.prototype['delete'] = listCacheDelete; +ListCache.prototype.get = listCacheGet; +ListCache.prototype.has = listCacheHas; +ListCache.prototype.set = listCacheSet; +function MapCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} +function mapCacheClear() { + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; +} +function mapCacheDelete(key) { + return getMapData(this, key)['delete'](key); +} +function mapCacheGet(key) { + return getMapData(this, key).get(key); +} +function mapCacheHas(key) { + return getMapData(this, key).has(key); +} +function mapCacheSet(key, value) { + getMapData(this, key).set(key, value); + return this; +} +MapCache.prototype.clear = mapCacheClear; +MapCache.prototype['delete'] = mapCacheDelete; +MapCache.prototype.get = mapCacheGet; +MapCache.prototype.has = mapCacheHas; +MapCache.prototype.set = mapCacheSet; +function Stack(entries) { + this.__data__ = new ListCache(entries); +} +function stackClear() { + this.__data__ = new ListCache; +} +function stackDelete(key) { + return this.__data__['delete'](key); +} +function stackGet(key) { + return this.__data__.get(key); +} +function stackHas(key) { + return this.__data__.has(key); +} +function stackSet(key, value) { + var cache = this.__data__; + if (cache instanceof ListCache) { + var pairs = cache.__data__; + if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) { + pairs.push([key, value]); + return this; + } + cache = this.__data__ = new MapCache(pairs); + } + cache.set(key, value); + return this; +} +Stack.prototype.clear = stackClear; +Stack.prototype['delete'] = stackDelete; +Stack.prototype.get = stackGet; +Stack.prototype.has = stackHas; +Stack.prototype.set = stackSet; +function arrayLikeKeys(value, inherited) { + var result = (isArray(value) || isArguments(value)) + ? baseTimes(value.length, String) + : []; + + var length = result.length, + skipIndexes = !!length; + + for (var key in value) { + if ((inherited || hasOwnProperty.call(value, key)) && + !(skipIndexes && (key == 'length' || isIndex(key, length)))) { + result.push(key); + } + } + return result; +} +function assignValue(object, key, value) { + var objValue = object[key]; + if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || + (value === undefined && !(key in object))) { + object[key] = value; + } +} +function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; +} +function baseAssign(object, source) { + return object && copyObject(source, keys(source), object); +} +function baseClone(value, isDeep, isFull, customizer, key, object, stack) { + var result; + if (customizer) { + result = object ? customizer(value, key, object, stack) : customizer(value); + } + if (result !== undefined) { + return result; + } + if (!isObject(value)) { + return value; + } + var isArr = isArray(value); + if (isArr) { + result = initCloneArray(value); + if (!isDeep) { + return copyArray(value, result); + } + } else { + var tag = getTag(value), + isFunc = tag == funcTag || tag == genTag; + + if (isBuffer(value)) { + return cloneBuffer(value, isDeep); + } + if (tag == objectTag || tag == argsTag || (isFunc && !object)) { + if (isHostObject(value)) { + return object ? value : {}; + } + result = initCloneObject(isFunc ? {} : value); + if (!isDeep) { + return copySymbols(value, baseAssign(result, value)); + } + } else { + if (!cloneableTags[tag]) { + return object ? value : {}; + } + result = initCloneByTag(value, tag, baseClone, isDeep); + } + } + stack || (stack = new Stack); + var stacked = stack.get(value); + if (stacked) { + return stacked; + } + stack.set(value, result); + + if (!isArr) { + var props = isFull ? getAllKeys(value) : keys(value); + } + arrayEach(props || value, function(subValue, key) { + if (props) { + key = subValue; + subValue = value[key]; + } + assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack)); + }); + return result; +} +function baseCreate(proto) { + return isObject(proto) ? objectCreate(proto) : {}; +} +function baseGetAllKeys(object, keysFunc, symbolsFunc) { + var result = keysFunc(object); + return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); +} +function baseGetTag(value) { + return objectToString.call(value); +} +function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); +} +function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; +} +function cloneBuffer(buffer, isDeep) { + if (isDeep) { + return buffer.slice(); + } + var result = new buffer.constructor(buffer.length); + buffer.copy(result); + return result; +} +function cloneArrayBuffer(arrayBuffer) { + var result = new arrayBuffer.constructor(arrayBuffer.byteLength); + new Uint8Array(result).set(new Uint8Array(arrayBuffer)); + return result; +} +function cloneDataView(dataView, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer; + return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); +} +function cloneMap(map, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(mapToArray(map), true) : mapToArray(map); + return arrayReduce(array, addMapEntry, new map.constructor); +} +function cloneRegExp(regexp) { + var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); + result.lastIndex = regexp.lastIndex; + return result; +} +function cloneSet(set, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(setToArray(set), true) : setToArray(set); + return arrayReduce(array, addSetEntry, new set.constructor); +} +function cloneSymbol(symbol) { + return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; +} +function cloneTypedArray(typedArray, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; + return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); +} +function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; +} +function copyObject(source, props, object, customizer) { + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + + var newValue = customizer + ? customizer(object[key], source[key], key, object, source) + : undefined; + + assignValue(object, key, newValue === undefined ? source[key] : newValue); + } + return object; +} +function copySymbols(source, object) { + return copyObject(source, getSymbols(source), object); +} +function getAllKeys(object) { + return baseGetAllKeys(object, keys, getSymbols); +} +function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; +} +function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; +} +var getSymbols = nativeGetSymbols ? overArg(nativeGetSymbols, Object) : stubArray; +var getTag = baseGetTag; +if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || + (Map && getTag(new Map) != mapTag) || + (Promise && getTag(Promise.resolve()) != promiseTag) || + (Set && getTag(new Set) != setTag) || + (WeakMap && getTag(new WeakMap) != weakMapTag)) { + getTag = function(value) { + var result = objectToString.call(value), + Ctor = result == objectTag ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : undefined; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag; + case mapCtorString: return mapTag; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag; + case weakMapCtorString: return weakMapTag; + } + } + return result; + }; +} +function initCloneArray(array) { + var length = array.length, + result = array.constructor(length); + if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { + result.index = array.index; + result.input = array.input; + } + return result; +} +function initCloneObject(object) { + return (typeof object.constructor == 'function' && !isPrototype(object)) + ? baseCreate(getPrototype(object)) + : {}; +} +function initCloneByTag(object, tag, cloneFunc, isDeep) { + var Ctor = object.constructor; + switch (tag) { + case arrayBufferTag: + return cloneArrayBuffer(object); + + case boolTag: + case dateTag: + return new Ctor(+object); + + case dataViewTag: + return cloneDataView(object, isDeep); + + case float32Tag: case float64Tag: + case int8Tag: case int16Tag: case int32Tag: + case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: + return cloneTypedArray(object, isDeep); + + case mapTag: + return cloneMap(object, isDeep, cloneFunc); + + case numberTag: + case stringTag: + return new Ctor(object); + + case regexpTag: + return cloneRegExp(object); + + case setTag: + return cloneSet(object, isDeep, cloneFunc); + + case symbolTag: + return cloneSymbol(object); + } +} +function isIndex(value, length) { + length = length == null ? MAX_SAFE_INTEGER : length; + return !!length && + (typeof value == 'number' || reIsUint.test(value)) && + (value > -1 && value % 1 == 0 && value < length); +} +function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); +} +function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); +} +function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; + + return value === proto; +} +function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; +} +function clone(value) { + return baseClone(value, false, true); +} +function eq(value, other) { + return value === other || (value !== value && other !== other); +} +function isArguments(value) { + return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') && + (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag); +} +var isArray = Array.isArray; +function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); +} +function isArrayLikeObject(value) { + return isObjectLike(value) && isArrayLike(value); +} +var isBuffer = nativeIsBuffer || stubFalse; +function isFunction(value) { + var tag = isObject(value) ? objectToString.call(value) : ''; + return tag == funcTag || tag == genTag; +} +function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; +} +function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); +} +function isObjectLike(value) { + return !!value && typeof value == 'object'; +} +function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); +} +function stubArray() { + return []; +} +function stubFalse() { + return false; +} + +module.exports = clone; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],"/../../jshint/node_modules/lodash.slice/index.js":[function(_dereq_,module,exports){ +var INFINITY = 1 / 0, + MAX_SAFE_INTEGER = 9007199254740991, + MAX_INTEGER = 1.7976931348623157e+308, + NAN = 0 / 0; +var funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + symbolTag = '[object Symbol]'; +var reTrim = /^\s+|\s+$/g; +var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; +var reIsBinary = /^0b[01]+$/i; +var reIsOctal = /^0o[0-7]+$/i; +var reIsUint = /^(?:0|[1-9]\d*)$/; +var freeParseInt = parseInt; +var objectProto = Object.prototype; +var objectToString = objectProto.toString; +function baseSlice(array, start, end) { + var index = -1, + length = array.length; + + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = end > length ? length : end; + if (end < 0) { + end += length; + } + length = start > end ? 0 : ((end - start) >>> 0); + start >>>= 0; + + var result = Array(length); + while (++index < length) { + result[index] = array[index + start]; + } + return result; +} +function isIndex(value, length) { + length = length == null ? MAX_SAFE_INTEGER : length; + return !!length && + (typeof value == 'number' || reIsUint.test(value)) && + (value > -1 && value % 1 == 0 && value < length); +} +function isIterateeCall(value, index, object) { + if (!isObject(object)) { + return false; + } + var type = typeof index; + if (type == 'number' + ? (isArrayLike(object) && isIndex(index, object.length)) + : (type == 'string' && index in object) + ) { + return eq(object[index], value); + } + return false; +} +function slice(array, start, end) { + var length = array ? array.length : 0; + if (!length) { + return []; + } + if (end && typeof end != 'number' && isIterateeCall(array, start, end)) { + start = 0; + end = length; + } + else { + start = start == null ? 0 : toInteger(start); + end = end === undefined ? length : toInteger(end); + } + return baseSlice(array, start, end); +} +function eq(value, other) { + return value === other || (value !== value && other !== other); +} +function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); +} +function isFunction(value) { + var tag = isObject(value) ? objectToString.call(value) : ''; + return tag == funcTag || tag == genTag; +} +function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; +} +function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); +} +function isObjectLike(value) { + return !!value && typeof value == 'object'; +} +function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && objectToString.call(value) == symbolTag); +} +function toFinite(value) { + if (!value) { + return value === 0 ? value : 0; + } + value = toNumber(value); + if (value === INFINITY || value === -INFINITY) { + var sign = (value < 0 ? -1 : 1); + return sign * MAX_INTEGER; + } + return value === value ? value : 0; +} +function toInteger(value) { + var result = toFinite(value), + remainder = result % 1; + + return result === result ? (remainder ? result - remainder : result) : 0; +} +function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ''); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); +} + +module.exports = slice; + +},{}],"/../../jshint/node_modules/underscore/underscore.js":[function(_dereq_,module,exports){ +(function (global){ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define('underscore', factory) : + (function() { + var current = global._; + var exports = factory(); + global._ = exports; + exports.noConflict = function() { global._ = current; return exports; }; + })(); +}(this, (function () { + var root = typeof self == 'object' && self.self === self && self || + typeof global == 'object' && global.global === global && global || + Function('return this')() || + {}; + var ArrayProto = Array.prototype, ObjProto = Object.prototype; + var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null; + var push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + var nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeCreate = Object.create; + var _isNaN = root.isNaN, + _isFinite = root.isFinite; + var Ctor = function(){}; + function _(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + } + var VERSION = _.VERSION = '1.10.2'; + function optimizeCb(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + } + function baseIteratee(value, context, argCount) { + if (value == null) return identity; + if (isFunction(value)) return optimizeCb(value, context, argCount); + if (isObject(value) && !isArray(value)) return matcher(value); + return property(value); + } + _.iteratee = iteratee; + function iteratee(value, context) { + return baseIteratee(value, context, Infinity); + } + function cb(value, context, argCount) { + if (_.iteratee !== iteratee) return _.iteratee(value, context); + return baseIteratee(value, context, argCount); + } + function restArguments(func, startIndex) { + startIndex = startIndex == null ? func.length - 1 : +startIndex; + return function() { + var length = Math.max(arguments.length - startIndex, 0), + rest = Array(length), + index = 0; + for (; index < length; index++) { + rest[index] = arguments[index + startIndex]; + } + switch (startIndex) { + case 0: return func.call(this, rest); + case 1: return func.call(this, arguments[0], rest); + case 2: return func.call(this, arguments[0], arguments[1], rest); + } + var args = Array(startIndex + 1); + for (index = 0; index < startIndex; index++) { + args[index] = arguments[index]; + } + args[startIndex] = rest; + return func.apply(this, args); + }; + } + function baseCreate(prototype) { + if (!isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + } + + function shallowProperty(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + } + + function _has(obj, path) { + return obj != null && hasOwnProperty.call(obj, path); + } + + function deepGet(obj, path) { + var length = path.length; + for (var i = 0; i < length; i++) { + if (obj == null) return void 0; + obj = obj[path[i]]; + } + return length ? obj : void 0; + } + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + var getLength = shallowProperty('length'); + function isArrayLike(collection) { + var length = getLength(collection); + return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; + } + function each(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var _keys = keys(obj); + for (i = 0, length = _keys.length; i < length; i++) { + iteratee(obj[_keys[i]], _keys[i], obj); + } + } + return obj; + } + function map(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + function createReduce(dir) { + var reducer = function(obj, iteratee, memo, initial) { + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + index = dir > 0 ? 0 : length - 1; + if (!initial) { + memo = obj[_keys ? _keys[index] : index]; + index += dir; + } + for (; index >= 0 && index < length; index += dir) { + var currentKey = _keys ? _keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + return function(obj, iteratee, memo, context) { + var initial = arguments.length >= 3; + return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); + }; + } + var reduce = createReduce(1); + var reduceRight = createReduce(-1); + function find(obj, predicate, context) { + var keyFinder = isArrayLike(obj) ? findIndex : findKey; + var key = keyFinder(obj, predicate, context); + if (key !== void 0 && key !== -1) return obj[key]; + } + function filter(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + } + function reject(obj, predicate, context) { + return filter(obj, negate(cb(predicate)), context); + } + function every(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + } + function some(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + } + function contains(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return indexOf(obj, item, fromIndex) >= 0; + } + var invoke = restArguments(function(obj, path, args) { + var contextPath, func; + if (isFunction(path)) { + func = path; + } else if (isArray(path)) { + contextPath = path.slice(0, -1); + path = path[path.length - 1]; + } + return map(obj, function(context) { + var method = func; + if (!method) { + if (contextPath && contextPath.length) { + context = deepGet(context, contextPath); + } + if (context == null) return void 0; + method = context[path]; + } + return method == null ? method : method.apply(context, args); + }); + }); + function pluck(obj, key) { + return map(obj, property(key)); + } + function where(obj, attrs) { + return filter(obj, matcher(attrs)); + } + function findWhere(obj, attrs) { + return find(obj, matcher(attrs)); + } + function max(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + function min(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + function shuffle(obj) { + return sample(obj, Infinity); + } + function sample(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = values(obj); + return obj[random(obj.length - 1)]; + } + var sample = isArrayLike(obj) ? clone(obj) : values(obj); + var length = getLength(sample); + n = Math.max(Math.min(n, length), 0); + var last = length - 1; + for (var index = 0; index < n; index++) { + var rand = random(index, last); + var temp = sample[index]; + sample[index] = sample[rand]; + sample[rand] = temp; + } + return sample.slice(0, n); + } + function sortBy(obj, iteratee, context) { + var index = 0; + iteratee = cb(iteratee, context); + return pluck(map(obj, function(value, key, list) { + return { + value: value, + index: index++, + criteria: iteratee(value, key, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + } + function group(behavior, partition) { + return function(obj, iteratee, context) { + var result = partition ? [[], []] : {}; + iteratee = cb(iteratee, context); + each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + } + var groupBy = group(function(result, value, key) { + if (_has(result, key)) result[key].push(value); else result[key] = [value]; + }); + var indexBy = group(function(result, value, key) { + result[key] = value; + }); + var countBy = group(function(result, value, key) { + if (_has(result, key)) result[key]++; else result[key] = 1; + }); + + var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; + function toArray(obj) { + if (!obj) return []; + if (isArray(obj)) return slice.call(obj); + if (isString(obj)) { + return obj.match(reStrSymbol); + } + if (isArrayLike(obj)) return map(obj, identity); + return values(obj); + } + function size(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : keys(obj).length; + } + var partition = group(function(result, value, pass) { + result[pass ? 0 : 1].push(value); + }, true); + function first(array, n, guard) { + if (array == null || array.length < 1) return n == null ? void 0 : []; + if (n == null || guard) return array[0]; + return initial(array, array.length - n); + } + function initial(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + } + function last(array, n, guard) { + if (array == null || array.length < 1) return n == null ? void 0 : []; + if (n == null || guard) return array[array.length - 1]; + return rest(array, Math.max(0, array.length - n)); + } + function rest(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + } + function compact(array) { + return filter(array, Boolean); + } + function _flatten(input, shallow, strict, output) { + output = output || []; + var idx = output.length; + for (var i = 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (isArray(value) || isArguments(value))) { + if (shallow) { + var j = 0, len = value.length; + while (j < len) output[idx++] = value[j++]; + } else { + _flatten(value, shallow, strict, output); + idx = output.length; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + } + function flatten(array, shallow) { + return _flatten(array, shallow, false); + } + var without = restArguments(function(array, otherArrays) { + return difference(array, otherArrays); + }); + function uniq(array, isSorted, iteratee, context) { + if (!isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted && !iteratee) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!contains(result, value)) { + result.push(value); + } + } + return result; + } + var union = restArguments(function(arrays) { + return uniq(_flatten(arrays, true, true)); + }); + function intersection(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (contains(result, item)) continue; + var j; + for (j = 1; j < argsLength; j++) { + if (!contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + } + var difference = restArguments(function(array, rest) { + rest = _flatten(rest, true, true); + return filter(array, function(value){ + return !contains(rest, value); + }); + }); + function unzip(array) { + var length = array && max(array, getLength).length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = pluck(array, index); + } + return result; + } + var zip = restArguments(unzip); + function object(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + } + function createPredicateIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + var findIndex = createPredicateIndexFinder(1); + var findLastIndex = createPredicateIndexFinder(-1); + function sortedIndex(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + } + function createIndexFinder(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), isNaN); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + } + var indexOf = createIndexFinder(1, findIndex, sortedIndex); + var lastIndexOf = createIndexFinder(-1, findLastIndex); + function range(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + if (!step) { + step = stop < start ? -1 : 1; + } + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + } + function chunk(array, count) { + if (count == null || count < 1) return []; + var result = []; + var i = 0, length = array.length; + while (i < length) { + result.push(slice.call(array, i, i += count)); + } + return result; + } + function executeBound(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (isObject(result)) return result; + return self; + } + var bind = restArguments(function(func, context, args) { + if (!isFunction(func)) throw new TypeError('Bind must be called on a function'); + var bound = restArguments(function(callArgs) { + return executeBound(func, bound, context, this, args.concat(callArgs)); + }); + return bound; + }); + var partial = restArguments(function(func, boundArgs) { + var placeholder = partial.placeholder; + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }); + + partial.placeholder = _; + var bindAll = restArguments(function(obj, _keys) { + _keys = _flatten(_keys, false, false); + var index = _keys.length; + if (index < 1) throw new Error('bindAll must be passed function names'); + while (index--) { + var key = _keys[index]; + obj[key] = bind(obj[key], obj); + } + }); + function memoize(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!_has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + } + var delay = restArguments(function(func, wait, args) { + return setTimeout(function() { + return func.apply(null, args); + }, wait); + }); + var defer = partial(delay, _, 1); + function throttle(func, wait, options) { + var timeout, context, args, result; + var previous = 0; + if (!options) options = {}; + + var later = function() { + previous = options.leading === false ? 0 : now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + + var throttled = function() { + var _now = now(); + if (!previous && options.leading === false) previous = _now; + var remaining = wait - (_now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = _now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + + throttled.cancel = function() { + clearTimeout(timeout); + previous = 0; + timeout = context = args = null; + }; + + return throttled; + } + function debounce(func, wait, immediate) { + var timeout, result; + + var later = function(context, args) { + timeout = null; + if (args) result = func.apply(context, args); + }; + + var debounced = restArguments(function(args) { + if (timeout) clearTimeout(timeout); + if (immediate) { + var callNow = !timeout; + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(this, args); + } else { + timeout = delay(later, wait, this, args); + } + + return result; + }); + + debounced.cancel = function() { + clearTimeout(timeout); + timeout = null; + }; + + return debounced; + } + function wrap(func, wrapper) { + return partial(wrapper, func); + } + function negate(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + } + function compose() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + } + function after(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + } + function before(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + } + var once = partial(before, 2); + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + function collectNonEnumProps(obj, _keys) { + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = isFunction(constructor) && constructor.prototype || ObjProto; + var prop = 'constructor'; + if (_has(obj, prop) && !contains(_keys, prop)) _keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !contains(_keys, prop)) { + _keys.push(prop); + } + } + } + function keys(obj) { + if (!isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var _keys = []; + for (var key in obj) if (_has(obj, key)) _keys.push(key); + if (hasEnumBug) collectNonEnumProps(obj, _keys); + return _keys; + } + function allKeys(obj) { + if (!isObject(obj)) return []; + var _keys = []; + for (var key in obj) _keys.push(key); + if (hasEnumBug) collectNonEnumProps(obj, _keys); + return _keys; + } + function values(obj) { + var _keys = keys(obj); + var length = _keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[_keys[i]]; + } + return values; + } + function mapObject(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = keys(obj), + length = _keys.length, + results = {}; + for (var index = 0; index < length; index++) { + var currentKey = _keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + function pairs(obj) { + var _keys = keys(obj); + var length = _keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [_keys[i], obj[_keys[i]]]; + } + return pairs; + } + function invert(obj) { + var result = {}; + var _keys = keys(obj); + for (var i = 0, length = _keys.length; i < length; i++) { + result[obj[_keys[i]]] = _keys[i]; + } + return result; + } + function functions(obj) { + var names = []; + for (var key in obj) { + if (isFunction(obj[key])) names.push(key); + } + return names.sort(); + } + function createAssigner(keysFunc, defaults) { + return function(obj) { + var length = arguments.length; + if (defaults) obj = Object(obj); + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + _keys = keysFunc(source), + l = _keys.length; + for (var i = 0; i < l; i++) { + var key = _keys[i]; + if (!defaults || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + } + var extend = createAssigner(allKeys); + var extendOwn = createAssigner(keys); + function findKey(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = keys(obj), key; + for (var i = 0, length = _keys.length; i < length; i++) { + key = _keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + } + function keyInObj(value, key, obj) { + return key in obj; + } + var pick = restArguments(function(obj, _keys) { + var result = {}, iteratee = _keys[0]; + if (obj == null) return result; + if (isFunction(iteratee)) { + if (_keys.length > 1) iteratee = optimizeCb(iteratee, _keys[1]); + _keys = allKeys(obj); + } else { + iteratee = keyInObj; + _keys = _flatten(_keys, false, false); + obj = Object(obj); + } + for (var i = 0, length = _keys.length; i < length; i++) { + var key = _keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }); + var omit = restArguments(function(obj, _keys) { + var iteratee = _keys[0], context; + if (isFunction(iteratee)) { + iteratee = negate(iteratee); + if (_keys.length > 1) context = _keys[1]; + } else { + _keys = map(_flatten(_keys, false, false), String); + iteratee = function(value, key) { + return !contains(_keys, key); + }; + } + return pick(obj, iteratee, context); + }); + var defaults = createAssigner(allKeys, true); + function create(prototype, props) { + var result = baseCreate(prototype); + if (props) extendOwn(result, props); + return result; + } + function clone(obj) { + if (!isObject(obj)) return obj; + return isArray(obj) ? obj.slice() : extend({}, obj); + } + function tap(obj, interceptor) { + interceptor(obj); + return obj; + } + function isMatch(object, attrs) { + var _keys = keys(attrs), length = _keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = _keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + } + function eq(a, b, aStack, bStack) { + if (a === b) return a !== 0 || 1 / a === 1 / b; + if (a == null || b == null) return false; + if (a !== a) return b !== b; + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + return deepEq(a, b, aStack, bStack); + } + function deepEq(a, b, aStack, bStack) { + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + case '[object RegExp]': + case '[object String]': + return '' + a === '' + b; + case '[object Number]': + if (+a !== +a) return +b !== +b; + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + return +a === +b; + case '[object Symbol]': + return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + } + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && + isFunction(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + if (aStack[length] === a) return bStack[length] === b; + } + aStack.push(a); + bStack.push(b); + if (areArrays) { + length = a.length; + if (length !== b.length) return false; + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + var _keys = keys(a), key; + length = _keys.length; + if (keys(b).length !== length) return false; + while (length--) { + key = _keys[length]; + if (!(_has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + aStack.pop(); + bStack.pop(); + return true; + } + function isEqual(a, b) { + return eq(a, b); + } + function isEmpty(obj) { + if (obj == null) return true; + if (isArrayLike(obj) && (isArray(obj) || isString(obj) || isArguments(obj))) return obj.length === 0; + return keys(obj).length === 0; + } + function isElement(obj) { + return !!(obj && obj.nodeType === 1); + } + function tagTester(name) { + return function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + } + var isArray = nativeIsArray || tagTester('Array'); + function isObject(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + } + var isArguments = tagTester('Arguments'); + var isFunction = tagTester('Function'); + var isString = tagTester('String'); + var isNumber = tagTester('Number'); + var isDate = tagTester('Date'); + var isRegExp = tagTester('RegExp'); + var isError = tagTester('Error'); + var isSymbol = tagTester('Symbol'); + var isMap = tagTester('Map'); + var isWeakMap = tagTester('WeakMap'); + var isSet = tagTester('Set'); + var isWeakSet = tagTester('WeakSet'); + (function() { + if (!isArguments(arguments)) { + isArguments = function(obj) { + return _has(obj, 'callee'); + }; + } + }()); + var nodelist = root.document && root.document.childNodes; + if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') { + isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + function isFinite(obj) { + return !isSymbol(obj) && _isFinite(obj) && !_isNaN(parseFloat(obj)); + } + function isNaN(obj) { + return isNumber(obj) && _isNaN(obj); + } + function isBoolean(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + } + function isNull(obj) { + return obj === null; + } + function isUndefined(obj) { + return obj === void 0; + } + function has(obj, path) { + if (!isArray(path)) { + return _has(obj, path); + } + var length = path.length; + for (var i = 0; i < length; i++) { + var key = path[i]; + if (obj == null || !hasOwnProperty.call(obj, key)) { + return false; + } + obj = obj[key]; + } + return !!length; + } + function identity(value) { + return value; + } + function constant(value) { + return function() { + return value; + }; + } + + function noop(){} + function property(path) { + if (!isArray(path)) { + return shallowProperty(path); + } + return function(obj) { + return deepGet(obj, path); + }; + } + function propertyOf(obj) { + if (obj == null) { + return function(){}; + } + return function(path) { + return !isArray(path) ? obj[path] : deepGet(obj, path); + }; + } + function matcher(attrs) { + attrs = extendOwn({}, attrs); + return function(obj) { + return isMatch(obj, attrs); + }; + } + function times(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + } + function random(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + } + var now = Date.now || function() { + return new Date().getTime(); + }; + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + var unescapeMap = invert(escapeMap); + function createEscaper(map) { + var escaper = function(match) { + return map[match]; + }; + var source = '(?:' + keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + } + var escape = createEscaper(escapeMap); + var unescape = createEscaper(unescapeMap); + function result(obj, path, fallback) { + if (!isArray(path)) path = [path]; + var length = path.length; + if (!length) { + return isFunction(fallback) ? fallback.call(obj) : fallback; + } + for (var i = 0; i < length; i++) { + var prop = obj == null ? void 0 : obj[path[i]]; + if (prop === void 0) { + prop = fallback; + i = length; // Ensure we don't continue iterating. + } + obj = isFunction(prop) ? prop.call(obj) : prop; + } + return obj; + } + var idCounter = 0; + function uniqueId(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + } + var templateSettings = _.templateSettings = { + evaluate: /<%([\s\S]+?)%>/g, + interpolate: /<%=([\s\S]+?)%>/g, + escape: /<%-([\s\S]+?)%>/g + }; + var noMatch = /(.)^/; + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; + + var escapeChar = function(match) { + return '\\' + escapes[match]; + }; + function template(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = defaults({}, settings, _.templateSettings); + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escapeRegExp, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + return match; + }); + source += "';\n"; + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + var render; + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + } + function chain(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + } + function chainResult(instance, obj) { + return instance._chain ? _(obj).chain() : obj; + } + function mixin(obj) { + each(functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return chainResult(this, func.apply(_, args)); + }; + }); + return _; + } + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; + return chainResult(this, obj); + }; + }); + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return chainResult(this, method.apply(this._wrapped, arguments)); + }; + }); + _.prototype.value = function() { + return this._wrapped; + }; + _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; + + _.prototype.toString = function() { + return String(this._wrapped); + }; + + var allExports = ({ + 'default': _, + VERSION: VERSION, + iteratee: iteratee, + restArguments: restArguments, + each: each, + forEach: each, + map: map, + collect: map, + reduce: reduce, + foldl: reduce, + inject: reduce, + reduceRight: reduceRight, + foldr: reduceRight, + find: find, + detect: find, + filter: filter, + select: filter, + reject: reject, + every: every, + all: every, + some: some, + any: some, + contains: contains, + includes: contains, + include: contains, + invoke: invoke, + pluck: pluck, + where: where, + findWhere: findWhere, + max: max, + min: min, + shuffle: shuffle, + sample: sample, + sortBy: sortBy, + groupBy: groupBy, + indexBy: indexBy, + countBy: countBy, + toArray: toArray, + size: size, + partition: partition, + first: first, + head: first, + take: first, + initial: initial, + last: last, + rest: rest, + tail: rest, + drop: rest, + compact: compact, + flatten: flatten, + without: without, + uniq: uniq, + unique: uniq, + union: union, + intersection: intersection, + difference: difference, + unzip: unzip, + zip: zip, + object: object, + findIndex: findIndex, + findLastIndex: findLastIndex, + sortedIndex: sortedIndex, + indexOf: indexOf, + lastIndexOf: lastIndexOf, + range: range, + chunk: chunk, + bind: bind, + partial: partial, + bindAll: bindAll, + memoize: memoize, + delay: delay, + defer: defer, + throttle: throttle, + debounce: debounce, + wrap: wrap, + negate: negate, + compose: compose, + after: after, + before: before, + once: once, + keys: keys, + allKeys: allKeys, + values: values, + mapObject: mapObject, + pairs: pairs, + invert: invert, + functions: functions, + methods: functions, + extend: extend, + extendOwn: extendOwn, + assign: extendOwn, + findKey: findKey, + pick: pick, + omit: omit, + defaults: defaults, + create: create, + clone: clone, + tap: tap, + isMatch: isMatch, + isEqual: isEqual, + isEmpty: isEmpty, + isElement: isElement, + isArray: isArray, + isObject: isObject, + isArguments: isArguments, + isFunction: isFunction, + isString: isString, + isNumber: isNumber, + isDate: isDate, + isRegExp: isRegExp, + isError: isError, + isSymbol: isSymbol, + isMap: isMap, + isWeakMap: isWeakMap, + isSet: isSet, + isWeakSet: isWeakSet, + isFinite: isFinite, + isNaN: isNaN, + isBoolean: isBoolean, + isNull: isNull, + isUndefined: isUndefined, + has: has, + identity: identity, + constant: constant, + noop: noop, + property: property, + propertyOf: propertyOf, + matcher: matcher, + matches: matcher, + times: times, + random: random, + now: now, + escape: escape, + unescape: unescape, + result: result, + uniqueId: uniqueId, + templateSettings: templateSettings, + template: template, + chain: chain, + mixin: mixin + }); + var _$1 = mixin(allExports); + _$1._ = _$1; + + return _$1; + +}))); + + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],"/../../jshint/src/jshint.js":[function(_dereq_,module,exports){ + +var _ = _dereq_("underscore"); +_.clone = _dereq_("lodash.clone"); +var events = _dereq_("events"); +var vars = _dereq_("./vars.js"); +var messages = _dereq_("./messages.js"); +var Lexer = _dereq_("./lex.js").Lexer; +var reg = _dereq_("./reg.js"); +var state = _dereq_("./state.js").state; +var style = _dereq_("./style.js"); +var options = _dereq_("./options.js"); +var scopeManager = _dereq_("./scope-manager.js"); +var prodParams = _dereq_("./prod-params.js"); +var console = _dereq_("console-browserify"); + +var JSHINT = (function() { + "use strict"; + + var api, // Extension API + bang = { + "<" : true, + "<=" : true, + "==" : true, + "===": true, + "!==": true, + "!=" : true, + ">" : true, + ">=" : true, + "+" : true, + "-" : true, + "*" : true, + "/" : true, + "%" : true + }, + + declared, // Globals that were declared using /*global ... */ syntax. + + functions, // All of the functions + + inblock, + indent, + lookahead, + lex, + member, + membersOnly, + predefined, // Global variables defined by option + + extraModules = [], + emitter = new events.EventEmitter(); + + function checkOption(name, isStable, t) { + var type, validNames; + + if (isStable) { + type = ""; + validNames = options.validNames; + } else { + type = "unstable "; + validNames = options.unstableNames; + } + + name = name.trim(); + + if (/^[+-]W\d{3}$/g.test(name)) { + return true; + } + + if (validNames.indexOf(name) === -1) { + if (t.type !== "jslint" && !_.has(options.removed, name)) { + error("E001", t, type, name); + return false; + } + } + + return true; + } + + function isString(obj) { + return Object.prototype.toString.call(obj) === "[object String]"; + } + + function isIdentifier(tkn, value) { + if (!tkn) + return false; + + if (!tkn.identifier || tkn.value !== value) + return false; + + return true; + } + function isReserved(context, token) { + if (!token.reserved) { + return false; + } + var meta = token.meta; + + if (meta && meta.isFutureReservedWord) { + if (state.inES5()) { + if (!meta.es5) { + return false; + } + + if (token.isProperty) { + return false; + } + } + } else if (meta && meta.es5 && !state.inES5()) { + return false; + } + if (meta && meta.strictOnly && state.inES5()) { + if (!state.option.strict && !state.isStrict()) { + return false; + } + } + + if (token.id === "await" && (!(context & prodParams.async) && !state.option.module)) { + return false; + } + + if (token.id === "yield" && (!(context & prodParams.yield))) { + return state.isStrict(); + } + + return true; + } + + function supplant(str, data) { + return str.replace(/\{([^{}]*)\}/g, function(a, b) { + var r = data[b]; + return typeof r === "string" || typeof r === "number" ? r : a; + }); + } + + function combine(dest, src) { + Object.keys(src).forEach(function(name) { + if (_.has(JSHINT.blacklist, name)) return; + dest[name] = src[name]; + }); + } + + function processenforceall() { + if (state.option.enforceall) { + for (var enforceopt in options.bool.enforcing) { + if (state.option[enforceopt] === undefined && + !options.noenforceall[enforceopt]) { + state.option[enforceopt] = true; + } + } + for (var relaxopt in options.bool.relaxing) { + if (state.option[relaxopt] === undefined) { + state.option[relaxopt] = false; + } + } + } + } + function applyOptions() { + var badESOpt = null; + processenforceall(); + badESOpt = state.inferEsVersion(); + if (badESOpt) { + quit("E059", state.tokens.next, "esversion", badESOpt); + } + + if (state.inES5()) { + combine(predefined, vars.ecmaIdentifiers[5]); + } + + if (state.inES6()) { + combine(predefined, vars.ecmaIdentifiers[6]); + } + + if (state.inES8()) { + combine(predefined, vars.ecmaIdentifiers[8]); + } + if (state.option.strict === "global" && "globalstrict" in state.option) { + quit("E059", state.tokens.next, "strict", "globalstrict"); + } + + if (state.option.module) { + if (!state.inES6()) { + warning("W134", state.tokens.next, "module", 6); + } + } + + if (state.option.regexpu) { + if (!state.inES6()) { + warning("W134", state.tokens.next, "regexpu", 6); + } + } + + if (state.option.couch) { + combine(predefined, vars.couch); + } + + if (state.option.qunit) { + combine(predefined, vars.qunit); + } + + if (state.option.rhino) { + combine(predefined, vars.rhino); + } + + if (state.option.shelljs) { + combine(predefined, vars.shelljs); + combine(predefined, vars.node); + } + if (state.option.typed) { + combine(predefined, vars.typed); + } + + if (state.option.phantom) { + combine(predefined, vars.phantom); + } + + if (state.option.prototypejs) { + combine(predefined, vars.prototypejs); + } + + if (state.option.node) { + combine(predefined, vars.node); + combine(predefined, vars.typed); + } + + if (state.option.devel) { + combine(predefined, vars.devel); + } + + if (state.option.dojo) { + combine(predefined, vars.dojo); + } + + if (state.option.browser) { + combine(predefined, vars.browser); + combine(predefined, vars.typed); + } + + if (state.option.browserify) { + combine(predefined, vars.browser); + combine(predefined, vars.typed); + combine(predefined, vars.browserify); + } + + if (state.option.nonstandard) { + combine(predefined, vars.nonstandard); + } + + if (state.option.jasmine) { + combine(predefined, vars.jasmine); + } + + if (state.option.jquery) { + combine(predefined, vars.jquery); + } + + if (state.option.mootools) { + combine(predefined, vars.mootools); + } + + if (state.option.worker) { + combine(predefined, vars.worker); + } + + if (state.option.wsh) { + combine(predefined, vars.wsh); + } + + if (state.option.yui) { + combine(predefined, vars.yui); + } + + if (state.option.mocha) { + combine(predefined, vars.mocha); + } + } + function quit(code, token, a, b) { + var percentage = Math.floor((token.line / state.lines.length) * 100); + var message = messages.errors[code].desc; + + var exception = { + name: "JSHintError", + line: token.line, + character: token.from, + message: message + " (" + percentage + "% scanned).", + raw: message, + code: code, + a: a, + b: b + }; + + exception.reason = supplant(message, exception) + " (" + percentage + + "% scanned)."; + + throw exception; + } + + function removeIgnoredMessages() { + var ignored = state.ignoredLines; + + if (_.isEmpty(ignored)) return; + JSHINT.errors = _.reject(JSHINT.errors, function(err) { return ignored[err.line] }); + } + + function warning(code, t, a, b, c, d) { + var ch, l, w, msg; + + if (/^W\d{3}$/.test(code)) { + if (state.ignored[code]) + return; + + msg = messages.warnings[code]; + } else if (/E\d{3}/.test(code)) { + msg = messages.errors[code]; + } else if (/I\d{3}/.test(code)) { + msg = messages.info[code]; + } + + t = t || state.tokens.next || {}; + if (t.id === "(end)") { // `~ + t = state.tokens.curr; + } + + l = t.line; + ch = t.from; + + w = { + id: "(error)", + raw: msg.desc, + code: msg.code, + evidence: state.lines[l - 1] || "", + line: l, + character: ch, + scope: JSHINT.scope, + a: a, + b: b, + c: c, + d: d + }; + + w.reason = supplant(msg.desc, w); + JSHINT.errors.push(w); + + removeIgnoredMessages(); + + if (JSHINT.errors.length >= state.option.maxerr) + quit("E043", t); + + return w; + } + + function warningAt(m, l, ch, a, b, c, d) { + return warning(m, { + line: l, + from: ch + }, a, b, c, d); + } + + function error(m, t, a, b, c, d) { + warning(m, t, a, b, c, d); + } + + function errorAt(m, l, ch, a, b, c, d) { + return error(m, { + line: l, + from: ch + }, a, b, c, d); + } + function addEvalCode(elem, token) { + JSHINT.internals.push({ + id: "(internal)", + elem: elem, + token: token, + code: token.value.replace(/([^\\])(\\*)\2\\n/g, "$1\n") + }); + } + function lintingDirective(directiveToken, previous) { + var body = directiveToken.body.split(",") + .map(function(s) { return s.trim(); }); + var predef = {}; + + if (directiveToken.type === "falls through") { + previous.caseFallsThrough = true; + return; + } + + if (directiveToken.type === "globals") { + body.forEach(function(item, idx) { + var parts = item.split(":"); + var key = parts[0].trim(); + + if (key === "-" || !key.length) { + if (idx > 0 && idx === body.length - 1) { + return; + } + error("E002", directiveToken); + return; + } + + if (key.charAt(0) === "-") { + key = key.slice(1); + + JSHINT.blacklist[key] = key; + delete predefined[key]; + } else { + predef[key] = parts.length > 1 && parts[1].trim() === "true"; + } + }); + + combine(predefined, predef); + + for (var key in predef) { + if (_.has(predef, key)) { + declared[key] = directiveToken; + } + } + } + + if (directiveToken.type === "exported") { + body.forEach(function(e, idx) { + if (!e.length) { + if (idx > 0 && idx === body.length - 1) { + return; + } + error("E002", directiveToken); + return; + } + + state.funct["(scope)"].addExported(e); + }); + } + + if (directiveToken.type === "members") { + membersOnly = membersOnly || {}; + + body.forEach(function(m) { + var ch1 = m.charAt(0); + var ch2 = m.charAt(m.length - 1); + + if (ch1 === ch2 && (ch1 === "\"" || ch1 === "'")) { + m = m + .substr(1, m.length - 2) + .replace("\\\"", "\""); + } + + membersOnly[m] = false; + }); + } + + var numvals = [ + "maxstatements", + "maxparams", + "maxdepth", + "maxcomplexity", + "maxerr", + "maxlen", + "indent" + ]; + + if (directiveToken.type === "jshint" || directiveToken.type === "jslint" || + directiveToken.type === "jshint.unstable") { + body.forEach(function(item) { + var parts = item.split(":"); + var key = parts[0].trim(); + var val = parts.length > 1 ? parts[1].trim() : ""; + var numberVal; + + if (!checkOption(key, directiveToken.type !== "jshint.unstable", directiveToken)) { + return; + } + + if (numvals.indexOf(key) >= 0) { + if (val !== "false") { + numberVal = +val; + + if (typeof numberVal !== "number" || !isFinite(numberVal) || + numberVal <= 0 || Math.floor(numberVal) !== numberVal) { + error("E032", directiveToken, val); + return; + } + + state.option[key] = numberVal; + } else { + state.option[key] = key === "indent" ? 4 : false; + } + + return; + } + + if (key === "validthis") { + + if (state.funct["(global)"]) + return void error("E009"); + + if (val !== "true" && val !== "false") + return void error("E002", directiveToken); + + state.option.validthis = (val === "true"); + return; + } + + if (key === "quotmark") { + switch (val) { + case "true": + case "false": + state.option.quotmark = (val === "true"); + break; + case "double": + case "single": + state.option.quotmark = val; + break; + default: + error("E002", directiveToken); + } + return; + } + + if (key === "shadow") { + switch (val) { + case "true": + state.option.shadow = true; + break; + case "outer": + state.option.shadow = "outer"; + break; + case "false": + case "inner": + state.option.shadow = "inner"; + break; + default: + error("E002", directiveToken); + } + return; + } + + if (key === "unused") { + switch (val) { + case "true": + state.option.unused = true; + break; + case "false": + state.option.unused = false; + break; + case "vars": + case "strict": + state.option.unused = val; + break; + default: + error("E002", directiveToken); + } + return; + } + + if (key === "latedef") { + switch (val) { + case "true": + state.option.latedef = true; + break; + case "false": + state.option.latedef = false; + break; + case "nofunc": + state.option.latedef = "nofunc"; + break; + default: + error("E002", directiveToken); + } + return; + } + + if (key === "ignore") { + switch (val) { + case "line": + state.ignoredLines[directiveToken.line] = true; + removeIgnoredMessages(); + break; + default: + error("E002", directiveToken); + } + return; + } + + if (key === "strict") { + switch (val) { + case "true": + state.option.strict = true; + break; + case "false": + state.option.strict = false; + break; + case "global": + case "implied": + state.option.strict = val; + break; + default: + error("E002", directiveToken); + } + return; + } + + if (key === "module") { + if (!hasParsedCode(state.funct)) { + error("E055", directiveToken, "module"); + } + } + + if (key === "esversion") { + switch (val) { + case "3": + case "5": + case "6": + case "7": + case "8": + case "9": + case "10": + state.option.moz = false; + state.option.esversion = +val; + break; + case "2015": + case "2016": + case "2017": + case "2018": + case "2019": + state.option.moz = false; + state.option.esversion = +val - 2009; + break; + default: + error("E002", directiveToken); + } + if (!hasParsedCode(state.funct)) { + error("E055", directiveToken, "esversion"); + } + return; + } + + var match = /^([+-])(W\d{3})$/g.exec(key); + if (match) { + state.ignored[match[2]] = (match[1] === "-"); + return; + } + + var tn; + if (val === "true" || val === "false") { + if (directiveToken.type === "jslint") { + tn = options.renamed[key] || key; + state.option[tn] = (val === "true"); + + if (options.inverted[tn] !== undefined) { + state.option[tn] = !state.option[tn]; + } + } else if (directiveToken.type === "jshint.unstable") { + state.option.unstable[key] = (val === "true"); + } else { + state.option[key] = (val === "true"); + } + + return; + } + + error("E002", directiveToken); + }); + + applyOptions(); + } + } + function peek(p) { + var i = p || 0, j = lookahead.length, t; + + if (i < j) { + return lookahead[i]; + } + + while (j <= i) { + t = lex.token(); + if (!t) { + if (!lookahead.length) { + return state.tokens.next; + } + + return lookahead[j - 1]; + } + + lookahead[j] = t; + j += 1; + } + + return t; + } + + function peekIgnoreEOL() { + var i = 0; + var t; + do { + t = peek(i++); + } while (t.id === "(endline)"); + return t; + } + function advance(expected, relatedToken) { + var nextToken = state.tokens.next; + + if (expected && nextToken.id !== expected) { + if (relatedToken) { + if (nextToken.id === "(end)") { + error("E019", relatedToken, relatedToken.id); + } else { + error("E020", nextToken, expected, relatedToken.id, + relatedToken.line, nextToken.value); + } + } else if (nextToken.type !== "(identifier)" || nextToken.value !== expected) { + error("E021", nextToken, expected, nextToken.value); + } + } + + state.tokens.prev = state.tokens.curr; + state.tokens.curr = state.tokens.next; + for (;;) { + state.tokens.next = lookahead.shift() || lex.token(); + + if (!state.tokens.next) { // No more tokens left, give up + quit("E041", state.tokens.curr); + } + + if (state.tokens.next.id === "(end)" || state.tokens.next.id === "(error)") { + return; + } + + if (state.tokens.next.check) { + state.tokens.next.check(); + } + + if (state.tokens.next.isSpecial) { + lintingDirective(state.tokens.next, state.tokens.curr); + } else { + if (state.tokens.next.id !== "(endline)") { + break; + } + } + } + } + function isOperator(token) { + return token.first || token.right || token.left || token.id === "yield" || token.id === "await"; + } + + function isEndOfExpr(context, curr, next) { + if (arguments.length <= 1) { + curr = state.tokens.curr; + next = state.tokens.next; + } + + if (next.id === "in" && context & prodParams.noin) { + return true; + } + + if (next.id === ";" || next.id === "}" || next.id === ":") { + return true; + } + + if (next.infix === curr.infix || + (curr.id === "yield" && curr.rbp < next.rbp)) { + return !sameLine(curr, next); + } + + return false; + } + function expression(context, rbp) { + var left, isArray = false, isObject = false; + var initial = context & prodParams.initial; + var curr; + + context &= ~prodParams.initial; + + state.nameStack.push(); + + if (state.tokens.next.id === "(end)") + error("E006", state.tokens.curr); + + advance(); + + if (initial) { + state.funct["(verb)"] = state.tokens.curr.value; + state.tokens.curr.beginsStmt = true; + } + + curr = state.tokens.curr; + + if (initial && curr.fud && (!curr.useFud || curr.useFud(context))) { + left = state.tokens.curr.fud(context); + } else { + if (state.tokens.curr.nud) { + left = state.tokens.curr.nud(context, rbp); + } else { + error("E030", state.tokens.curr, state.tokens.curr.id); + } + + while (rbp < state.tokens.next.lbp && !isEndOfExpr(context)) { + isArray = state.tokens.curr.value === "Array"; + isObject = state.tokens.curr.value === "Object"; + if (left && (left.value || (left.first && left.first.value))) { + if (left.value !== "new" || + (left.first && left.first.value && left.first.value === ".")) { + isArray = false; + if (left.value !== state.tokens.curr.value) { + isObject = false; + } + } + } + + advance(); + + if (isArray && state.tokens.curr.id === "(" && state.tokens.next.id === ")") { + warning("W009", state.tokens.curr); + } + + if (isObject && state.tokens.curr.id === "(" && state.tokens.next.id === ")") { + warning("W010", state.tokens.curr); + } + + if (left && state.tokens.curr.led) { + left = state.tokens.curr.led(context, left); + } else { + error("E033", state.tokens.curr, state.tokens.curr.id); + } + } + } + + state.nameStack.pop(); + + return left; + } + + function sameLine(first, second) { + return first.line === (second.startLine || second.line); + } + + function nobreaknonadjacent(left, right) { + if (!state.option.laxbreak && !sameLine(left, right)) { + warning("W014", right, right.value); + } + } + + function nolinebreak(t) { + t = t; + if (!sameLine(t, state.tokens.next)) { + warning("E022", t, t.value); + } + } + function checkComma(opts) { + var prev = state.tokens.prev; + var curr = state.tokens.curr; + opts = opts || {}; + + if (!sameLine(prev, curr)) { + if (!state.option.laxcomma) { + if (checkComma.first) { + warning("I001", curr); + checkComma.first = false; + } + warning("W014", prev, curr.value); + } + } + + if (state.tokens.next.identifier && !(opts.property && state.inES5())) { + switch (state.tokens.next.value) { + case "break": + case "case": + case "catch": + case "continue": + case "default": + case "do": + case "else": + case "finally": + case "for": + case "if": + case "in": + case "instanceof": + case "return": + case "switch": + case "throw": + case "try": + case "var": + case "let": + case "while": + case "with": + error("E024", state.tokens.next, state.tokens.next.value); + return false; + } + } + + if (state.tokens.next.type === "(punctuator)") { + switch (state.tokens.next.value) { + case "}": + case "]": + case ",": + case ")": + if (opts.allowTrailing) { + return true; + } + + error("E024", state.tokens.next, state.tokens.next.value); + return false; + } + } + return true; + } + function symbol(s, p) { + var x = state.syntax[s]; + if (!x || typeof x !== "object") { + state.syntax[s] = x = { + id: s, + lbp: p, + rbp: p, + value: s + }; + } + return x; + } + function delim(s) { + var x = symbol(s, 0); + x.delim = true; + return x; + } + function stmt(s, f) { + var x = delim(s); + x.identifier = x.reserved = true; + x.fud = f; + return x; + } + function blockstmt(s, f) { + var x = stmt(s, f); + x.block = true; + return x; + } + function reserveName(x) { + var c = x.id.charAt(0); + if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z")) { + x.identifier = x.reserved = true; + } + return x; + } + function prefix(s, f) { + var x = symbol(s, 150); + reserveName(x); + + x.nud = (typeof f === "function") ? f : function(context) { + this.arity = "unary"; + this.right = expression(context, 150); + + if (this.id === "++" || this.id === "--") { + if (state.option.plusplus) { + warning("W016", this, this.id); + } + + if (this.right) { + checkLeftSideAssign(context, this.right, this); + } + } + + return this; + }; + + return x; + } + function type(s, f) { + var x = symbol(s, 0); + x.type = s; + x.nud = f; + return x; + } + function reserve(name, func) { + var x = type(name, func); + x.identifier = true; + x.reserved = true; + return x; + } + function FutureReservedWord(name, meta) { + var x = type(name, state.syntax["(identifier)"].nud); + + meta = meta || {}; + meta.isFutureReservedWord = true; + + x.value = name; + x.identifier = true; + x.reserved = true; + x.meta = meta; + + return x; + } + function infix(s, f, p, w) { + var x = symbol(s, p); + reserveName(x); + x.infix = true; + x.led = function(context, left) { + if (!w) { + nobreaknonadjacent(state.tokens.prev, state.tokens.curr); + } + if ((s === "in" || s === "instanceof") && left.id === "!") { + warning("W018", left, "!"); + } + if (typeof f === "function") { + return f(context, left, this); + } else { + this.left = left; + this.right = expression(context, p); + return this; + } + }; + return x; + } + function application(s) { + var x = symbol(s, 42); + + x.infix = true; + x.led = function(context, left) { + nobreaknonadjacent(state.tokens.prev, state.tokens.curr); + + this.left = left; + this.right = doFunction(context, { type: "arrow", loneArg: left }); + return this; + }; + return x; + } + function relation(s, f) { + var x = symbol(s, 100); + + x.infix = true; + x.led = function(context, left) { + nobreaknonadjacent(state.tokens.prev, state.tokens.curr); + this.left = left; + var right = this.right = expression(context, 100); + + if (isIdentifier(left, "NaN") || isIdentifier(right, "NaN")) { + warning("W019", this); + } else if (f) { + f.apply(this, [context, left, right]); + } + + if (!left || !right) { + quit("E041", state.tokens.curr); + } + + if (left.id === "!") { + warning("W018", left, "!"); + } + + if (right.id === "!") { + warning("W018", right, "!"); + } + + return this; + }; + return x; + } + function beginsUnaryExpression(token) { + return token.arity === "unary" && token.id !== "++" && token.id !== "--"; + } + + var typeofValues = {}; + typeofValues.legacy = [ + "xml", + "unknown" + ]; + typeofValues.es3 = [ + "undefined", "boolean", "number", "string", "function", "object", + ]; + typeofValues.es3 = typeofValues.es3.concat(typeofValues.legacy); + typeofValues.es6 = typeofValues.es3.concat("symbol", "bigint"); + function isTypoTypeof(left, right, state) { + var values; + + if (state.option.notypeof) + return false; + + if (!left || !right) + return false; + + values = state.inES6() ? typeofValues.es6 : typeofValues.es3; + + if (right.type === "(identifier)" && right.value === "typeof" && left.type === "(string)") { + if (left.value === "bigint") { + if (!state.option.unstable.bigint) { + warning("W144", left, "BigInt", "bigint"); + } + + return false; + } + + return !_.includes(values, left.value); + } + + return false; + } + function isGlobalEval(left, state) { + var isGlobal = false; + if (left.type === "this" && state.funct["(context)"] === null) { + isGlobal = true; + } + else if (left.type === "(identifier)") { + if (state.option.node && left.value === "global") { + isGlobal = true; + } + + else if (state.option.browser && (left.value === "window" || left.value === "document")) { + isGlobal = true; + } + } + + return isGlobal; + } + function findNativePrototype(left) { + var natives = [ + "Array", "ArrayBuffer", "Boolean", "Collator", "DataView", "Date", + "DateTimeFormat", "Error", "EvalError", "Float32Array", "Float64Array", + "Function", "Infinity", "Intl", "Int16Array", "Int32Array", "Int8Array", + "Iterator", "Number", "NumberFormat", "Object", "RangeError", + "ReferenceError", "RegExp", "StopIteration", "String", "SyntaxError", + "TypeError", "Uint16Array", "Uint32Array", "Uint8Array", "Uint8ClampedArray", + "URIError" + ]; + + function walkPrototype(obj) { + if (typeof obj !== "object") return; + return obj.right === "prototype" ? obj : walkPrototype(obj.left); + } + + function walkNative(obj) { + while (!obj.identifier && typeof obj.left === "object") + obj = obj.left; + + if (obj.identifier && natives.indexOf(obj.value) >= 0 && + state.funct["(scope)"].isPredefined(obj.value)) { + return obj.value; + } + } + + var prototype = walkPrototype(left); + if (prototype) return walkNative(prototype); + } + function checkLeftSideAssign(context, left, assignToken, options) { + + var allowDestructuring = options && options.allowDestructuring; + + assignToken = assignToken || left; + + if (state.option.freeze) { + var nativeObject = findNativePrototype(left); + if (nativeObject) + warning("W121", left, nativeObject); + } + + if (left.identifier && !left.isMetaProperty) { + state.funct["(scope)"].block.reassign(left.value, left); + } + + if (left.id === ".") { + if (!left.left || left.left.value === "arguments" && !state.isStrict()) { + warning("W143", assignToken); + } + + state.nameStack.set(state.tokens.prev); + return true; + } else if (left.id === "{" || left.id === "[") { + if (!allowDestructuring || !left.destructAssign) { + if (left.id === "{" || !left.left) { + warning("E031", assignToken); + } else if (left.left.value === "arguments" && !state.isStrict()) { + warning("W143", assignToken); + } + } + + if (left.id === "[") { + state.nameStack.set(left.right); + } + + return true; + } else if (left.identifier && !isReserved(context, left) && !left.isMetaProperty) { + if (state.funct["(scope)"].bindingtype(left.value) === "exception") { + warning("W022", left); + } + + if (left.value === "eval" && state.isStrict()) { + error("E031", assignToken); + return false; + } else if (left.value === "arguments") { + if (!state.isStrict()) { + warning("W143", assignToken); + } else { + error("E031", assignToken); + return false; + } + } + state.nameStack.set(left); + return true; + } + + error("E031", assignToken); + + return false; + } + function assignop(s, f) { + var x = infix(s, typeof f === "function" ? f : function(context, left, that) { + that.left = left; + + checkLeftSideAssign(context, left, that, { allowDestructuring: true }); + + that.right = expression(context, 10); + + return that; + }, 20); + + x.exps = true; + x.assign = true; + + return x; + } + function bitwise(s, f, p) { + var x = symbol(s, p); + reserveName(x); + x.infix = true; + x.led = (typeof f === "function") ? f : function(context, left) { + if (state.option.bitwise) { + warning("W016", this, this.id); + } + this.left = left; + this.right = expression(context, p); + return this; + }; + return x; + } + function bitwiseassignop(s) { + symbol(s, 20).exps = true; + return infix(s, function(context, left, that) { + if (state.option.bitwise) { + warning("W016", that, that.id); + } + + checkLeftSideAssign(context, left, that); + + that.right = expression(context, 10); + + return that; + }, 20); + } + function nullSafeProperty(s) { + console.log("test " + s); + symbol(s, 20).exps = true; + return infix(s, function(context, left, that) { + if (state.option.bitwise) { + warning("W016", that, that.id); + } + + checkLeftSideAssign(context, left, that); + + that.right = expression(context, 10); + + return that; + }, 20); + } + function suffix(s) { + var x = symbol(s, 150); + + x.led = function(context, left) { + if (state.option.plusplus) { + warning("W016", this, this.id); + } + + checkLeftSideAssign(context, left, this); + + this.left = left; + return this; + }; + return x; + } + function optionalidentifier(context, prop, preserve) { + if (!state.tokens.next.identifier) { + return; + } + + if (!preserve) { + advance(); + } + + var curr = state.tokens.curr; + var val = state.tokens.curr.value; + + if (!isReserved(context, curr)) { + return val; + } + + if (prop) { + if (state.inES5()) { + return val; + } + } + + warning("W024", state.tokens.curr, state.tokens.curr.id); + return val; + } + function spreadrest(operation) { + if (!checkPunctuator(state.tokens.next, "...")) { + return false; + } + + if (!state.inES6(true)) { + warning("W119", state.tokens.next, operation + " operator", "6"); + } + advance(); + + if (checkPunctuator(state.tokens.next, "...")) { + warning("E024", state.tokens.next, "..."); + while (checkPunctuator(state.tokens.next, "...")) { + advance(); + } + } + + return true; + } + function identifier(context, prop) { + var i = optionalidentifier(context, prop, false); + if (i) { + return i; + } + + error("E030", state.tokens.next, state.tokens.next.value); + if (state.tokens.next.id !== ";") { + advance(); + } + } + function reachable(controlToken) { + var i = 0, t; + if (state.tokens.next.id !== ";" || controlToken.inBracelessBlock) { + return; + } + for (;;) { + do { + t = peek(i); + i += 1; + } while (t.id !== "(end)" && t.id === "(comment)"); + + if (t.reach) { + return; + } + if (t.id !== "(endline)") { + if (t.id === "function") { + if (state.option.latedef === true) { + warning("W026", t); + } + break; + } + + warning("W027", t, t.value, controlToken.value); + break; + } + } + } + function parseFinalSemicolon(stmt) { + if (state.tokens.next.id !== ";") { + if (state.tokens.next.isUnclosed) return advance(); + + var isSameLine = sameLine(state.tokens.curr, state.tokens.next) && + state.tokens.next.id !== "(end)"; + var blockEnd = checkPunctuator(state.tokens.next, "}"); + + if (isSameLine && !blockEnd && !(stmt.id === "do" && state.inES6(true))) { + errorAt("E058", state.tokens.curr.line, state.tokens.curr.character); + } else if (!state.option.asi) { + if (!(blockEnd && isSameLine && state.option.lastsemic)) { + warningAt("W033", state.tokens.curr.line, state.tokens.curr.character); + } + } + } else { + advance(";"); + } + } + function statement(context) { + var i = indent, r, t = state.tokens.next, hasOwnScope = false; + + context |= prodParams.initial; + + if (t.id === ";") { + advance(";"); + return; + } + var res = isReserved(context, t); + + if (res && t.meta && t.meta.isFutureReservedWord && !t.fud) { + warning("W024", t, t.id); + res = false; + } + + if (t.identifier && !res && peek().id === ":") { + advance(); + advance(":"); + + hasOwnScope = true; + state.funct["(scope)"].stack(); + state.funct["(scope)"].block.addLabel(t.value, { token: state.tokens.curr }); + + if (!state.tokens.next.labelled && state.tokens.next.value !== "{") { + warning("W028", state.tokens.next, t.value, state.tokens.next.value); + } + + t = state.tokens.next; + } + + if (t.id === "{") { + var iscase = (state.funct["(verb)"] === "case" && state.tokens.curr.value === ":"); + block(context, true, true, false, false, iscase); + + if (hasOwnScope) { + state.funct["(scope)"].unstack(); + } + + return; + } + + r = expression(context, 0); + + if (r && !(r.identifier && r.value === "function") && + !(r.type === "(punctuator)" && r.left && + r.left.identifier && r.left.value === "function")) { + if (!state.isStrict() && state.stmtMissingStrict()) { + warning("E007"); + } + } + + if (!t.block) { + if (!state.option.expr && (!r || !r.exps)) { + warning("W030", state.tokens.curr); + } else if (state.option.nonew && r && r.left && r.id === "(" && r.left.id === "new") { + warning("W031", t); + } + + parseFinalSemicolon(t); + } + + indent = i; + if (hasOwnScope) { + state.funct["(scope)"].unstack(); + } + return r; + } + function statements(context) { + var a = [], p; + + while (!state.tokens.next.reach && state.tokens.next.id !== "(end)") { + if (state.tokens.next.id === ";") { + p = peek(); + + if (!p || (p.id !== "(" && p.id !== "[")) { + warning("W032"); + } + + advance(";"); + } else { + a.push(statement(context)); + } + } + return a; + } + function directives() { + var current = state.tokens.next; + while (state.tokens.next.id === "(string)") { + var next = peekIgnoreEOL(); + if (!isEndOfExpr(0, current, next)) { + break; + } + current = next; + + advance(); + var directive = state.tokens.curr.value; + if (state.directive[directive] || + (directive === "use strict" && state.option.strict === "implied")) { + warning("W034", state.tokens.curr, directive); + } + if (directive === "use strict" && state.inES7() && + !state.funct["(global)"] && state.funct["(hasSimpleParams)"] === false) { + error("E065", state.tokens.curr); + } + state.directive[directive] = true; + + parseFinalSemicolon(current); + } + + if (state.isStrict()) { + state.option.undef = true; + } + } + function block(context, ordinary, stmt, isfunc, isfatarrow, iscase) { + var a, + b = inblock, + old_indent = indent, + m, + t, + d; + + inblock = ordinary; + + t = state.tokens.next; + + var metrics = state.funct["(metrics)"]; + metrics.nestedBlockDepth += 1; + metrics.verifyMaxNestedBlockDepthPerFunction(); + + if (state.tokens.next.id === "{") { + advance("{"); + state.funct["(scope)"].stack(); + + if (state.tokens.next.id !== "}") { + indent += state.option.indent; + while (!ordinary && state.tokens.next.from > indent) { + indent += state.option.indent; + } + + if (isfunc) { + m = {}; + for (d in state.directive) { + m[d] = state.directive[d]; + } + directives(); + + state.funct["(isStrict)"] = state.isStrict(); + + if (state.option.strict && state.funct["(context)"]["(global)"]) { + if (!m["use strict"] && !state.isStrict()) { + warning("E007"); + } + } + } + + a = statements(context); + + metrics.statementCount += a.length; + + indent -= state.option.indent; + } else if (isfunc) { + state.funct["(isStrict)"] = state.isStrict(); + } + + advance("}", t); + + if (isfunc) { + state.funct["(scope)"].validateParams(isfatarrow); + if (m) { + state.directive = m; + } + } + + state.funct["(scope)"].unstack(); + + indent = old_indent; + } else if (!ordinary) { + if (isfunc) { + state.funct["(scope)"].stack(); + + if (stmt && !isfatarrow && !state.inMoz()) { + error("W118", state.tokens.curr, "function closure expressions"); + } + + if (isfatarrow) { + state.funct["(scope)"].validateParams(true); + } + + var expr = expression(context, 10); + + if (state.option.noreturnawait && context & prodParams.async && + expr.identifier && expr.value === "await") { + warning("W146", expr); + } + + if (state.option.strict && state.funct["(context)"]["(global)"]) { + if (!state.isStrict()) { + warning("E007"); + } + } + + state.funct["(scope)"].unstack(); + } else { + error("E021", state.tokens.next, "{", state.tokens.next.value); + } + } else { + + state.funct["(scope)"].stack(); + + if (!stmt || state.option.curly) { + warning("W116", state.tokens.next, "{", state.tokens.next.value); + } + var supportsFnDecl = state.funct["(verb)"] === "if" || + state.tokens.curr.id === "else"; + + state.tokens.next.inBracelessBlock = true; + indent += state.option.indent; + a = [statement(context)]; + indent -= state.option.indent; + + if (a[0] && a[0].declaration && + !(supportsFnDecl && a[0].id === "function")) { + error("E048", a[0], a[0].id[0].toUpperCase() + a[0].id.slice(1)); + } + + state.funct["(scope)"].unstack(); + } + switch (state.funct["(verb)"]) { + case "break": + case "continue": + case "return": + case "throw": + if (iscase) { + break; + } + default: + state.funct["(verb)"] = null; + } + + inblock = b; + if (ordinary && state.option.noempty && (!a || a.length === 0)) { + warning("W035", state.tokens.prev); + } + metrics.nestedBlockDepth -= 1; + return a; + } + function countMember(m) { + if (membersOnly && typeof membersOnly[m] !== "boolean") { + warning("W036", state.tokens.curr, m); + } + if (typeof member[m] === "number") { + member[m] += 1; + } else { + member[m] = 1; + } + } + + type("(number)", function() { + if (state.tokens.next.id === ".") { + warning("W005", this); + } + + return this; + }); + + type("(string)", function() { + return this; + }); + + state.syntax["(identifier)"] = { + type: "(identifier)", + lbp: 0, + identifier: true, + + nud: function(context) { + var v = this.value; + var isLoneArrowParam = state.tokens.next.id === "=>"; + + if (isReserved(context, this)) { + warning("W024", this, v); + } else if (!isLoneArrowParam && !state.funct["(comparray)"].check(v)) { + state.funct["(scope)"].block.use(v, state.tokens.curr); + } + + return this; + }, + + led: function() { + error("E033", state.tokens.next, state.tokens.next.value); + } + }; + + var baseTemplateSyntax = { + identifier: false, + template: true, + }; + state.syntax["(template)"] = _.extend({ + lbp: 155, + type: "(template)", + nud: doTemplateLiteral, + led: doTemplateLiteral, + noSubst: false + }, baseTemplateSyntax); + + state.syntax["(template middle)"] = _.extend({ + lbp: 0, + type: "(template middle)", + noSubst: false + }, baseTemplateSyntax); + + state.syntax["(template tail)"] = _.extend({ + lbp: 0, + type: "(template tail)", + tail: true, + noSubst: false + }, baseTemplateSyntax); + + state.syntax["(no subst template)"] = _.extend({ + lbp: 155, + type: "(template)", + nud: doTemplateLiteral, + led: doTemplateLiteral, + noSubst: true, + tail: true // mark as tail, since it's always the last component + }, baseTemplateSyntax); + + type("(regexp)", function() { + return this; + }); + + delim("(endline)"); + (function(x) { + x.line = x.from = 0; + })(delim("(begin)")); + delim("(end)").reach = true; + delim("(error)").reach = true; + delim("}").reach = true; + delim(")"); + delim("]"); + delim("\"").reach = true; + delim("'").reach = true; + delim(";"); + delim(":").reach = true; + delim("#"); + + reserve("else"); + reserve("case").reach = true; + reserve("catch"); + reserve("default").reach = true; + reserve("finally"); + reserve("true", function() { return this; }); + reserve("false", function() { return this; }); + reserve("null", function() { return this; }); + reserve("this", function() { + if (state.isStrict() && !isMethod() && + !state.option.validthis && ((state.funct["(statement)"] && + state.funct["(name)"].charAt(0) > "Z") || state.funct["(global)"])) { + warning("W040", this); + } + + return this; + }); + reserve("super", function() { + superNud.call(state.tokens.curr, this); + + return this; + }); + + assignop("=", "assign"); + assignop("+=", "assignadd"); + assignop("-=", "assignsub"); + assignop("*=", "assignmult"); + assignop("/=", "assigndiv").nud = function() { + error("E014"); + }; + assignop("%=", "assignmod"); + assignop("**=", function(context, left, that) { + if (!state.inES7()) { + warning("W119", that, "Exponentiation operator", "7"); + } + + that.left = left; + + checkLeftSideAssign(context, left, that); + + that.right = expression(context, 10); + + return that; + }); + + bitwiseassignop("&="); + bitwiseassignop("|="); + bitwiseassignop("^="); + bitwiseassignop("<<="); + bitwiseassignop(">>="); + bitwiseassignop(">>>="); + + nullSafeProperty(".?"); + + infix(",", function(context, left, that) { + if (state.option.nocomma) { + warning("W127", that); + } + + that.left = left; + + if (checkComma()) { + that.right = expression(context, 10); + } else { + that.right = null; + } + + return that; + }, 10, true); + + infix("?", function(context, left, that) { + increaseComplexityCount(); + that.left = left; + that.right = expression(context & ~prodParams.noin, 10); + advance(":"); + expression(context, 10); + return that; + }, 30); + + var orPrecendence = 40; + infix("||", function(context, left, that) { + increaseComplexityCount(); + that.left = left; + that.right = expression(context, orPrecendence); + return that; + }, orPrecendence); + infix("&&", "and", 50); + infix("**", function(context, left, that) { + if (!state.inES7()) { + warning("W119", that, "Exponentiation operator", "7"); + } + if (!left.paren && beginsUnaryExpression(left)) { + error("E024", that, "**"); + } + + that.left = left; + that.right = expression(context, that.rbp); + return that; + }, 150); + state.syntax["**"].rbp = 140; + bitwise("|", "bitor", 70); + bitwise("^", "bitxor", 80); + bitwise("&", "bitand", 90); + relation("==", function(context, left, right) { + var eqnull = state.option.eqnull && + ((left && left.value) === "null" || (right && right.value) === "null"); + + switch (true) { + case !eqnull && state.option.eqeqeq: + this.from = this.character; + warning("W116", this, "===", "=="); + break; + case isTypoTypeof(right, left, state): + warning("W122", this, right.value); + break; + case isTypoTypeof(left, right, state): + warning("W122", this, left.value); + break; + } + + return this; + }); + relation("===", function(context, left, right) { + if (isTypoTypeof(right, left, state)) { + warning("W122", this, right.value); + } else if (isTypoTypeof(left, right, state)) { + warning("W122", this, left.value); + } + return this; + }); + relation("!=", function(context, left, right) { + var eqnull = state.option.eqnull && + ((left && left.value) === "null" || (right && right.value) === "null"); + + if (!eqnull && state.option.eqeqeq) { + this.from = this.character; + warning("W116", this, "!==", "!="); + } else if (isTypoTypeof(right, left, state)) { + warning("W122", this, right.value); + } else if (isTypoTypeof(left, right, state)) { + warning("W122", this, left.value); + } + return this; + }); + relation("!==", function(context, left, right) { + if (isTypoTypeof(right, left, state)) { + warning("W122", this, right.value); + } else if (isTypoTypeof(left, right, state)) { + warning("W122", this, left.value); + } + return this; + }); + relation("<"); + relation(">"); + relation("<="); + relation(">="); + bitwise("<<", "shiftleft", 120); + bitwise(">>", "shiftright", 120); + bitwise(">>>", "shiftrightunsigned", 120); + infix("in", "in", 120); + infix("instanceof", function(context, left, token) { + var right; + var scope = state.funct["(scope)"]; + token.left = left; + token.right = right = expression(context, 120); + if (!right) { + return token; + } + + if (right.id === "(number)" || + right.id === "(string)" || + right.value === "null" || + (right.value === "undefined" && !scope.has("undefined")) || + right.arity === "unary" || + right.id === "{" || + (right.id === "[" && !right.right) || + right.id === "(regexp)" || + (right.id === "(template)" && !right.tag)) { + error("E060"); + } + + if (right.id === "function") { + warning("W139"); + } + + return token; + }, 120); + infix("+", function(context, left, that) { + var next = state.tokens.next; + var right; + that.left = left; + that.right = right = expression(context, 130); + + if (left && right && left.id === "(string)" && right.id === "(string)") { + left.value += right.value; + left.character = right.character; + if (!state.option.scripturl && reg.javascriptURL.test(left.value)) { + warning("W050", left); + } + return left; + } + + if (next.id === "+" || next.id === "++") { + warning("W007", that.right); + } + + return that; + }, 130); + prefix("+", function(context) { + var next = state.tokens.next; + this.arity = "unary"; + this.right = expression(context, 150); + + if (next.id === "+" || next.id === "++") { + warning("W007", this.right); + } + + return this; + }); + infix("-", function(context, left, that) { + var next = state.tokens.next; + that.left = left; + that.right = expression(context, 130); + + if (next.id === "-" || next.id === "--") { + warning("W006", that.right); + } + + return that; + }, 130); + prefix("-", function(context) { + var next = state.tokens.next; + this.arity = "unary"; + this.right = expression(context, 150); + + if (next.id === "-" || next.id === "--") { + warning("W006", this.right); + } + + return this; + }); + infix("*", "mult", 140); + infix("/", "div", 140); + infix("%", "mod", 140); + + suffix("++"); + prefix("++", "preinc"); + state.syntax["++"].exps = true; + + suffix("--"); + prefix("--", "predec"); + state.syntax["--"].exps = true; + + prefix("delete", function(context) { + this.arity = "unary"; + var p = expression(context, 150); + if (!p) { + return this; + } + + if (p.id !== "." && p.id !== "[") { + warning("W051"); + } + this.first = p; + if (p.identifier && !state.isStrict()) { + p.forgiveUndef = true; + } + return this; + }).exps = true; + + prefix("~", function(context) { + if (state.option.bitwise) { + warning("W016", this, "~"); + } + this.arity = "unary"; + this.right = expression(context, 150); + return this; + }); + + infix("..."); + + prefix("!", function(context) { + this.arity = "unary"; + this.right = expression(context, 150); + + if (!this.right) { // '!' followed by nothing? Give up. + quit("E041", this); + } + + if (bang[this.right.id] === true) { + warning("W018", this, "!"); + } + return this; + }); + + prefix("typeof", function(context) { + this.arity = "unary"; + var p = expression(context, 150); + this.first = this.right = p; + + if (!p) { // 'typeof' followed by nothing? Give up. + quit("E041", this); + } + if (p.identifier) { + p.forgiveUndef = true; + } + return this; + }); + prefix("new", function(context) { + var mp = metaProperty(context, "target", function() { + if (!state.inES6(true)) { + warning("W119", state.tokens.prev, "new.target", "6"); + } + var inFunction, c = state.funct; + while (c) { + inFunction = !c["(global)"]; + if (!c["(arrow)"]) { break; } + c = c["(context)"]; + } + if (!inFunction) { + warning("W136", state.tokens.prev, "new.target"); + } + }); + if (mp) { return mp; } + + var c = expression(context, 155), i; + if (c && c.id !== "function") { + if (c.identifier) { + switch (c.value) { + case "Number": + case "String": + case "Boolean": + case "Math": + case "JSON": + warning("W053", state.tokens.prev, c.value); + break; + case "Symbol": + if (state.inES6()) { + warning("W053", state.tokens.prev, c.value); + } + break; + case "Function": + if (!state.option.evil) { + warning("W054"); + } + break; + case "Date": + case "RegExp": + case "this": + break; + default: + i = c.value.substr(0, 1); + if (state.option.newcap && (i < "A" || i > "Z") && + !state.funct["(scope)"].isPredefined(c.value)) { + warning("W055", state.tokens.curr); + } + } + } else { + if (c.id !== "." && c.id !== "[" && c.id !== "(") { + warning("W056", state.tokens.curr); + } + } + } else { + if (!state.option.supernew) + warning("W057", this); + } + if (state.tokens.next.id !== "(" && !state.option.supernew) { + warning("W058", state.tokens.curr, state.tokens.curr.value); + } + this.first = this.right = c; + return this; + }); + state.syntax["new"].exps = true; + blockstmt("class", function(context) { + var className, classNameToken; + var inexport = context & prodParams.export; + + if (!state.inES6()) { + warning("W104", state.tokens.curr, "class", "6"); + } + state.inClassBody = true; + if (state.tokens.next.identifier && state.tokens.next.value !== "extends") { + classNameToken = state.tokens.next; + className = classNameToken.value; + identifier(context); + state.funct["(scope)"].addbinding(className, { + type: "class", + initialized: false, + token: classNameToken + }); + } + if (state.tokens.next.value === "extends") { + advance("extends"); + expression(context, 0); + } + + if (classNameToken) { + this.name = className; + state.funct["(scope)"].initialize(className); + if (inexport) { + state.funct["(scope)"].setExported(className, classNameToken); + } + } + state.funct["(scope)"].stack(); + classBody(this, context); + return this; + }).exps = true; + prefix("class", function(context) { + var className, classNameToken; + + if (!state.inES6()) { + warning("W104", state.tokens.curr, "class", "6"); + } + state.inClassBody = true; + if (state.tokens.next.identifier && state.tokens.next.value !== "extends") { + classNameToken = state.tokens.next; + className = classNameToken.value; + identifier(context); + } + if (state.tokens.next.value === "extends") { + advance("extends"); + expression(context, 0); + } + + state.funct["(scope)"].stack(); + if (classNameToken) { + this.name = className; + state.funct["(scope)"].addbinding(className, { + type: "class", + initialized: true, + token: classNameToken + }); + state.funct["(scope)"].block.use(className, classNameToken); + } + + classBody(this, context); + return this; + }); + + function classBody(classToken, context) { + var props = Object.create(null); + var name, accessorType, token, isStatic, inGenerator, hasConstructor; + if (state.tokens.next.value === "{") { + advance("{"); + } else { + warning("W116", state.tokens.curr, "identifier", state.tokens.next.type); //? + advance(); + } + + while (state.tokens.next.value !== "}") { + isStatic = false; + inGenerator = false; + context &= ~prodParams.preAsync; + + if (state.tokens.next.value === "static" && + !checkPunctuator(peek(), "(")) { + isStatic = true; + advance(); + } + + if (state.tokens.next.value === "async") { + if (!checkPunctuator(peek(), "(")) { + context |= prodParams.preAsync; + advance(); + + nolinebreak(state.tokens.curr); + + if (checkPunctuator(state.tokens.next, "*")) { + inGenerator = true; + advance("*"); + + if (!state.inES9()) { + warning("W119", state.tokens.next, "async generators", "9"); + } + } + + if (!state.inES8()) { + warning("W119", state.tokens.curr, "async functions", "8"); + } + } + } + + if (state.tokens.next.value === "*") { + inGenerator = true; + advance(); + } + + token = state.tokens.next; + + if ((token.value === "set" || token.value === "get") && !checkPunctuator(peek(), "(")) { + if (inGenerator) { + error("E024", token, token.value); + } + accessorType = token.value; + advance(); + token = state.tokens.next; + + if (!isStatic && token.value === "constructor") { + error("E049", token, "class " + accessorType + "ter method", token.value); + } else if (isStatic && token.value === "prototype") { + error("E049", token, "static class " + accessorType + "ter method", token.value); + } + } else { + accessorType = null; + } + + switch (token.value) { + case ";": + warning("W032", token); + advance(); + break; + case "constructor": + if (isStatic) { + name = propertyName(context); + saveProperty(props, name, token, true, isStatic); + doMethod(classToken, context, name, inGenerator); + } else { + if (inGenerator || context & prodParams.preAsync) { + error("E024", token, token.value); + } else if (hasConstructor) { + error("E024", token, token.value); + } else { + hasConstructor = !accessorType && !isStatic; + } + advance(); + doMethod(classToken, context, state.nameStack.infer()); + } + break; + case "[": + name = computedPropertyName(context); + doMethod(classToken, context, name, inGenerator); + break; + default: + name = propertyName(context); + if (name === undefined) { + error("E024", token, token.value); + advance(); + break; + } + + if (accessorType) { + saveAccessor(accessorType, props, name, token, true, isStatic); + name = state.nameStack.infer(); + } else { + if (isStatic && name === "prototype") { + error("E049", token, "static class method", name); + } + + saveProperty(props, name, token, true, isStatic); + } + + doMethod(classToken, context, name, inGenerator); + break; + } + } + advance("}"); + checkProperties(props); + + state.inClassBody = false; + state.funct["(scope)"].unstack(); + } + + function doMethod(classToken, context, name, generator) { + if (generator) { + if (!state.inES6()) { + warning("W119", state.tokens.curr, "function*", "6"); + } + } + + if (state.tokens.next.value !== "(") { + error("E054", state.tokens.next, state.tokens.next.value); + advance(); + if (state.tokens.next.value === "{") { + advance(); + if (state.tokens.next.value === "}") { + warning("W116", state.tokens.next, "(", state.tokens.next.value); + advance(); + identifier(context); + advance(); + } + return; + } else { + while (state.tokens.next.value !== "(") { + advance(); + } + } + } + + doFunction(context, { name: name, + type: generator ? "generator" : null, + isMethod: true, + statement: classToken }); + } + + prefix("void").exps = true; + + infix(".", function(context, left, that) { + var m = identifier(context, true); + + if (typeof m === "string") { + countMember(m); + } + + that.left = left; + that.right = m; + + if (m && m === "hasOwnProperty" && state.tokens.next.value === "=") { + warning("W001"); + } + + if (left && left.value === "arguments" && (m === "callee" || m === "caller")) { + if (state.option.noarg) + warning("W059", left, m); + else if (state.isStrict()) + error("E008"); + } else if (!state.option.evil && left && left.value === "document" && + (m === "write" || m === "writeln")) { + warning("W060", left); + } + + if (!state.option.evil && (m === "eval" || m === "execScript")) { + if (isGlobalEval(left, state)) { + warning("W061"); + } + } + + return that; + }, 160, true); + + infix("(", function(context, left, that) { + if (state.option.immed && left && !left.immed && left.id === "function") { + warning("W062"); + } + + if (state.option.asi && checkPunctuators(state.tokens.prev, [")", "]"]) && + !sameLine(state.tokens.prev, state.tokens.curr)) { + warning("W014", state.tokens.curr, state.tokens.curr.id); + } + + var n = 0; + var p = []; + + if (left) { + if (left.type === "(identifier)") { + if (left.value.match(/^[A-Z]([A-Z0-9_$]*[a-z][A-Za-z0-9_$]*)?$/)) { + if ("Array Number String Boolean Date Object Error Symbol".indexOf(left.value) === -1) { + if (left.value === "Math") { + warning("W063", left); + } else if (state.option.newcap) { + warning("W064", left); + } + } + } + } + } + + if (state.tokens.next.id !== ")") { + for (;;) { + spreadrest("spread"); + + p[p.length] = expression(context, 10); + n += 1; + if (state.tokens.next.id !== ",") { + break; + } + advance(","); + checkComma({ allowTrailing: true }); + + if (state.tokens.next.id === ")") { + if (!state.inES8()) { + warning("W119", state.tokens.curr, "Trailing comma in arguments lists", "8"); + } + + break; + } + } + } + + advance(")"); + + if (typeof left === "object") { + if (!state.inES5() && left.value === "parseInt" && n === 1) { + warning("W065", state.tokens.curr); + } + if (!state.option.evil) { + if (left.value === "eval" || left.value === "Function" || + left.value === "execScript") { + warning("W061", left); + } else if (p[0] && p[0].id === "(string)" && + (left.value === "setTimeout" || + left.value === "setInterval")) { + warning("W066", left); + addEvalCode(left, p[0]); + } else if (p[0] && p[0].id === "(string)" && + left.value === "." && + left.left.value === "window" && + (left.right === "setTimeout" || + left.right === "setInterval")) { + warning("W066", left); + addEvalCode(left, p[0]); + } + } + if (!left.identifier && left.id !== "." && left.id !== "[" && left.id !== "=>" && + left.id !== "(" && left.id !== "&&" && left.id !== "||" && left.id !== "?" && + left.id !== "async" && !(state.inES6() && left["(name)"])) { + warning("W067", that); + } + } + + that.left = left; + return that; + }, 155, true).exps = true; + + function peekThroughParens(parens) { + var pn = state.tokens.next; + var i = -1; + var pn1; + + do { + if (pn.value === "(") { + parens += 1; + } else if (pn.value === ")") { + parens -= 1; + } + + i += 1; + pn1 = pn; + pn = peek(i); + } while (!(parens === 0 && pn1.value === ")") && pn.type !== "(end)"); + + return pn; + } + + prefix("(", function(context, rbp) { + var ret, triggerFnExpr, first, last; + var opening = state.tokens.curr; + var preceeding = state.tokens.prev; + var isNecessary = !state.option.singleGroups; + var pn = peekThroughParens(1); + + if (state.tokens.next.id === "function") { + triggerFnExpr = state.tokens.next.immed = true; + } + if (pn.value === "=>") { + pn.funct = doFunction(context, { type: "arrow", parsedOpening: true }); + return pn; + } + if (state.tokens.next.id === ")") { + advance(")"); + return; + } + + ret = expression(context, 0); + + advance(")", this); + + if (!ret) { + return; + } + + ret.paren = true; + + if (state.option.immed && ret && ret.id === "function") { + if (state.tokens.next.id !== "(" && + state.tokens.next.id !== "." && state.tokens.next.id !== "[") { + warning("W068", this); + } + } + + if (ret.id === ",") { + first = ret.left; + while (first.id === ",") { + first = first.left; + } + + last = ret.right; + } else { + first = last = ret; + + if (!isNecessary) { + if (!triggerFnExpr) { + triggerFnExpr = ret.id === "async"; + } + + isNecessary = + (opening.beginsStmt && (ret.id === "{" || triggerFnExpr)) || + (triggerFnExpr && + (!isEndOfExpr() || state.tokens.prev.id !== "}")) || + (ret.id === "=>" && !isEndOfExpr()) || + (ret.id === "{" && preceeding.id === "=>") || + (beginsUnaryExpression(ret) && state.tokens.next.id === "**") || + (ret.type === "(number)" && + checkPunctuator(pn, ".") && /^\d+$/.test(ret.value)) || + (opening.beginsStmt && ret.id === "=" && ret.left.id === "{"); + } + } + if (!isNecessary && (isOperator(first) || first !== last)) { + isNecessary = + (rbp > first.lbp) || + (rbp > 0 && rbp === first.lbp) || + (!isEndOfExpr() && last.rbp < state.tokens.next.lbp); + } + + if (!isNecessary) { + warning("W126", opening); + } + + return ret; + }); + + application("=>"); + + infix("[", function(context, left, that) { + var e, s, canUseDot; + + if (state.option.asi && checkPunctuators(state.tokens.prev, [")", "]"]) && + !sameLine(state.tokens.prev, state.tokens.curr)) { + warning("W014", state.tokens.curr, state.tokens.curr.id); + } + + e = expression(context & ~prodParams.noin, 10); + + if (e && e.type === "(string)") { + if (!state.option.evil && (e.value === "eval" || e.value === "execScript")) { + if (isGlobalEval(left, state)) { + warning("W061"); + } + } + + countMember(e.value); + if (!state.option.sub && reg.identifier.test(e.value)) { + s = state.syntax[e.value]; + + if (s) { + canUseDot = !isReserved(context, s); + } else { + canUseDot = e.value !== "eval" && e.value !== "arguments"; + } + + if (canUseDot) { + warning("W069", state.tokens.prev, e.value); + } + } + } + advance("]", that); + + if (e && e.value === "hasOwnProperty" && state.tokens.next.value === "=") { + warning("W001"); + } + + that.left = left; + that.right = e; + return that; + }, 160, true); + + function comprehensiveArrayExpression(context) { + var res = {}; + res.exps = true; + state.funct["(comparray)"].stack(); + var reversed = false; + if (state.tokens.next.value !== "for") { + reversed = true; + if (!state.inMoz()) { + warning("W116", state.tokens.next, "for", state.tokens.next.value); + } + state.funct["(comparray)"].setState("use"); + res.right = expression(context, 10); + } + + advance("for"); + if (state.tokens.next.value === "each") { + advance("each"); + if (!state.inMoz()) { + warning("W118", state.tokens.curr, "for each"); + } + } + advance("("); + state.funct["(comparray)"].setState("define"); + res.left = expression(context, 130); + if (_.includes(["in", "of"], state.tokens.next.value)) { + advance(); + } else { + error("E045", state.tokens.curr); + } + state.funct["(comparray)"].setState("generate"); + expression(context, 10); + + advance(")"); + if (state.tokens.next.value === "if") { + advance("if"); + advance("("); + state.funct["(comparray)"].setState("filter"); + expression(context, 10); + advance(")"); + } + + if (!reversed) { + state.funct["(comparray)"].setState("use"); + res.right = expression(context, 10); + } + + advance("]"); + state.funct["(comparray)"].unstack(); + return res; + } + + prefix("[", function(context) { + var blocktype = lookupBlockType(); + if (blocktype.isCompArray) { + if (!state.option.esnext && !state.inMoz()) { + warning("W118", state.tokens.curr, "array comprehension"); + } + return comprehensiveArrayExpression(context); + } else if (blocktype.isDestAssign) { + this.destructAssign = destructuringPattern(context, { + openingParsed: true, + assignment: true + }); + return this; + } + var b = !sameLine(state.tokens.curr, state.tokens.next); + this.first = []; + if (b) { + indent += state.option.indent; + if (state.tokens.next.from === indent + state.option.indent) { + indent += state.option.indent; + } + } + while (state.tokens.next.id !== "(end)") { + while (state.tokens.next.id === ",") { + if (!state.option.elision) { + if (!state.inES5()) { + warning("W070"); + } else { + warning("W128"); + do { + advance(","); + } while (state.tokens.next.id === ","); + continue; + } + } + advance(","); + } + + if (state.tokens.next.id === "]") { + break; + } + + spreadrest("spread"); + + this.first.push(expression(context, 10)); + if (state.tokens.next.id === ",") { + advance(","); + checkComma({ allowTrailing: true }); + if (state.tokens.next.id === "]" && !state.inES5()) { + warning("W070", state.tokens.curr); + break; + } + } else { + if (state.option.trailingcomma && state.inES5()) { + warningAt("W140", state.tokens.curr.line, state.tokens.curr.character); + } + break; + } + } + if (b) { + indent -= state.option.indent; + } + advance("]", this); + return this; + }); + + + function isMethod() { + return !!state.funct["(method)"]; + } + + function propertyName(context, preserveOrToken) { + var id; + var preserve = true; + if (typeof preserveOrToken === "object") { + id = preserveOrToken; + } else { + preserve = preserveOrToken; + id = optionalidentifier(context, true, preserve); + } + + if (!id) { + if (state.tokens.next.id === "(string)") { + id = state.tokens.next.value; + if (!preserve) { + advance(); + } + } else if (state.tokens.next.id === "(number)") { + id = state.tokens.next.value.toString(); + if (!preserve) { + advance(); + } + } + } else if (typeof id === "object") { + if (id.id === "(string)" || id.id === "(identifier)") id = id.value; + else if (id.id === "(number)") id = id.value.toString(); + } + + if (id === "hasOwnProperty") { + warning("W001"); + } + + return id; + } + function functionparams(context, options) { + var next; + var paramsIds = []; + var ident; + var tokens = []; + var t; + var pastDefault = false; + var pastRest = false; + var arity = 0; + var loneArg = options && options.loneArg; + var hasDestructuring = false; + + if (loneArg && loneArg.identifier === true) { + state.funct["(scope)"].addParam(loneArg.value, loneArg); + return { arity: 1, params: [ loneArg.value ], isSimple: true }; + } + + next = state.tokens.next; + + if (!options || !options.parsedOpening) { + advance("("); + } + + if (state.tokens.next.id === ")") { + advance(")"); + return; + } + + function addParam(addParamArgs) { + state.funct["(scope)"].addParam.apply(state.funct["(scope)"], addParamArgs); + } + + for (;;) { + arity++; + var currentParams = []; + + if (_.includes(["{", "["], state.tokens.next.id)) { + hasDestructuring = true; + tokens = destructuringPattern(context); + for (t in tokens) { + t = tokens[t]; + if (t.id) { + paramsIds.push(t.id); + currentParams.push([t.id, t.token]); + } + } + } else { + pastRest = spreadrest("rest"); + ident = identifier(context); + + if (ident) { + paramsIds.push(ident); + currentParams.push([ident, state.tokens.curr]); + } else { + while (!checkPunctuators(state.tokens.next, [",", ")"])) advance(); + } + } + if (pastDefault) { + if (state.tokens.next.id !== "=") { + error("W138", state.tokens.curr); + } + } + if (state.tokens.next.id === "=") { + if (!state.inES6()) { + warning("W119", state.tokens.next, "default parameters", "6"); + } + + if (pastRest) { + error("E062", state.tokens.next); + } + + advance("="); + pastDefault = true; + expression(context, 10); + } + currentParams.forEach(addParam); + if (state.tokens.next.id === ",") { + if (pastRest) { + warning("W131", state.tokens.next); + } + advance(","); + checkComma({ allowTrailing: true }); + } + + if (state.tokens.next.id === ")") { + if (state.tokens.curr.id === "," && !state.inES8()) { + warning("W119", state.tokens.curr, "Trailing comma in function parameters", "8"); + } + + advance(")", next); + return { + arity: arity, + params: paramsIds, + isSimple: !hasDestructuring && !pastRest && !pastDefault + }; + } + } + } + function functor(name, token, overwrites) { + var funct = { + "(name)" : name, + "(breakage)" : 0, + "(loopage)" : 0, + "(isStrict)" : "unknown", + + "(global)" : false, + + "(line)" : null, + "(character)" : null, + "(metrics)" : null, + "(statement)" : null, + "(context)" : null, + "(scope)" : null, + "(comparray)" : null, + "(yielded)" : null, + "(arrow)" : null, + "(async)" : null, + "(params)" : null + }; + + if (token) { + _.extend(funct, { + "(line)" : token.line, + "(character)": token.character, + "(metrics)" : createMetrics(token) + }); + } + + _.extend(funct, overwrites); + + if (funct["(context)"]) { + funct["(scope)"] = funct["(context)"]["(scope)"]; + funct["(comparray)"] = funct["(context)"]["(comparray)"]; + } + + return funct; + } + function hasParsedCode(funct) { + return funct["(global)"] && !funct["(verb)"]; + } + function doTemplateLiteral(context, leftOrRbp) { + var ctx = this.context; + var noSubst = this.noSubst; + var depth = this.depth; + var left = typeof leftOrRbp === "number" ? null : leftOrRbp; + + if (!noSubst) { + while (!end()) { + if (!state.tokens.next.template || state.tokens.next.depth > depth) { + expression(context, 0); // should probably have different rbp? + } else { + advance(); + } + } + } + + return { + id: "(template)", + type: "(template)", + tag: left + }; + + function end() { + if (state.tokens.curr.template && state.tokens.curr.tail && + state.tokens.curr.context === ctx) { + return true; + } + var complete = (state.tokens.next.template && state.tokens.next.tail && + state.tokens.next.context === ctx); + if (complete) advance(); + return complete || state.tokens.next.isUnclosed; + } + } + function doFunction(context, options) { + var f, token, name, statement, classExprBinding, isGenerator, isArrow, + isMethod, ignoreLoopFunc; + var oldOption = state.option; + var oldIgnored = state.ignored; + var isAsync = context & prodParams.preAsync; + + if (options) { + name = options.name; + statement = options.statement; + classExprBinding = options.classExprBinding; + isGenerator = options.type === "generator"; + isArrow = options.type === "arrow"; + isMethod = options.isMethod; + ignoreLoopFunc = options.ignoreLoopFunc; + } + + context &= ~prodParams.noin; + context &= ~prodParams.tryClause; + + if (isAsync) { + context |= prodParams.async; + } else { + context &= ~prodParams.async; + } + + if (isGenerator) { + context |= prodParams.yield; + } else if (!isArrow) { + context &= ~prodParams.yield; + } + context &= ~prodParams.preAsync; + + state.option = Object.create(state.option); + state.ignored = Object.create(state.ignored); + + state.funct = functor(name || state.nameStack.infer(), state.tokens.next, { + "(statement)": statement, + "(context)": state.funct, + "(arrow)": isArrow, + "(method)": isMethod, + "(async)": isAsync + }); + + f = state.funct; + token = state.tokens.curr; + + functions.push(state.funct); + state.funct["(scope)"].stack("functionouter"); + var internallyAccessibleName = !isMethod && (name || classExprBinding); + if (internallyAccessibleName) { + state.funct["(scope)"].block.add(internallyAccessibleName, + classExprBinding ? "class" : "function", state.tokens.curr, false); + } + + if (!isArrow) { + state.funct["(scope)"].funct.add("arguments", "var", token, false); + } + state.funct["(scope)"].stack("functionparams"); + + var paramsInfo = functionparams(context, options); + + if (paramsInfo) { + state.funct["(params)"] = paramsInfo.params; + state.funct["(hasSimpleParams)"] = paramsInfo.isSimple; + state.funct["(metrics)"].arity = paramsInfo.arity; + state.funct["(metrics)"].verifyMaxParametersPerFunction(); + } else { + state.funct["(params)"] = []; + state.funct["(metrics)"].arity = 0; + state.funct["(hasSimpleParams)"] = true; + } + + if (isArrow) { + context &= ~prodParams.yield; + + if (!state.inES6(true)) { + warning("W119", state.tokens.curr, "arrow function syntax (=>)", "6"); + } + + if (!options.loneArg) { + advance("=>"); + } + } + + block(context, false, true, true, isArrow); + + if (!state.option.noyield && isGenerator && !state.funct["(yielded)"]) { + warning("W124", state.tokens.curr); + } + + state.funct["(metrics)"].verifyMaxStatementsPerFunction(); + state.funct["(metrics)"].verifyMaxComplexityPerFunction(); + state.funct["(unusedOption)"] = state.option.unused; + state.option = oldOption; + state.ignored = oldIgnored; + state.funct["(last)"] = state.tokens.curr.line; + state.funct["(lastcharacter)"] = state.tokens.curr.character; + state.funct["(scope)"].unstack(); // also does usage and label checks + state.funct["(scope)"].unstack(); + + state.funct = state.funct["(context)"]; + + if (!ignoreLoopFunc && !state.option.loopfunc && state.funct["(loopage)"]) { + if (f["(outerMutables)"]) { + warning("W083", token, f["(outerMutables)"].join(", ")); + } + } + + return f; + } + + function createMetrics(functionStartToken) { + return { + statementCount: 0, + nestedBlockDepth: -1, + ComplexityCount: 1, + arity: 0, + + verifyMaxStatementsPerFunction: function() { + if (state.option.maxstatements && + this.statementCount > state.option.maxstatements) { + warning("W071", functionStartToken, this.statementCount); + } + }, + + verifyMaxParametersPerFunction: function() { + if (_.isNumber(state.option.maxparams) && + this.arity > state.option.maxparams) { + warning("W072", functionStartToken, this.arity); + } + }, + + verifyMaxNestedBlockDepthPerFunction: function() { + if (state.option.maxdepth && + this.nestedBlockDepth > 0 && + this.nestedBlockDepth === state.option.maxdepth + 1) { + warning("W073", null, this.nestedBlockDepth); + } + }, + + verifyMaxComplexityPerFunction: function() { + var max = state.option.maxcomplexity; + var cc = this.ComplexityCount; + if (max && cc > max) { + warning("W074", functionStartToken, cc); + } + } + }; + } + + function increaseComplexityCount() { + state.funct["(metrics)"].ComplexityCount += 1; + } + + function checkCondAssignment(token) { + if (!token || token.paren) { + return; + } + + if (token.id === ",") { + checkCondAssignment(token.right); + return; + } + + switch (token.id) { + case "=": + case "+=": + case "-=": + case "*=": + case "%=": + case "&=": + case "|=": + case "^=": + case "/=": + if (!state.option.boss) { + warning("W084", token); + } + } + } + function checkProperties(props) { + if (state.inES5()) { + for (var name in props) { + if (props[name] && props[name].setterToken && !props[name].getterToken && + !props[name].static) { + warning("W078", props[name].setterToken); + } + } + } + } + + function metaProperty(context, name, c) { + if (checkPunctuator(state.tokens.next, ".")) { + var left = state.tokens.curr.id; + advance("."); + var id = identifier(context); + state.tokens.curr.isMetaProperty = true; + if (name !== id) { + error("E057", state.tokens.prev, left, id); + } else { + c(); + } + return state.tokens.curr; + } + } + (function(x) { + x.nud = function(context) { + var b, f, i, params, t, isGeneratorMethod = false, nextVal; + var props = Object.create(null); // All properties, including accessors + var isAsyncMethod = false; + + b = !sameLine(state.tokens.curr, state.tokens.next); + if (b) { + indent += state.option.indent; + if (state.tokens.next.from === indent + state.option.indent) { + indent += state.option.indent; + } + } + + var blocktype = lookupBlockType(); + if (blocktype.isDestAssign) { + this.destructAssign = destructuringPattern(context, { + openingParsed: true, + assignment: true + }); + return this; + } + state.inObjectBody = true; + for (;;) { + if (state.tokens.next.id === "}") { + break; + } + + nextVal = state.tokens.next.value; + if (state.tokens.next.identifier && + (peekIgnoreEOL().id === "," || peekIgnoreEOL().id === "}")) { + if (!state.inES6()) { + warning("W104", state.tokens.next, "object short notation", "6"); + } + i = propertyName(context, true); + saveProperty(props, i, state.tokens.next); + + expression(context, 10); + + } else if (peek().id !== ":" && (nextVal === "get" || nextVal === "set")) { + advance(nextVal); + + if (!state.inES5()) { + error("E034"); + } + + if (state.tokens.next.id === "[") { + i = computedPropertyName(context); + } else { + i = propertyName(context); + if (!i && !state.inES6()) { + error("E035"); + } + } + if (i) { + saveAccessor(nextVal, props, i, state.tokens.curr); + } + + t = state.tokens.next; + f = doFunction(context, { isMethod: true }); + params = f["(params)"]; + if (nextVal === "get" && i && params.length) { + warning("W076", t, params[0], i); + } else if (nextVal === "set" && i && f["(metrics)"].arity !== 1) { + warning("W077", t, i); + } + + } else if (spreadrest("spread")) { + if (!state.inES9()) { + warning("W119", state.tokens.next, "object spread property", "9"); + } + + expression(context, 10); + } else { + if (state.tokens.next.id === "async" && !checkPunctuators(peek(), ["(", ":"])) { + if (!state.inES8()) { + warning("W119", state.tokens.next, "async functions", "8"); + } + + isAsyncMethod = true; + advance(); + + nolinebreak(state.tokens.curr); + } else { + isAsyncMethod = false; + } + + if (state.tokens.next.value === "*" && state.tokens.next.type === "(punctuator)") { + if (isAsyncMethod && !state.inES9()) { + warning("W119", state.tokens.next, "async generators", "9"); + } else if (!state.inES6()) { + warning("W104", state.tokens.next, "generator functions", "6"); + } + + advance("*"); + isGeneratorMethod = true; + } else { + isGeneratorMethod = false; + } + + if (state.tokens.next.id === "[") { + i = computedPropertyName(context); + state.nameStack.set(i); + } else { + state.nameStack.set(state.tokens.next); + i = propertyName(context); + saveProperty(props, i, state.tokens.next); + + if (typeof i !== "string") { + break; + } + } + + if (state.tokens.next.value === "(") { + if (!state.inES6()) { + warning("W104", state.tokens.curr, "concise methods", "6"); + } + + doFunction(isAsyncMethod ? context | prodParams.preAsync : context, { + isMethod: true, + type: isGeneratorMethod ? "generator" : null + }); + } else { + advance(":"); + expression(context, 10); + } + } + + countMember(i); + + if (state.tokens.next.id === ",") { + advance(","); + checkComma({ allowTrailing: true, property: true }); + if (state.tokens.next.id === ",") { + warning("W070", state.tokens.curr); + } else if (state.tokens.next.id === "}" && !state.inES5()) { + warning("W070", state.tokens.curr); + } + } else { + if (state.option.trailingcomma && state.inES5()) { + warningAt("W140", state.tokens.curr.line, state.tokens.curr.character); + } + break; + } + } + if (b) { + indent -= state.option.indent; + } + advance("}", this); + + checkProperties(props); + state.inObjectBody = false; + + return this; + }; + x.fud = function() { + error("E036", state.tokens.curr); + }; + }(delim("{"))); + + function destructuringPattern(context, options) { + var isAssignment = options && options.assignment; + + context &= ~prodParams.noin; + + if (!state.inES6()) { + warning("W104", state.tokens.curr, + isAssignment ? "destructuring assignment" : "destructuring binding", "6"); + } + + return destructuringPatternRecursive(context, options); + } + + function destructuringPatternRecursive(context, options) { + var ids, idx; + var identifiers = []; + var openingParsed = options && options.openingParsed; + var isAssignment = options && options.assignment; + var recursiveOptions = isAssignment ? { assignment: isAssignment } : null; + var firstToken = openingParsed ? state.tokens.curr : state.tokens.next; + + var nextInnerDE = function() { + var ident; + if (checkPunctuators(state.tokens.next, ["[", "{"])) { + ids = destructuringPatternRecursive(context, recursiveOptions); + for (idx = 0; idx < ids.length; idx++) { + identifiers.push({ id: ids[idx].id, token: ids[idx].token }); + } + } else if (checkPunctuator(state.tokens.next, ",")) { + identifiers.push({ id: null, token: state.tokens.curr }); + } else if (checkPunctuator(state.tokens.next, "(")) { + advance("("); + nextInnerDE(); + advance(")"); + } else { + if (isAssignment) { + var assignTarget = expression(context, 20); + if (assignTarget) { + checkLeftSideAssign(context, assignTarget); + if (assignTarget.identifier) { + ident = assignTarget.value; + } + } + } else { + ident = identifier(context); + } + if (ident) { + identifiers.push({ id: ident, token: state.tokens.curr }); + } + } + }; + + var assignmentProperty = function(context) { + var id, expr; + + if (checkPunctuator(state.tokens.next, "[")) { + advance("["); + expression(context, 10); + advance("]"); + advance(":"); + nextInnerDE(); + } else if (state.tokens.next.id === "(string)" || + state.tokens.next.id === "(number)") { + advance(); + advance(":"); + nextInnerDE(); + } else { + var isRest = spreadrest("rest"); + + if (isRest) { + if (!state.inES9()) { + warning("W119", state.tokens.next, "object rest property", "9"); + } + if (state.tokens.next.type === "(identifier)") { + id = identifier(context); + } else { + expr = expression(context, 10); + error("E030", expr, expr.value); + } + } else { + id = identifier(context); + } + + if (!isRest && checkPunctuator(state.tokens.next, ":")) { + advance(":"); + nextInnerDE(); + } else if (id) { + if (isAssignment) { + checkLeftSideAssign(context, state.tokens.curr); + } + identifiers.push({ id: id, token: state.tokens.curr }); + } + + if (isRest && checkPunctuator(state.tokens.next, ",")) { + warning("W130", state.tokens.next); + } + } + }; + + var id, value; + if (checkPunctuator(firstToken, "[")) { + if (!openingParsed) { + advance("["); + } + if (checkPunctuator(state.tokens.next, "]")) { + warning("W137", state.tokens.curr); + } + var element_after_rest = false; + while (!checkPunctuator(state.tokens.next, "]")) { + var isRest = spreadrest("rest"); + + nextInnerDE(); + + if (isRest && !element_after_rest && + checkPunctuator(state.tokens.next, ",")) { + warning("W130", state.tokens.next); + element_after_rest = true; + } + if (!isRest && checkPunctuator(state.tokens.next, "=")) { + if (checkPunctuator(state.tokens.prev, "...")) { + advance("]"); + } else { + advance("="); + } + id = state.tokens.prev; + value = expression(context, 10); + if (value && value.identifier && value.value === "undefined") { + warning("W080", id, id.value); + } + } + if (!checkPunctuator(state.tokens.next, "]")) { + advance(","); + } + } + advance("]"); + } else if (checkPunctuator(firstToken, "{")) { + + if (!openingParsed) { + advance("{"); + } + if (checkPunctuator(state.tokens.next, "}")) { + warning("W137", state.tokens.curr); + } + while (!checkPunctuator(state.tokens.next, "}")) { + assignmentProperty(context); + if (checkPunctuator(state.tokens.next, "=")) { + advance("="); + id = state.tokens.prev; + value = expression(context, 10); + if (value && value.identifier && value.value === "undefined") { + warning("W080", id, id.value); + } + } + if (!checkPunctuator(state.tokens.next, "}")) { + advance(","); + if (checkPunctuator(state.tokens.next, "}")) { + break; + } + } + } + advance("}"); + } + return identifiers; + } + + function destructuringPatternMatch(tokens, value) { + var first = value.first; + + if (!first) + return; + + _.zip(tokens, Array.isArray(first) ? first : [ first ]).forEach(function(val) { + var token = val[0]; + var value = val[1]; + + if (token && value) + token.first = value; + else if (token && token.first && !value) + warning("W080", token.first, token.first.value); + }); + } + + function blockVariableStatement(type, statement, context) { + + var noin = context & prodParams.noin; + var inexport = context & prodParams.export; + var isLet = type === "let"; + var isConst = type === "const"; + var tokens, lone, value, letblock; + + if (!state.inES6()) { + warning("W104", state.tokens.curr, type, "6"); + } + + if (isLet && isMozillaLet()) { + advance("("); + state.funct["(scope)"].stack(); + letblock = true; + statement.declaration = false; + } + + statement.first = []; + for (;;) { + var names = []; + if (_.includes(["{", "["], state.tokens.next.value)) { + tokens = destructuringPattern(context); + lone = false; + } else { + tokens = [ { id: identifier(context), token: state.tokens.curr } ]; + lone = true; + } + if (!noin && isConst && state.tokens.next.id !== "=") { + warning("E012", state.tokens.curr, state.tokens.curr.value); + } + + for (var t in tokens) { + if (tokens.hasOwnProperty(t)) { + t = tokens[t]; + if (t.id === "let") { + warning("W024", t.token, t.id); + } + + if (state.funct["(scope)"].block.isGlobal()) { + if (predefined[t.id] === false) { + warning("W079", t.token, t.id); + } + } + if (t.id) { + state.funct["(scope)"].addbinding(t.id, { + type: type, + token: t.token }); + names.push(t.token); + } + } + } + + if (state.tokens.next.id === "=") { + statement.hasInitializer = true; + + advance("="); + if (!noin && peek(0).id === "=" && state.tokens.next.identifier) { + warning("W120", state.tokens.next, state.tokens.next.value); + } + var id = state.tokens.prev; + value = expression(context, 10); + if (value) { + if (value.identifier && value.value === "undefined") { + warning("W080", id, id.value); + } + if (!lone) { + destructuringPatternMatch(names, value); + } + } + } + if (state.tokens.next.value !== "in" && state.tokens.next.value !== "of") { + for (t in tokens) { + if (tokens.hasOwnProperty(t)) { + t = tokens[t]; + state.funct["(scope)"].initialize(t.id); + + if (lone && inexport) { + state.funct["(scope)"].setExported(t.token.value, t.token); + } + } + } + } + + statement.first = statement.first.concat(names); + + if (state.tokens.next.id !== ",") { + break; + } + + statement.hasComma = true; + advance(","); + checkComma(); + } + if (letblock) { + advance(")"); + block(context, true, true); + statement.block = true; + state.funct["(scope)"].unstack(); + } + + return statement; + } + + var conststatement = stmt("const", function(context) { + return blockVariableStatement("const", this, context); + }); + conststatement.exps = true; + conststatement.declaration = true; + function isMozillaLet() { + return state.tokens.next.id === "(" && state.inMoz(); + } + var letstatement = stmt("let", function(context) { + return blockVariableStatement("let", this, context); + }); + letstatement.nud = function(context, rbp) { + if (isMozillaLet()) { + state.funct["(scope)"].stack(); + advance("("); + state.tokens.prev.fud(context); + advance(")"); + expression(context, rbp); + state.funct["(scope)"].unstack(); + } else { + this.exps = false; + return state.syntax["(identifier)"].nud.apply(this, arguments); + } + }; + letstatement.meta = { es5: true, isFutureReservedWord: false, strictOnly: true }; + letstatement.exps = true; + letstatement.declaration = true; + letstatement.useFud = function(context) { + var next = state.tokens.next; + var nextIsBindingName; + + if (this.line !== next.line && !state.inES6()) { + return false; + } + nextIsBindingName = next.identifier && (!isReserved(context, next) || + next.id === "let"); + + return nextIsBindingName || checkPunctuators(next, ["{", "["]) || + isMozillaLet(); + }; + + var varstatement = stmt("var", function(context) { + var noin = context & prodParams.noin; + var inexport = context & prodParams.export; + var tokens, lone, value, id; + + this.first = []; + for (;;) { + var names = []; + if (_.includes(["{", "["], state.tokens.next.value)) { + tokens = destructuringPattern(context); + lone = false; + } else { + tokens = []; + id = identifier(context); + + if (id) { + tokens.push({ id: id, token: state.tokens.curr }); + } + + lone = true; + } + + if (state.option.varstmt) { + warning("W132", this); + } + + + for (var t in tokens) { + if (tokens.hasOwnProperty(t)) { + t = tokens[t]; + if (state.funct["(global)"] && !state.impliedClosure()) { + if (predefined[t.id] === false) { + warning("W079", t.token, t.id); + } else if (state.option.futurehostile === false) { + if ((!state.inES5() && vars.ecmaIdentifiers[5][t.id] === false) || + (!state.inES6() && vars.ecmaIdentifiers[6][t.id] === false)) { + warning("W129", t.token, t.id); + } + } + } + if (t.id) { + state.funct["(scope)"].addbinding(t.id, { + type: "var", + token: t.token }); + + if (lone && inexport) { + state.funct["(scope)"].setExported(t.id, t.token); + } + names.push(t.token); + } + } + } + + if (state.tokens.next.id === "=") { + this.hasInitializer = true; + + state.nameStack.set(state.tokens.curr); + + advance("="); + if (peek(0).id === "=" && state.tokens.next.identifier) { + if (!noin && + !state.funct["(params)"] || + state.funct["(params)"].indexOf(state.tokens.next.value) === -1) { + warning("W120", state.tokens.next, state.tokens.next.value); + } + } + id = state.tokens.prev; + value = expression(context, 10); + if (value) { + if (!state.funct["(loopage)"] && value.identifier && + value.value === "undefined") { + warning("W080", id, id.value); + } + if (!lone) { + destructuringPatternMatch(names, value); + } + } + } + + this.first = this.first.concat(names); + + if (state.tokens.next.id !== ",") { + break; + } + this.hasComma = true; + advance(","); + checkComma(); + } + + return this; + }); + varstatement.exps = true; + + blockstmt("function", function(context) { + var inexport = context & prodParams.export; + var generator = false; + var isAsync = context & prodParams.preAsync; + var labelType = ""; + + if (isAsync) { + labelType = "async "; + } + + if (state.tokens.next.value === "*") { + if (isAsync && !state.inES9()) { + warning("W119", state.tokens.prev, "async generators", "9"); + } else if (!isAsync && !state.inES6(true)) { + warning("W119", state.tokens.next, "function*", "6"); + } + + advance("*"); + labelType += "generator "; + generator = true; + } + + labelType += "function"; + + if (inblock) { + warning("W082", state.tokens.curr); + } + var nameToken = optionalidentifier(context) ? state.tokens.curr : null; + + if (!nameToken) { + if (!inexport) { + warning("W025"); + } + } else { + state.funct["(scope)"].addbinding(nameToken.value, { + type: labelType, + token: state.tokens.curr, + initialized: true }); + + if (inexport) { + state.funct["(scope)"].setExported(nameToken.value, state.tokens.prev); + } + } + + var f = doFunction(context, { + name: nameToken && nameToken.value, + statement: this, + type: generator ? "generator" : null, + ignoreLoopFunc: inblock // a declaration may already have warned + }); + var enablesStrictMode = f["(isStrict)"] && !state.isStrict(); + if (nameToken && (f["(name)"] === "arguments" || f["(name)"] === "eval") && + enablesStrictMode) { + error("E008", nameToken); + } + + if (state.tokens.next.id === "(" && state.tokens.next.line === state.tokens.curr.line) { + error("E039"); + } + return this; + }).declaration = true; + + prefix("function", function(context) { + var generator = false; + var isAsync = context & prodParams.preAsync; + + if (state.tokens.next.value === "*") { + if (isAsync && !state.inES9()) { + warning("W119", state.tokens.prev, "async generators", "9"); + } else if (!isAsync && !state.inES6(true)) { + warning("W119", state.tokens.curr, "function*", "6"); + } + + advance("*"); + generator = true; + } + var nameToken = optionalidentifier(isAsync ? context | prodParams.async : context) ? + state.tokens.curr : null; + + var f = doFunction(context, { + name: nameToken && nameToken.value, + type: generator ? "generator" : null + }); + + if (generator && nameToken && nameToken.value === "yield") { + error("E024", nameToken, "yield"); + } + + if (nameToken && (f["(name)"] === "arguments" || f["(name)"] === "eval") && + f["(isStrict)"]) { + error("E008", nameToken); + } + + return this; + }); + + blockstmt("if", function(context) { + var t = state.tokens.next; + increaseComplexityCount(); + advance("("); + var expr = expression(context, 0); + + if (!expr) { + quit("E041", this); + } + + checkCondAssignment(expr); + var forinifcheck = null; + if (state.option.forin && state.forinifcheckneeded) { + state.forinifcheckneeded = false; // We only need to analyze the first if inside the loop + forinifcheck = state.forinifchecks[state.forinifchecks.length - 1]; + if (expr.type === "(punctuator)" && expr.value === "!") { + forinifcheck.type = "(negative)"; + } else { + forinifcheck.type = "(positive)"; + } + } + + advance(")", t); + var s = block(context, true, true); + if (forinifcheck && forinifcheck.type === "(negative)") { + if (s && s[0] && s[0].type === "(identifier)" && s[0].value === "continue") { + forinifcheck.type = "(negative-with-continue)"; + } + } + + if (state.tokens.next.id === "else") { + advance("else"); + if (state.tokens.next.id === "if" || state.tokens.next.id === "switch") { + statement(context); + } else { + block(context, true, true); + } + } + return this; + }); + + blockstmt("try", function(context) { + var b; + var hasParameter = false; + + function catchParameter() { + advance("("); + + if (checkPunctuators(state.tokens.next, ["[", "{"])) { + var tokens = destructuringPattern(context); + _.each(tokens, function(token) { + if (token.id) { + state.funct["(scope)"].addParam(token.id, token, "exception"); + } + }); + } else if (state.tokens.next.type !== "(identifier)") { + warning("E030", state.tokens.next, state.tokens.next.value); + } else { + state.funct["(scope)"].addParam(identifier(context), state.tokens.curr, "exception"); + } + + if (state.tokens.next.value === "if") { + if (!state.inMoz()) { + warning("W118", state.tokens.curr, "catch filter"); + } + advance("if"); + expression(context, 0); + } + + advance(")"); + } + + block(context | prodParams.tryClause, true); + + while (state.tokens.next.id === "catch") { + increaseComplexityCount(); + if (b && (!state.inMoz())) { + warning("W118", state.tokens.next, "multiple catch blocks"); + } + advance("catch"); + if (state.tokens.next.id !== "{") { + state.funct["(scope)"].stack("catchparams"); + hasParameter = true; + catchParameter(); + } else if (!state.inES10()) { + warning("W119", state.tokens.curr, "optional catch binding", "10"); + } + block(context, false); + + if (hasParameter) { + state.funct["(scope)"].unstack(); + hasParameter = false; + } + b = true; + } + + if (state.tokens.next.id === "finally") { + advance("finally"); + block(context, true); + return; + } + + if (!b) { + error("E021", state.tokens.next, "catch", state.tokens.next.value); + } + + return this; + }); + + blockstmt("while", function(context) { + var t = state.tokens.next; + state.funct["(breakage)"] += 1; + state.funct["(loopage)"] += 1; + increaseComplexityCount(); + advance("("); + checkCondAssignment(expression(context, 0)); + advance(")", t); + block(context, true, true); + state.funct["(breakage)"] -= 1; + state.funct["(loopage)"] -= 1; + return this; + }).labelled = true; + + blockstmt("with", function(context) { + var t = state.tokens.next; + if (state.isStrict()) { + error("E010", state.tokens.curr); + } else if (!state.option.withstmt) { + warning("W085", state.tokens.curr); + } + + advance("("); + expression(context, 0); + advance(")", t); + block(context, true, true); + + return this; + }); + + blockstmt("switch", function(context) { + var t = state.tokens.next; + var g = false; + var noindent = false; + var seenCase = false; + + state.funct["(breakage)"] += 1; + advance("("); + checkCondAssignment(expression(context, 0)); + advance(")", t); + t = state.tokens.next; + advance("{"); + state.funct["(scope)"].stack(); + + if (state.tokens.next.from === indent) + noindent = true; + + if (!noindent) + indent += state.option.indent; + + for (;;) { + switch (state.tokens.next.id) { + case "case": + switch (state.funct["(verb)"]) { + case "yield": + case "break": + case "case": + case "continue": + case "return": + case "switch": + case "throw": + break; + case "default": + if (state.option.leanswitch) { + warning("W145", state.tokens.next); + } + + break; + default: + if (!state.tokens.curr.caseFallsThrough) { + warning("W086", state.tokens.curr, "case"); + } + } + + advance("case"); + expression(context, 0); + seenCase = true; + increaseComplexityCount(); + g = true; + advance(":"); + state.funct["(verb)"] = "case"; + break; + case "default": + switch (state.funct["(verb)"]) { + case "yield": + case "break": + case "continue": + case "return": + case "throw": + break; + case "case": + if (state.option.leanswitch) { + warning("W145", state.tokens.curr); + } + + break; + default: + if (seenCase && !state.tokens.curr.caseFallsThrough) { + warning("W086", state.tokens.curr, "default"); + } + } + + advance("default"); + g = true; + advance(":"); + state.funct["(verb)"] = "default"; + break; + case "}": + if (!noindent) + indent -= state.option.indent; + + advance("}", t); + state.funct["(scope)"].unstack(); + state.funct["(breakage)"] -= 1; + state.funct["(verb)"] = undefined; + return; + case "(end)": + error("E023", state.tokens.next, "}"); + return; + default: + indent += state.option.indent; + if (g) { + switch (state.tokens.curr.id) { + case ",": + error("E040"); + return; + case ":": + g = false; + statements(context); + break; + default: + error("E025", state.tokens.curr); + return; + } + } else { + if (state.tokens.curr.id === ":") { + advance(":"); + error("E024", state.tokens.curr, ":"); + statements(context); + } else { + error("E021", state.tokens.next, "case", state.tokens.next.value); + return; + } + } + indent -= state.option.indent; + } + } + }).labelled = true; + + stmt("debugger", function() { + if (!state.option.debug) { + warning("W087", this); + } + return this; + }).exps = true; + + (function() { + var x = stmt("do", function(context) { + state.funct["(breakage)"] += 1; + state.funct["(loopage)"] += 1; + increaseComplexityCount(); + + this.first = block(context, true, true); + advance("while"); + var t = state.tokens.next; + advance("("); + checkCondAssignment(expression(context, 0)); + advance(")", t); + state.funct["(breakage)"] -= 1; + state.funct["(loopage)"] -= 1; + return this; + }); + x.labelled = true; + x.exps = true; + }()); + + blockstmt("for", function(context) { + var s, t = state.tokens.next; + var letscope = false; + var isAsync = false; + var foreachtok = null; + + if (t.value === "each") { + foreachtok = t; + advance("each"); + if (!state.inMoz()) { + warning("W118", state.tokens.curr, "for each"); + } + } + + if (state.tokens.next.identifier && state.tokens.next.value === "await") { + advance("await"); + isAsync = true; + + if (!(context & prodParams.async)) { + error("E024", state.tokens.curr, "await"); + } else if (!state.inES9()) { + warning("W119", state.tokens.curr, "asynchronous iteration", "9"); + } + } + + increaseComplexityCount(); + advance("("); + var nextop; // contains the token of the "in" or "of" operator + var comma; // First comma punctuator at level 0 + var initializer; // First initializer at level 0 + var bindingPower; + var targets; + var target; + var decl; + var afterNext = peek(); + + var headContext = context | prodParams.noin; + + if (state.tokens.next.id === "var") { + advance("var"); + decl = state.tokens.curr.fud(headContext); + comma = decl.hasComma ? decl : null; + initializer = decl.hasInitializer ? decl : null; + } else if (state.tokens.next.id === "const" || + (state.tokens.next.id === "let" && + ((afterNext.identifier && afterNext.id !== "in") || + checkPunctuators(afterNext, ["{", "["])))) { + advance(state.tokens.next.id); + letscope = true; + state.funct["(scope)"].stack(); + decl = state.tokens.curr.fud(headContext); + comma = decl.hasComma ? decl : null; + initializer = decl.hasInitializer ? decl : null; + } else if (!checkPunctuator(state.tokens.next, ";")) { + targets = []; + + while (state.tokens.next.value !== "in" && + state.tokens.next.value !== "of" && + !checkPunctuator(state.tokens.next, ";")) { + + if (checkPunctuators(state.tokens.next, ["{", "["])) { + destructuringPattern(headContext, { assignment: true }) + .forEach(function(elem) { + this.push(elem.token); + }, targets); + if (checkPunctuator(state.tokens.next, "=")) { + advance("="); + initializer = state.tokens.curr; + expression(headContext, 10); + } + } else { + target = expression(headContext, 10); + + if (target) { + if (target.type === "(identifier)") { + targets.push(target); + } else if (checkPunctuator(target, "=")) { + initializer = target; + targets.push(target); + } + } + } + + if (checkPunctuator(state.tokens.next, ",")) { + advance(","); + + if (!comma) { + comma = state.tokens.curr; + } + } + } + if (!initializer && !comma) { + targets.forEach(function(token) { + if (!state.funct["(scope)"].has(token.value)) { + warning("W088", token, token.value); + } + }); + } + } + + nextop = state.tokens.next; + + if (isAsync && nextop.value !== "of") { + error("E066", nextop); + } + if (_.includes(["in", "of"], nextop.value)) { + if (nextop.value === "of") { + bindingPower = 20; + + if (!state.inES6()) { + warning("W104", nextop, "for of", "6"); + } + } else { + bindingPower = 0; + } + if (comma) { + error("W133", comma, nextop.value, "more than one ForBinding"); + } + if (initializer) { + error("W133", initializer, nextop.value, "initializer is forbidden"); + } + if (target && !comma && !initializer) { + checkLeftSideAssign(context, target, nextop); + } + + advance(nextop.value); + expression(context, bindingPower); + advance(")", t); + + if (nextop.value === "in" && state.option.forin) { + state.forinifcheckneeded = true; + + if (state.forinifchecks === undefined) { + state.forinifchecks = []; + } + state.forinifchecks.push({ + type: "(none)" + }); + } + + state.funct["(breakage)"] += 1; + state.funct["(loopage)"] += 1; + + s = block(context, true, true); + + if (nextop.value === "in" && state.option.forin) { + if (state.forinifchecks && state.forinifchecks.length > 0) { + var check = state.forinifchecks.pop(); + + if (// No if statement or not the first statement in loop body + s && s.length > 0 && (typeof s[0] !== "object" || s[0].value !== "if") || + check.type === "(positive)" && s.length > 1 || + check.type === "(negative)") { + warning("W089", this); + } + } + state.forinifcheckneeded = false; + } + + state.funct["(breakage)"] -= 1; + state.funct["(loopage)"] -= 1; + + } else { + if (foreachtok) { + error("E045", foreachtok); + } + nolinebreak(state.tokens.curr); + advance(";"); + if (decl) { + if (decl.value === "const" && !decl.hasInitializer) { + warning("E012", decl, decl.first[0].value); + } + + decl.first.forEach(function(token) { + state.funct["(scope)"].initialize(token.value); + }); + } + state.funct["(loopage)"] += 1; + if (state.tokens.next.id !== ";") { + checkCondAssignment(expression(context, 0)); + } + nolinebreak(state.tokens.curr); + advance(";"); + if (state.tokens.next.id === ";") { + error("E021", state.tokens.next, ")", ";"); + } + if (state.tokens.next.id !== ")") { + for (;;) { + expression(context, 0); + if (state.tokens.next.id !== ",") { + break; + } + advance(","); + checkComma(); + } + } + advance(")", t); + state.funct["(breakage)"] += 1; + block(context, true, true); + state.funct["(breakage)"] -= 1; + state.funct["(loopage)"] -= 1; + } + if (letscope) { + state.funct["(scope)"].unstack(); + } + return this; + }).labelled = true; + + blockstmt("foreach", function(context) { + var t = state.tokens.next; + var letscope = false; + + increaseComplexityCount(); + advance("("); + var nextop; // contains the token of the "in" or "of" operator + var comma; // First comma punctuator at level 0 + var initializer; // First initializer at level 0 + var bindingPower; + var targets; + var target; + var headContext = context | prodParams.noin; + + if (!checkPunctuator(state.tokens.next, ";")) { + targets = []; + + while (state.tokens.next.value !== ":" && + !checkPunctuator(state.tokens.next, ";")) { + + if (checkPunctuators(state.tokens.next, ["{", "["])) { + destructuringPattern(headContext, { assignment: true }) + .forEach(function(elem) { + this.push(elem.token); + }, targets); + if (checkPunctuator(state.tokens.next, "=")) { + advance("="); + initializer = state.tokens.curr; + expression(headContext, 10); + } + } else { + target = expression(headContext, 10); + + if (target) { + if (target.type === "(identifier)") { + targets.push(target); + } else if (checkPunctuator(target, "=")) { + initializer = target; + targets.push(target); + } + } + } + + if (checkPunctuator(state.tokens.next, ",")) { + advance(","); + + if (!comma) { + comma = state.tokens.curr; + } + } + } + } + + nextop = state.tokens.next; + + if (_.includes([":"], nextop.value)) { + bindingPower = 0; + if (comma) { + error("W133", comma, nextop.value, "more than one ForBinding"); + } + if (initializer) { + error("W133", initializer, nextop.value, "initializer is forbidden"); + } + if (target && !comma && !initializer) { + checkLeftSideAssign(context, target, nextop); + } + + advance(nextop.value); + expression(context, bindingPower); + advance(")", t); + + state.funct["(breakage)"] += 1; + state.funct["(loopage)"] += 1; + + state.funct["(breakage)"] -= 1; + state.funct["(loopage)"] -= 1; + } else { + nolinebreak(state.tokens.curr); + advance(";"); + state.funct["(loopage)"] += 1; + if (state.tokens.next.id !== ";") { + checkCondAssignment(expression(context, 0)); + } + nolinebreak(state.tokens.curr); + advance(";"); + if (state.tokens.next.id === ";") { + error("E021", state.tokens.next, ")", ";"); + } + if (state.tokens.next.id !== ")") { + for (;;) { + expression(context, 0); + if (state.tokens.next.id !== ",") { + break; + } + advance(","); + checkComma(); + } + } + advance(")", t); + state.funct["(breakage)"] += 1; + block(context, true, true); + state.funct["(breakage)"] -= 1; + state.funct["(loopage)"] -= 1; + } + if (letscope) { + state.funct["(scope)"].unstack(); + } + return this; + }).labelled = true; + + + stmt("break", function() { + var v = state.tokens.next.value; + + if (!state.option.asi) + nolinebreak(this); + + if (state.tokens.next.identifier && + sameLine(state.tokens.curr, state.tokens.next)) { + if (!state.funct["(scope)"].funct.hasLabel(v)) { + warning("W090", state.tokens.next, v); + } + this.first = state.tokens.next; + advance(); + } else { + if (state.funct["(breakage)"] === 0) + warning("W052", state.tokens.next, this.value); + } + + reachable(this); + + return this; + }).exps = true; + + + stmt("continue", function() { + var v = state.tokens.next.value; + + if (state.funct["(breakage)"] === 0 || !state.funct["(loopage)"]) { + warning("W052", state.tokens.next, this.value); + } + + if (!state.option.asi) + nolinebreak(this); + + if (state.tokens.next.identifier) { + if (sameLine(state.tokens.curr, state.tokens.next)) { + if (!state.funct["(scope)"].funct.hasLabel(v)) { + warning("W090", state.tokens.next, v); + } + this.first = state.tokens.next; + advance(); + } + } + + reachable(this); + + return this; + }).exps = true; + + + stmt("return", function(context) { + if (sameLine(this, state.tokens.next)) { + if (state.tokens.next.id !== ";" && !state.tokens.next.reach) { + this.first = expression(context, 0); + + if (this.first && + this.first.type === "(punctuator)" && this.first.value === "=" && + !this.first.paren && !state.option.boss) { + warning("W093", this.first); + } + + if (state.option.noreturnawait && context & prodParams.async && + !(context & prodParams.tryClause) && + this.first.identifier && this.first.value === "await") { + warning("W146", this.first); + } + } + } else { + if (state.tokens.next.type === "(punctuator)" && + ["[", "{", "+", "-"].indexOf(state.tokens.next.value) > -1) { + nolinebreak(this); // always warn (Line breaking error) + } + } + + reachable(this); + + return this; + }).exps = true; + + prefix("await", function(context) { + if (context & prodParams.async) { + if (!state.funct["(params)"]) { + error("E024", this, "await"); + } + + expression(context, 10); + return this; + } else { + this.exps = false; + return state.syntax["(identifier)"].nud.apply(this, arguments); + } + }).exps = true; + + (function(asyncSymbol) { + asyncSymbol.meta = { es5: true, isFutureReservedWord: true, strictOnly: true }; + asyncSymbol.isFunc = function() { + var next = state.tokens.next; + var afterParens; + + if (this.line !== next.line) { + return false; + } + + if (next.id === "function") { + return true; + } + + if (next.id === "(") { + afterParens = peekThroughParens(0); + + return afterParens.id === "=>"; + } + + if (next.identifier) { + return peek().id === "=>"; + } + + return false; + }; + asyncSymbol.useFud = asyncSymbol.isFunc; + asyncSymbol.fud = function(context) { + if (!state.inES8()) { + warning("W119", this, "async functions", "8"); + } + context |= prodParams.preAsync; + context |= prodParams.initial; + this.func = expression(context, 0); + this.block = this.func.block; + this.exps = this.func.exps; + return this; + }; + asyncSymbol.exps = true; + delete asyncSymbol.reserved; + }(prefix("async", function(context, rbp) { + if (this.isFunc(context)) { + if (!state.inES8()) { + warning("W119", this, "async functions", "8"); + } + + context |= prodParams.preAsync; + this.func = expression(context, rbp); + this.identifier = false; + return this; + } + + this.exps = false; + return state.syntax["(identifier)"].nud.apply(this, arguments); + }))); + + (function(yieldSymbol) { + yieldSymbol.rbp = yieldSymbol.lbp = 25; + yieldSymbol.exps = true; + })(prefix("yield", function(context) { + if (state.inMoz()) { + return mozYield.call(this, context); + } + + if (!(context & prodParams.yield)) { + this.exps = false; + return state.syntax["(identifier)"].nud.apply(this, arguments); + } + + var prev = state.tokens.prev; + if (!state.funct["(params)"]) { + error("E024", this, "yield"); + } + + if (!this.beginsStmt && prev.lbp > 30 && !checkPunctuators(prev, ["("])) { + error("E061", this); + } + + if (!state.inES6()) { + warning("W104", state.tokens.curr, "yield", "6"); + } + state.funct["(yielded)"] = true; + + if (state.tokens.next.value === "*") { + advance("*"); + } + if (state.tokens.curr.value === "*" || sameLine(state.tokens.curr, state.tokens.next)) { + if (state.tokens.next.nud) { + + nobreaknonadjacent(state.tokens.curr, state.tokens.next); + this.first = expression(context, 10); + + if (this.first.type === "(punctuator)" && this.first.value === "=" && + !this.first.paren && !state.option.boss) { + warning("W093", this.first); + } + } else if (state.tokens.next.led) { + if (state.tokens.next.id !== ",") { + error("W017", state.tokens.next); + } + } + } + + return this; + })); + var mozYield = function(context) { + var prev = state.tokens.prev; + if (state.inES6(true) && !(context & prodParams.yield)) { + error("E046", state.tokens.curr, "yield"); + } + state.funct["(yielded)"] = true; + var delegatingYield = false; + + if (state.tokens.next.value === "*") { + delegatingYield = true; + advance("*"); + } + + if (sameLine(this, state.tokens.next)) { + if (delegatingYield || + (state.tokens.next.id !== ";" && !state.option.asi && + !state.tokens.next.reach && state.tokens.next.nud)) { + + nobreaknonadjacent(state.tokens.curr, state.tokens.next); + this.first = expression(context, 10); + + if (this.first.type === "(punctuator)" && this.first.value === "=" && + !this.first.paren && !state.option.boss) { + warning("W093", this.first); + } + } + if (state.tokens.next.id !== ")" && + (prev.lbp > 30 || (!prev.assign && !isEndOfExpr()))) { + error("E050", this); + } + } else if (!state.option.asi) { + nolinebreak(this); // always warn (Line breaking error) + } + return this; + }; + + stmt("throw", function(context) { + nolinebreak(this); + this.first = expression(context, 20); + + reachable(this); + + return this; + }).exps = true; + + stmt("import", function(context) { + if (!state.funct["(scope)"].block.isGlobal()) { + error("E053", state.tokens.curr, "Import"); + } + + if (!state.inES6()) { + warning("W119", state.tokens.curr, "import", "6"); + } + + if (state.tokens.next.type === "(string)") { + advance("(string)"); + return this; + } + + if (state.tokens.next.identifier) { + this.name = identifier(context); + state.funct["(scope)"].addbinding(this.name, { + type: "import", + initialized: true, + token: state.tokens.curr }); + + if (state.tokens.next.value === ",") { + advance(","); + } else { + advance("from"); + advance("(string)"); + return this; + } + } + + if (state.tokens.next.id === "*") { + advance("*"); + advance("as"); + if (state.tokens.next.identifier) { + this.name = identifier(context); + state.funct["(scope)"].addbinding(this.name, { + type: "import", + initialized: true, + token: state.tokens.curr }); + } + } else { + advance("{"); + for (;;) { + if (state.tokens.next.value === "}") { + advance("}"); + break; + } + var importName; + if (state.tokens.next.type === "default") { + importName = "default"; + advance("default"); + } else { + importName = identifier(context); + } + if (state.tokens.next.value === "as") { + advance("as"); + importName = identifier(context); + } + state.funct["(scope)"].addbinding(importName, { + type: "import", + initialized: true, + token: state.tokens.curr }); + + if (state.tokens.next.value === ",") { + advance(","); + } else if (state.tokens.next.value === "}") { + advance("}"); + break; + } else { + error("E024", state.tokens.next, state.tokens.next.value); + break; + } + } + } + advance("from"); + advance("(string)"); + + return this; + }).exps = true; + + stmt("export", function(context) { + var ok = true; + var token; + var identifier; + var moduleSpecifier; + context = context | prodParams.export; + + if (!state.inES6()) { + warning("W119", state.tokens.curr, "export", "6"); + ok = false; + } + + if (!state.funct["(scope)"].block.isGlobal()) { + error("E053", state.tokens.curr, "Export"); + ok = false; + } + + if (state.tokens.next.value === "*") { + advance("*"); + advance("from"); + advance("(string)"); + return this; + } + + if (state.tokens.next.type === "default") { + state.nameStack.set(state.tokens.next); + + advance("default"); + var exportType = state.tokens.next.id; + if (exportType === "function") { + this.block = true; + advance("function"); + state.syntax["function"].fud(context); + } else if (exportType === "async" && peek().id === "function") { + this.block = true; + advance("async"); + advance("function"); + state.syntax["function"].fud(context | prodParams.preAsync); + } else if (exportType === "class") { + this.block = true; + advance("class"); + state.syntax["class"].fud(context); + } else { + token = expression(context, 10); + if (token.identifier) { + identifier = token.value; + state.funct["(scope)"].setExported(identifier, token); + } + } + return this; + } + if (state.tokens.next.value === "{") { + advance("{"); + var exportedTokens = []; + while (!checkPunctuator(state.tokens.next, "}")) { + if (!state.tokens.next.identifier) { + error("E030", state.tokens.next, state.tokens.next.value); + } + advance(); + + exportedTokens.push(state.tokens.curr); + + if (state.tokens.next.value === "as") { + advance("as"); + if (!state.tokens.next.identifier) { + error("E030", state.tokens.next, state.tokens.next.value); + } + advance(); + } + + if (!checkPunctuator(state.tokens.next, "}")) { + advance(","); + } + } + advance("}"); + if (state.tokens.next.value === "from") { + advance("from"); + moduleSpecifier = state.tokens.next; + advance("(string)"); + } else if (ok) { + exportedTokens.forEach(function(token) { + state.funct["(scope)"].setExported(token.value, token); + }); + } + + if (exportedTokens.length === 0) { + if (moduleSpecifier) { + warning("W142", this, "export", moduleSpecifier.value); + } else { + warning("W141", this, "export"); + } + } + + return this; + } else if (state.tokens.next.id === "var") { + advance("var"); + state.tokens.curr.fud(context); + } else if (state.tokens.next.id === "let") { + advance("let"); + state.tokens.curr.fud(context); + } else if (state.tokens.next.id === "const") { + advance("const"); + state.tokens.curr.fud(context); + } else if (state.tokens.next.id === "function") { + this.block = true; + advance("function"); + state.syntax["function"].fud(context); + } else if (state.tokens.next.id === "async" && peek().id === "function") { + this.block = true; + advance("async"); + advance("function"); + state.syntax["function"].fud(context | prodParams.preAsync); + } else if (state.tokens.next.id === "class") { + this.block = true; + advance("class"); + state.syntax["class"].fud(context); + } else { + error("E024", state.tokens.next, state.tokens.next.value); + } + + return this; + }).exps = true; + function supportsSuper(type, funct) { + if (type === "call" && funct["(async)"]) { + return false; + } + + if (type === "property" && funct["(method)"]) { + return true; + } + + if (type === "call" && funct["(statement)"] && + funct["(statement)"].id === "class") { + return true; + } + + if (funct["(arrow)"]) { + return supportsSuper(type, funct["(context)"]); + } + + return false; + } + + var superNud = function() { + var next = state.tokens.next; + + if (checkPunctuators(next, ["[", "."])) { + if (!supportsSuper("property", state.funct)) { + error("E063", this); + } + } else if (checkPunctuator(next, "(")) { + if (!supportsSuper("call", state.funct)) { + error("E064", this); + } + } else { + error("E024", next, next.value || next.id); + } + + return this; + }; + + FutureReservedWord("abstract"); + FutureReservedWord("boolean"); + FutureReservedWord("byte"); + FutureReservedWord("char"); + FutureReservedWord("double"); + FutureReservedWord("enum", { es5: true }); + FutureReservedWord("export", { es5: true }); + FutureReservedWord("extends", { es5: true }); + FutureReservedWord("final"); + FutureReservedWord("float"); + FutureReservedWord("goto"); + FutureReservedWord("implements", { es5: true, strictOnly: true }); + FutureReservedWord("import", { es5: true }); + FutureReservedWord("int"); + FutureReservedWord("interface", { es5: true, strictOnly: true }); + FutureReservedWord("long"); + FutureReservedWord("native"); + FutureReservedWord("package", { es5: true, strictOnly: true }); + FutureReservedWord("private", { es5: true, strictOnly: true }); + FutureReservedWord("protected", { es5: true, strictOnly: true }); + FutureReservedWord("public", { es5: true, strictOnly: true }); + FutureReservedWord("short"); + FutureReservedWord("static", { es5: true, strictOnly: true }); + FutureReservedWord("synchronized"); + FutureReservedWord("transient"); + FutureReservedWord("volatile"); + + var lookupBlockType = function() { + var pn, pn1, prev; + var i = -1; + var bracketStack = 0; + var ret = {}; + if (checkPunctuators(state.tokens.curr, ["[", "{"])) { + bracketStack += 1; + } + do { + prev = i === -1 ? state.tokens.curr : pn; + pn = i === -1 ? state.tokens.next : peek(i); + pn1 = peek(i + 1); + i = i + 1; + if (checkPunctuators(pn, ["[", "{"])) { + bracketStack += 1; + } else if (checkPunctuators(pn, ["]", "}"])) { + bracketStack -= 1; + } + if (bracketStack === 1 && pn.identifier && pn.value === "for" && + !checkPunctuator(prev, ".")) { + ret.isCompArray = true; + ret.notJson = true; + break; + } + if (bracketStack === 0 && checkPunctuators(pn, ["}", "]"])) { + if (pn1.value === "=") { + ret.isDestAssign = true; + ret.notJson = true; + break; + } else if (pn1.value === ".") { + ret.notJson = true; + break; + } + } + if (checkPunctuator(pn, ";")) { + ret.notJson = true; + } + } while (bracketStack > 0 && pn.id !== "(end)"); + return ret; + }; + function saveProperty(props, name, tkn, isClass, isStatic, isComputed) { + if (tkn.identifier) { + name = tkn.value; + } + var key = name; + if (isClass && isStatic) { + key = "static " + name; + } + + if (props[key] && name !== "__proto__" && !isComputed) { + var msg = ["key", "class method", "static class method"]; + msg = msg[(isClass || false) + (isStatic || false)]; + warning("W075", state.tokens.next, msg, name); + } else { + props[key] = Object.create(null); + } + + props[key].basic = true; + props[key].basictkn = tkn; + } + function saveAccessor(accessorType, props, name, tkn, isClass, isStatic) { + var flagName = accessorType === "get" ? "getterToken" : "setterToken"; + var key = name; + state.tokens.curr.accessorType = accessorType; + state.nameStack.set(tkn); + if (isClass && isStatic) { + key = "static " + name; + } + + if (props[key]) { + if ((props[key].basic || props[key][flagName]) && name !== "__proto__") { + var msg = ""; + if (isClass) { + if (isStatic) { + msg += "static "; + } + msg += accessorType + "ter method"; + } else { + msg = "key"; + } + warning("W075", state.tokens.next, msg, name); + } + } else { + props[key] = Object.create(null); + } + + props[key][flagName] = tkn; + if (isStatic) { + props[key].static = true; + } + } + function computedPropertyName(context) { + advance("["); + state.tokens.curr.delim = true; + state.tokens.curr.lbp = 0; + + if (!state.inES6()) { + warning("W119", state.tokens.curr, "computed property names", "6"); + } + var value = expression(context & ~prodParams.noin, 10); + advance("]"); + return value; + } + function checkPunctuators(token, values) { + if (token.type === "(punctuator)") { + return _.includes(values, token.value); + } + return false; + } + function checkPunctuator(token, value) { + return token.type === "(punctuator)" && token.value === value; + } + function destructuringAssignOrJsonValue(context) { + + var block = lookupBlockType(); + if (block.notJson) { + if (!state.inES6() && block.isDestAssign) { + warning("W104", state.tokens.curr, "destructuring assignment", "6"); + } + statements(context); + } else { + state.option.laxbreak = true; + state.jsonMode = true; + jsonValue(); + } + } + var arrayComprehension = function() { + var CompArray = function() { + this.mode = "use"; + this.variables = []; + }; + var _carrays = []; + var _current; + function declare(v) { + var l = _current.variables.filter(function(elt) { + if (elt.value === v) { + elt.undef = false; + return v; + } + }).length; + return l !== 0; + } + function use(v) { + var l = _current.variables.filter(function(elt) { + if (elt.value === v && !elt.undef) { + if (elt.unused === true) { + elt.unused = false; + } + return v; + } + }).length; + return (l === 0); + } + return { stack: function() { + _current = new CompArray(); + _carrays.push(_current); + }, + unstack: function() { + _current.variables.filter(function(v) { + if (v.unused) + warning("W098", v.token, v.token.raw_text || v.value); + if (v.undef) + state.funct["(scope)"].block.use(v.value, v.token); + }); + _carrays.splice(-1, 1); + _current = _carrays[_carrays.length - 1]; + }, + setState: function(s) { + if (_.includes(["use", "define", "generate", "filter"], s)) + _current.mode = s; + }, + check: function(v) { + if (!_current) { + return; + } + if (_current && _current.mode === "use") { + if (use(v)) { + _current.variables.push({ + token: state.tokens.curr, + value: v, + undef: true, + unused: false + }); + } + return true; + } else if (_current && _current.mode === "define") { + if (!declare(v)) { + _current.variables.push({ + token: state.tokens.curr, + value: v, + undef: false, + unused: true + }); + } + return true; + } else if (_current && _current.mode === "generate") { + state.funct["(scope)"].block.use(v, state.tokens.curr); + return true; + } else if (_current && _current.mode === "filter") { + if (use(v)) { + state.funct["(scope)"].block.use(v, state.tokens.curr); + } + return true; + } + return false; + } + }; + }; + function jsonValue() { + function jsonObject() { + var o = {}, t = state.tokens.next; + advance("{"); + if (state.tokens.next.id !== "}") { + for (;;) { + if (state.tokens.next.id === "(end)") { + error("E026", state.tokens.next, t.line); + } else if (state.tokens.next.id === "}") { + warning("W094", state.tokens.curr); + break; + } else if (state.tokens.next.id === ",") { + error("E028", state.tokens.next); + } else if (state.tokens.next.id !== "(string)") { + warning("W095", state.tokens.next, state.tokens.next.value); + } + if (o[state.tokens.next.value] === true) { + warning("W075", state.tokens.next, "key", state.tokens.next.value); + } else if ((state.tokens.next.value === "__proto__" && + !state.option.proto) || (state.tokens.next.value === "__iterator__" && + !state.option.iterator)) { + warning("W096", state.tokens.next, state.tokens.next.value); + } else { + o[state.tokens.next.value] = true; + } + advance(); + advance(":"); + jsonValue(); + if (state.tokens.next.id !== ",") { + break; + } + advance(","); + } + } + advance("}"); + } + + function jsonArray() { + var t = state.tokens.next; + advance("["); + if (state.tokens.next.id !== "]") { + for (;;) { + if (state.tokens.next.id === "(end)") { + error("E027", state.tokens.next, t.line); + } else if (state.tokens.next.id === "]") { + warning("W094", state.tokens.curr); + break; + } else if (state.tokens.next.id === ",") { + error("E028", state.tokens.next); + } + jsonValue(); + if (state.tokens.next.id !== ",") { + break; + } + advance(","); + } + } + advance("]"); + } + + switch (state.tokens.next.id) { + case "{": + jsonObject(); + break; + case "[": + jsonArray(); + break; + case "true": + case "false": + case "null": + case "(number)": + case "(string)": + advance(); + break; + case "-": + advance("-"); + advance("(number)"); + break; + default: + error("E003", state.tokens.next); + } + } + function lintEvalCode(internals, options, globals) { + var priorErrorCount, idx, jdx, internal; + + for (idx = 0; idx < internals.length; idx += 1) { + internal = internals[idx]; + options.scope = internal.elem; + priorErrorCount = JSHINT.errors.length; + + itself(internal.code, options, globals); + + for (jdx = priorErrorCount; jdx < JSHINT.errors.length; jdx += 1) { + JSHINT.errors[jdx].line += internal.token.line - 1; + } + } + } + + var escapeRegex = function(str) { + return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); + }; + var itself = function(s, o, g) { + var x, reIgnoreStr, reIgnore; + var optionKeys, newOptionObj, newIgnoredObj; + + o = _.clone(o); + state.reset(); + newOptionObj = state.option; + newIgnoredObj = state.ignored; + + if (o && o.scope) { + JSHINT.scope = o.scope; + } else { + JSHINT.errors = []; + JSHINT.internals = []; + JSHINT.blacklist = {}; + JSHINT.scope = "(main)"; + } + + predefined = Object.create(null); + combine(predefined, vars.ecmaIdentifiers[3]); + combine(predefined, vars.reservedVars); + + declared = Object.create(null); + var exported = Object.create(null); // Variables that live outside the current file + + function each(obj, cb) { + if (!obj) + return; + + if (!Array.isArray(obj) && typeof obj === "object") + obj = Object.keys(obj); + + obj.forEach(cb); + } + + if (o) { + + each([o.predef, o.globals], function(dict) { + each(dict, function(item) { + var slice, prop; + + if (item[0] === "-") { + slice = item.slice(1); + JSHINT.blacklist[slice] = slice; + delete predefined[slice]; + } else { + prop = Object.getOwnPropertyDescriptor(dict, item); + predefined[item] = prop ? prop.value : false; + } + }); + }); + + each(o.exported || null, function(item) { + exported[item] = true; + }); + + delete o.predef; + delete o.exported; + + optionKeys = Object.keys(o); + for (x = 0; x < optionKeys.length; x++) { + if (/^-W\d{3}$/g.test(optionKeys[x])) { + newIgnoredObj[optionKeys[x].slice(1)] = true; + } else { + var optionKey = optionKeys[x]; + newOptionObj[optionKey] = o[optionKey]; + } + } + } + + state.option = newOptionObj; + state.ignored = newIgnoredObj; + + state.option.indent = state.option.indent || 4; + state.option.maxerr = state.option.maxerr || 50; + + indent = 1; + + var scopeManagerInst = scopeManager(state, predefined, exported, declared); + scopeManagerInst.on("warning", function(ev) { + warning.apply(null, [ ev.code, ev.token].concat(ev.data)); + }); + + scopeManagerInst.on("error", function(ev) { + error.apply(null, [ ev.code, ev.token ].concat(ev.data)); + }); + + state.funct = functor("(global)", null, { + "(global)" : true, + "(scope)" : scopeManagerInst, + "(comparray)" : arrayComprehension(), + "(metrics)" : createMetrics(state.tokens.next) + }); + + functions = [state.funct]; + member = {}; + membersOnly = null; + inblock = false; + lookahead = []; + + if (!isString(s) && !Array.isArray(s)) { + errorAt("E004", 0); + return false; + } + + api = { + get isJSON() { + return state.jsonMode; + }, + + getOption: function(name) { + return state.option[name] || null; + }, + + getCache: function(name) { + return state.cache[name]; + }, + + setCache: function(name, value) { + state.cache[name] = value; + }, + + warn: function(code, data) { + warningAt.apply(null, [ code, data.line, data.char ].concat(data.data)); + }, + + on: function(names, listener) { + names.split(" ").forEach(function(name) { + emitter.on(name, listener); + }.bind(this)); + } + }; + + emitter.removeAllListeners(); + (extraModules || []).forEach(function(func) { + func(api); + }); + + state.tokens.prev = state.tokens.curr = state.tokens.next = state.syntax["(begin)"]; + if (o && o.ignoreDelimiters) { + + if (!Array.isArray(o.ignoreDelimiters)) { + o.ignoreDelimiters = [o.ignoreDelimiters]; + } + + o.ignoreDelimiters.forEach(function(delimiterPair) { + if (!delimiterPair.start || !delimiterPair.end) + return; + + reIgnoreStr = escapeRegex(delimiterPair.start) + + "[\\s\\S]*?" + + escapeRegex(delimiterPair.end); + + reIgnore = new RegExp(reIgnoreStr, "ig"); + + s = s.replace(reIgnore, function(match) { + return match.replace(/./g, " "); + }); + }); + } + + lex = new Lexer(s); + + lex.on("warning", function(ev) { + warningAt.apply(null, [ ev.code, ev.line, ev.character].concat(ev.data)); + }); + + lex.on("error", function(ev) { + errorAt.apply(null, [ ev.code, ev.line, ev.character ].concat(ev.data)); + }); + + lex.on("fatal", function(ev) { + quit("E041", ev); + }); + + lex.on("Identifier", function(ev) { + emitter.emit("Identifier", ev); + }); + + lex.on("String", function(ev) { + emitter.emit("String", ev); + }); + + lex.on("Number", function(ev) { + emitter.emit("Number", ev); + }); + var name; + for (name in o) { + if (_.has(o, name)) { + checkOption(name, true, state.tokens.curr); + } + } + if (o) { + for (name in o.unstable) { + if (_.has(o.unstable, name)) { + checkOption(name, false, state.tokens.curr); + } + } + } + + try { + applyOptions(); + combine(predefined, g || {}); + checkComma.first = true; + + advance(); + switch (state.tokens.next.id) { + case "{": + case "[": + destructuringAssignOrJsonValue(0); + break; + default: + directives(); + + if (state.directive["use strict"]) { + if (!state.allowsGlobalUsd()) { + warning("W097", state.tokens.prev); + } + } + + statements(0); + } + + if (state.tokens.next.id !== "(end)") { + quit("E041", state.tokens.curr); + } + + state.funct["(scope)"].unstack(); + + } catch (err) { + if (err && err.name === "JSHintError") { + var nt = state.tokens.next || {}; + JSHINT.errors.push({ + scope : "(main)", + raw : err.raw, + code : err.code, + reason : err.reason, + line : err.line || nt.line, + character : err.character || nt.from + }); + } else { + throw err; + } + } + if (JSHINT.scope === "(main)") { + lintEvalCode(JSHINT.internals, o || {}, g); + } + + return JSHINT.errors.length === 0; + }; + itself.addModule = function(func) { + extraModules.push(func); + }; + + itself.addModule(style.register); + itself.data = function() { + var data = { + functions: [], + options: state.option + }; + + var fu, f, i, n, globals; + + if (itself.errors.length) { + data.errors = itself.errors; + } + + if (state.jsonMode) { + data.json = true; + } + + var impliedGlobals = state.funct["(scope)"].getImpliedGlobals(); + if (impliedGlobals.length > 0) { + data.implieds = impliedGlobals; + } + + globals = state.funct["(scope)"].getUsedOrDefinedGlobals(); + if (globals.length > 0) { + data.globals = globals; + } + + for (i = 1; i < functions.length; i += 1) { + f = functions[i]; + fu = {}; + + fu.name = f["(name)"]; + fu.param = f["(params)"]; + fu.line = f["(line)"]; + fu.character = f["(character)"]; + fu.last = f["(last)"]; + fu.lastcharacter = f["(lastcharacter)"]; + + fu.metrics = { + complexity: f["(metrics)"].ComplexityCount, + parameters: f["(metrics)"].arity, + statements: f["(metrics)"].statementCount + }; + + data.functions.push(fu); + } + + var unuseds = state.funct["(scope)"].getUnuseds(); + if (unuseds.length > 0) { + data.unused = unuseds; + } + + for (n in member) { + if (typeof member[n] === "number") { + data.member = member; + break; + } + } + + return data; + }; + + itself.jshint = itself; + + return itself; +}()); +if (typeof exports === "object" && exports) { + exports.JSHINT = JSHINT; +} + +},{"./lex.js":"/../../jshint/src/lex.js","./messages.js":"/../../jshint/src/messages.js","./options.js":"/../../jshint/src/options.js","./prod-params.js":"/../../jshint/src/prod-params.js","./reg.js":"/../../jshint/src/reg.js","./scope-manager.js":"/../../jshint/src/scope-manager.js","./state.js":"/../../jshint/src/state.js","./style.js":"/../../jshint/src/style.js","./vars.js":"/../../jshint/src/vars.js","console-browserify":"/../../jshint/node_modules/console-browserify/index.js","events":"/../node_modules/events/events.js","lodash.clone":"/../../jshint/node_modules/lodash.clone/index.js","underscore":"/../../jshint/node_modules/underscore/underscore.js"}],"/../../jshint/src/lex.js":[function(_dereq_,module,exports){ + +"use strict"; + +var _ = _dereq_("underscore"); +var events = _dereq_("events"); +var reg = _dereq_("./reg.js"); +var state = _dereq_("./state.js").state; + +var unicodeData = _dereq_("../data/ascii-identifier-data.js"); +var asciiIdentifierStartTable = unicodeData.asciiIdentifierStartTable; +var asciiIdentifierPartTable = unicodeData.asciiIdentifierPartTable; +var nonAsciiIdentifierStartTable = _dereq_("../data/non-ascii-identifier-start.js"); +var nonAsciiIdentifierPartTable = _dereq_("../data/non-ascii-identifier-part-only.js"); +var es5IdentifierNames; + +var Token = { + Identifier: 1, + Punctuator: 2, + NumericLiteral: 3, + StringLiteral: 4, + Comment: 5, + Keyword: 6, + RegExp: 9, + TemplateHead: 10, + TemplateMiddle: 11, + TemplateTail: 12, + NoSubstTemplate: 13 +}; + +var Context = { + Block: 1, + Template: 2 +}; + +function isHex(str) { + return /^[0-9a-fA-F]+$/.test(str); +} + +function isHexDigit(str) { + return str.length === 1 && isHex(str); +} + +function asyncTrigger() { + var _checks = []; + + return { + push: function(fn) { + _checks.push(fn); + }, + + check: function() { + for (var check = 0; check < _checks.length; ++check) { + _checks[check](); + } + + _checks.splice(0, _checks.length); + } + }; +} +function Lexer(source) { + var lines = source; + + if (typeof lines === "string") { + lines = lines + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n") + .split("\n"); + } + + if (lines[0] && lines[0].substr(0, 2) === "#!") { + if (lines[0].indexOf("node") !== -1) { + state.option.node = true; + } + lines[0] = ""; + } + + this.emitter = new events.EventEmitter(); + this.source = source; + this.setLines(lines); + this.prereg = true; + + this.line = 0; + this.char = 1; + this.from = 1; + this.input = ""; + this.inComment = false; + this.context = []; + this.templateStarts = []; + + for (var i = 0; i < state.option.indent; i += 1) { + state.tab += " "; + } +} + +Lexer.prototype = { + _lines: [], + + inContext: function(ctxType) { + return this.context.length > 0 && this.context[this.context.length - 1].type === ctxType; + }, + + pushContext: function(ctxType) { + this.context.push({ type: ctxType }); + }, + + popContext: function() { + return this.context.pop(); + }, + + currentContext: function() { + return this.context.length > 0 && this.context[this.context.length - 1]; + }, + + getLines: function() { + this._lines = state.lines; + return this._lines; + }, + + setLines: function(val) { + this._lines = val; + state.lines = this._lines; + }, + peek: function(i) { + return this.input.charAt(i || 0); + }, + skip: function(i) { + i = i || 1; + this.char += i; + this.input = this.input.slice(i); + }, + on: function(names, listener) { + names.split(" ").forEach(function(name) { + this.emitter.on(name, listener); + }.bind(this)); + }, + trigger: function() { + this.emitter.emit.apply(this.emitter, Array.prototype.slice.call(arguments)); + }, + triggerAsync: function(type, args, checks, fn) { + checks.push(function() { + if (fn()) { + this.trigger(type, args); + } + }.bind(this)); + }, + scanPunctuator: function() { + var ch1 = this.peek(); + var ch2, ch3, ch4; + + switch (ch1) { + case ".": + if (ch1 === "." && this.peek(1) === "?") { + return { + type: Token.Punctuator, + value: ".?" + }; + } + if ((/^[0-9]$/).test(this.peek(1))) { + return null; + } + if (this.peek(1) === "." && this.peek(2) === ".") { + return { + type: Token.Punctuator, + value: "..." + }; + } + case "(": + case ")": + case ";": + case ",": + case "[": + case "]": + case ":": + case "~": + case "?": + return { + type: Token.Punctuator, + value: ch1 + }; + case "{": + this.pushContext(Context.Block); + return { + type: Token.Punctuator, + value: ch1 + }; + case "}": + if (this.inContext(Context.Block)) { + this.popContext(); + } + return { + type: Token.Punctuator, + value: ch1 + }; + case "#": + return { + type: Token.Punctuator, + value: ch1 + }; + case "": + return null; + } + + ch2 = this.peek(1); + ch3 = this.peek(2); + ch4 = this.peek(3); + + if (ch1 === ">" && ch2 === ">" && ch3 === ">" && ch4 === "=") { + return { + type: Token.Punctuator, + value: ">>>=" + }; + } + + if (ch1 === "=" && ch2 === "=" && ch3 === "=") { + return { + type: Token.Punctuator, + value: "===" + }; + } + + if (ch1 === "!" && ch2 === "=" && ch3 === "=") { + return { + type: Token.Punctuator, + value: "!==" + }; + } + + if (ch1 === ">" && ch2 === ">" && ch3 === ">") { + return { + type: Token.Punctuator, + value: ">>>" + }; + } + + if (ch1 === "<" && ch2 === "<" && ch3 === "=") { + return { + type: Token.Punctuator, + value: "<<=" + }; + } + + if (ch1 === ">" && ch2 === ">" && ch3 === "=") { + return { + type: Token.Punctuator, + value: ">>=" + }; + } + if (ch1 === "=" && ch2 === ">") { + return { + type: Token.Punctuator, + value: ch1 + ch2 + }; + } + if (ch1 === ch2 && ("+-<>&|*".indexOf(ch1) >= 0)) { + if (ch1 === "*" && ch3 === "=") { + return { + type: Token.Punctuator, + value: ch1 + ch2 + ch3 + }; + } + + return { + type: Token.Punctuator, + value: ch1 + ch2 + }; + } + if ("<>=!+-*%&|^/".indexOf(ch1) >= 0) { + if (ch2 === "=") { + return { + type: Token.Punctuator, + value: ch1 + ch2 + }; + } + + return { + type: Token.Punctuator, + value: ch1 + }; + } + + return null; + }, + scanComments: function(checks) { + var ch1 = this.peek(); + var ch2 = this.peek(1); + var rest = this.input.substr(2); + var startLine = this.line; + var startChar = this.char; + var self = this; + + function commentToken(label, body, opt) { + var special = [ + "jshint", "jshint.unstable", "jslint", "members", "member", "globals", + "global", "exported" + ]; + var isSpecial = false; + var value = label + body; + var commentType = "plain"; + opt = opt || {}; + + if (opt.isMultiline) { + value += "*/"; + } + + body = body.replace(/\n/g, " "); + + if (label === "/*" && reg.fallsThrough.test(body)) { + isSpecial = true; + commentType = "falls through"; + } + + special.forEach(function(str) { + if (isSpecial) { + return; + } + if (label === "//" && str !== "jshint" && str !== "jshint.unstable") { + return; + } + + if (body.charAt(str.length) === " " && body.substr(0, str.length) === str) { + isSpecial = true; + label = label + str; + body = body.substr(str.length); + } + + if (!isSpecial && body.charAt(0) === " " && body.charAt(str.length + 1) === " " && + body.substr(1, str.length) === str) { + isSpecial = true; + label = label + " " + str; + body = body.substr(str.length + 1); + } + var strIndex = body.indexOf(str); + if (!isSpecial && strIndex >= 0 && body.charAt(strIndex + str.length) === " ") { + var isAllWhitespace = body.substr(0, strIndex).trim().length === 0; + if (isAllWhitespace) { + isSpecial = true; + body = body.substr(str.length + strIndex); + } + } + + if (!isSpecial) { + return; + } + + switch (str) { + case "member": + commentType = "members"; + break; + case "global": + commentType = "globals"; + break; + default: + var options = body.split(":").map(function(v) { + return v.replace(/^\s+/, "").replace(/\s+$/, ""); + }); + + if (options.length === 2) { + switch (options[0]) { + case "ignore": + switch (options[1]) { + case "start": + self.ignoringLinterErrors = true; + isSpecial = false; + break; + case "end": + self.ignoringLinterErrors = false; + isSpecial = false; + break; + } + } + } + + commentType = str; + } + }); + + return { + type: Token.Comment, + commentType: commentType, + value: value, + body: body, + isSpecial: isSpecial, + isMalformed: opt.isMalformed || false + }; + } + if (ch1 === "*" && ch2 === "/") { + this.trigger("error", { + code: "E018", + line: startLine, + character: startChar + }); + + this.skip(2); + return null; + } + if (ch1 !== "/" || (ch2 !== "*" && ch2 !== "/")) { + return null; + } + if (ch2 === "/") { + this.skip(this.input.length); // Skip to the EOL. + return commentToken("//", rest); + } + + var body = ""; + if (ch2 === "*") { + this.inComment = true; + this.skip(2); + + while (this.peek() !== "*" || this.peek(1) !== "/") { + if (this.peek() === "") { // End of Line + body += "\n"; + if (!this.nextLine(checks)) { + this.trigger("error", { + code: "E017", + line: startLine, + character: startChar + }); + + this.inComment = false; + return commentToken("/*", body, { + isMultiline: true, + isMalformed: true + }); + } + } else { + body += this.peek(); + this.skip(); + } + } + + this.skip(2); + this.inComment = false; + return commentToken("/*", body, { isMultiline: true }); + } + }, + scanKeyword: function() { + var result = /^[a-zA-Z_$][a-zA-Z0-9_$]*/.exec(this.input); + var keywords = [ + "if", "in", "do", "var", "for", "new", + "try", "let", "this", "else", "case", + "void", "with", "enum", "while", "break", + "catch", "throw", "const", "yield", "class", + "super", "return", "typeof", "delete", + "switch", "export", "import", "default", + "finally", "extends", "function", "continue", + "debugger", "instanceof", "true", "false", "null", "async", "await" + ]; + + if (result && keywords.indexOf(result[0]) >= 0) { + return { + type: Token.Keyword, + value: result[0] + }; + } + + return null; + }, + scanIdentifier: function(checks) { + var id = ""; + var index = 0; + var char, value; + + function isNonAsciiIdentifierStart(code) { + return nonAsciiIdentifierStartTable.indexOf(code) > -1; + } + + function isNonAsciiIdentifierPart(code) { + return isNonAsciiIdentifierStart(code) || nonAsciiIdentifierPartTable.indexOf(code) > -1; + } + + var readUnicodeEscapeSequence = function() { + index += 1; + + if (this.peek(index) !== "u") { + return null; + } + + var sequence = this.peek(index + 1) + this.peek(index + 2) + + this.peek(index + 3) + this.peek(index + 4); + var code; + + if (isHex(sequence)) { + code = parseInt(sequence, 16); + + if (asciiIdentifierPartTable[code] || isNonAsciiIdentifierPart(code)) { + index += 5; + return "\\u" + sequence; + } + + return null; + } + + return null; + }.bind(this); + + var getIdentifierStart = function() { + var chr = this.peek(index); + var code = chr.charCodeAt(0); + + if (code === 92) { + return readUnicodeEscapeSequence(); + } + + if (code < 128) { + if (asciiIdentifierStartTable[code]) { + index += 1; + return chr; + } + + return null; + } + + if (isNonAsciiIdentifierStart(code)) { + index += 1; + return chr; + } + + return null; + }.bind(this); + + var getIdentifierPart = function() { + var chr = this.peek(index); + var code = chr.charCodeAt(0); + + if (code === 92) { + return readUnicodeEscapeSequence(); + } + + if (code < 128) { + if (asciiIdentifierPartTable[code]) { + index += 1; + return chr; + } + + return null; + } + + if (isNonAsciiIdentifierPart(code)) { + index += 1; + return chr; + } + + return null; + }.bind(this); + + function removeEscapeSequences(id) { + return id.replace(/\\u([0-9a-fA-F]{4})/g, function(m0, codepoint) { + return String.fromCharCode(parseInt(codepoint, 16)); + }); + } + + char = getIdentifierStart(); + if (char === null) { + return null; + } + + id = char; + for (;;) { + char = getIdentifierPart(); + + if (char === null) { + break; + } + + id += char; + } + + value = removeEscapeSequences(id); + + if (!state.inES6(true)) { + es5IdentifierNames = _dereq_("../data/es5-identifier-names.js"); + + if (!es5IdentifierNames.test(value)) { + this.triggerAsync( + "warning", + { + code: "W119", + line: this.line, + character: this.char, + data: ["unicode 8", "6"] + }, + checks, + function() { return true; } + ); + } + } + + return { + type: Token.Identifier, + value: value, + text: id, + tokenLength: id.length + }; + }, + scanNumericLiteral: function(checks) { + var index = 0; + var value = ""; + var length = this.input.length; + var char = this.peek(index); + var isAllowedDigit = isDecimalDigit; + var base = 10; + var isLegacy = false; + + function isDecimalDigit(str) { + return (/^[0-9]$/).test(str); + } + + function isOctalDigit(str) { + return (/^[0-7]$/).test(str); + } + + function isBinaryDigit(str) { + return (/^[01]$/).test(str); + } + + function isIdentifierStart(ch) { + return (ch === "$") || (ch === "_") || (ch === "\\") || + (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z"); + } + + if (char !== "." && !isDecimalDigit(char)) { + return null; + } + + if (char !== ".") { + value = this.peek(index); + index += 1; + char = this.peek(index); + + if (value === "0") { + if (char === "x" || char === "X") { + isAllowedDigit = isHexDigit; + base = 16; + + index += 1; + value += char; + } + if (char === "o" || char === "O") { + isAllowedDigit = isOctalDigit; + base = 8; + + if (!state.inES6(true)) { + this.triggerAsync( + "warning", + { + code: "W119", + line: this.line, + character: this.char, + data: [ "Octal integer literal", "6" ] + }, + checks, + function() { return true; } + ); + } + + index += 1; + value += char; + } + if (char === "b" || char === "B") { + isAllowedDigit = isBinaryDigit; + base = 2; + + if (!state.inES6(true)) { + this.triggerAsync( + "warning", + { + code: "W119", + line: this.line, + character: this.char, + data: [ "Binary integer literal", "6" ] + }, + checks, + function() { return true; } + ); + } + + index += 1; + value += char; + } + if (isOctalDigit(char)) { + isAllowedDigit = isOctalDigit; + base = 8; + isLegacy = true; + + index += 1; + value += char; + } + + if (!isOctalDigit(char) && isDecimalDigit(char)) { + index += 1; + value += char; + } + } + + while (index < length) { + char = this.peek(index); + if (!(isLegacy && isDecimalDigit(char)) && !isAllowedDigit(char)) { + break; + } + value += char; + index += 1; + } + + var isBigInt = this.peek(index) === 'n'; + + if (isAllowedDigit !== isDecimalDigit || isBigInt) { + if (isBigInt) { + if (!state.option.unstable.bigint) { + this.triggerAsync( + "warning", + { + code: "W144", + line: this.line, + character: this.char, + data: [ "BigInt", "bigint" ] + }, + checks, + function() { return true; } + ); + } + + value += char; + index += 1; + } else if (!isLegacy && value.length <= 2) { // 0x + return { + type: Token.NumericLiteral, + value: value, + isMalformed: true + }; + } + + if (index < length) { + char = this.peek(index); + if (isIdentifierStart(char)) { + return null; + } + } + + return { + type: Token.NumericLiteral, + value: value, + base: base, + isLegacy: isLegacy, + isMalformed: false + }; + } + } + + if (char === ".") { + value += char; + index += 1; + + while (index < length) { + char = this.peek(index); + if (!isDecimalDigit(char)) { + break; + } + value += char; + index += 1; + } + } + + if (char === "e" || char === "E") { + value += char; + index += 1; + char = this.peek(index); + + if (char === "+" || char === "-") { + value += this.peek(index); + index += 1; + } + + char = this.peek(index); + if (isDecimalDigit(char)) { + value += char; + index += 1; + + while (index < length) { + char = this.peek(index); + if (!isDecimalDigit(char)) { + break; + } + value += char; + index += 1; + } + } else { + return null; + } + } + + if (index < length) { + char = this.peek(index); + if (isIdentifierStart(char)) { + return null; + } + } + + return { + type: Token.NumericLiteral, + value: value, + base: base, + isMalformed: !isFinite(value) + }; + }, + scanEscapeSequence: function(checks) { + var allowNewLine = false; + var jump = 1; + this.skip(); + var char = this.peek(); + + switch (char) { + case "'": + this.triggerAsync("warning", { + code: "W114", + line: this.line, + character: this.char, + data: [ "\\'" ] + }, checks, function() {return state.jsonMode; }); + break; + case "b": + char = "\\b"; + break; + case "f": + char = "\\f"; + break; + case "n": + char = "\\n"; + break; + case "r": + char = "\\r"; + break; + case "t": + char = "\\t"; + break; + case "0": + char = "\\0"; + var n = parseInt(this.peek(1), 10); + this.triggerAsync("warning", { + code: "W115", + line: this.line, + character: this.char + }, checks, + function() { return n >= 0 && n <= 7 && state.isStrict(); }); + break; + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + char = "\\" + char; + this.triggerAsync("warning", { + code: "W115", + line: this.line, + character: this.char + }, checks, + function() { return state.isStrict(); }); + break; + case "u": + var sequence = this.input.substr(1, 4); + var code = parseInt(sequence, 16); + if (!isHex(sequence)) { + this.trigger("warning", { + code: "W052", + line: this.line, + character: this.char, + data: [ "u" + sequence ] + }); + } + char = String.fromCharCode(code); + jump = 5; + break; + case "v": + this.triggerAsync("warning", { + code: "W114", + line: this.line, + character: this.char, + data: [ "\\v" ] + }, checks, function() { return state.jsonMode; }); + + char = "\v"; + break; + case "x": + var x = parseInt(this.input.substr(1, 2), 16); + + this.triggerAsync("warning", { + code: "W114", + line: this.line, + character: this.char, + data: [ "\\x-" ] + }, checks, function() { return state.jsonMode; }); + + char = String.fromCharCode(x); + jump = 3; + break; + case "\\": + char = "\\\\"; + break; + case "\"": + char = "\\\""; + break; + case "/": + break; + case "": + allowNewLine = true; + char = ""; + break; + } + + return { char: char, jump: jump, allowNewLine: allowNewLine }; + }, + scanTemplateLiteral: function(checks) { + var tokenType; + var value = ""; + var ch; + var startLine = this.line; + var startChar = this.char; + var depth = this.templateStarts.length; + + if (this.peek() === "`") { + if (!state.inES6(true)) { + this.triggerAsync( + "warning", + { + code: "W119", + line: this.line, + character: this.char, + data: ["template literal syntax", "6"] + }, + checks, + function() { return true; } + ); + } + tokenType = Token.TemplateHead; + this.templateStarts.push({ line: this.line, char: this.char }); + depth = this.templateStarts.length; + this.skip(1); + this.pushContext(Context.Template); + } else if (this.inContext(Context.Template) && this.peek() === "}") { + tokenType = Token.TemplateMiddle; + } else { + return null; + } + + while (this.peek() !== "`") { + while ((ch = this.peek()) === "") { + value += "\n"; + if (!this.nextLine(checks)) { + var startPos = this.templateStarts.pop(); + this.trigger("error", { + code: "E052", + line: startPos.line, + character: startPos.char + }); + return { + type: tokenType, + value: value, + startLine: startLine, + startChar: startChar, + isUnclosed: true, + depth: depth, + context: this.popContext() + }; + } + } + + if (ch === '$' && this.peek(1) === '{') { + value += '${'; + this.skip(2); + return { + type: tokenType, + value: value, + startLine: startLine, + startChar: startChar, + isUnclosed: false, + depth: depth, + context: this.currentContext() + }; + } else if (ch === '\\') { + var escape = this.scanEscapeSequence(checks); + value += escape.char; + this.skip(escape.jump); + } else if (ch !== '`') { + value += ch; + this.skip(1); + } + } + tokenType = tokenType === Token.TemplateHead ? Token.NoSubstTemplate : Token.TemplateTail; + this.skip(1); + this.templateStarts.pop(); + + return { + type: tokenType, + value: value, + startLine: startLine, + startChar: startChar, + isUnclosed: false, + depth: depth, + context: this.popContext() + }; + }, + scanStringLiteral: function(checks) { + var quote = this.peek(); + if (quote !== "\"" && quote !== "'") { + return null; + } + this.triggerAsync("warning", { + code: "W108", + line: this.line, + character: this.char // +1? + }, checks, function() { return state.jsonMode && quote !== "\""; }); + + var value = ""; + var startLine = this.line; + var startChar = this.char; + var allowNewLine = false; + + this.skip(); + + while (this.peek() !== quote) { + if (this.peek() === "") { // End Of Line + + if (!allowNewLine) { + this.trigger("warning", { + code: "W112", + line: this.line, + character: this.char + }); + } else { + allowNewLine = false; + + this.triggerAsync("warning", { + code: "W043", + line: this.line, + character: this.char + }, checks, function() { return !state.option.multistr; }); + + this.triggerAsync("warning", { + code: "W042", + line: this.line, + character: this.char + }, checks, function() { return state.jsonMode && state.option.multistr; }); + } + + if (!this.nextLine(checks)) { + return { + type: Token.StringLiteral, + value: value, + startLine: startLine, + startChar: startChar, + isUnclosed: true, + quote: quote + }; + } + + } else { // Any character other than End Of Line + + allowNewLine = false; + var char = this.peek(); + var jump = 1; // A length of a jump, after we're done + + if (char < " ") { + this.triggerAsync( + "warning", + { + code: "W113", + line: this.line, + character: this.char, + data: [ "" ] + }, + checks, + function() { return true; } + ); + } + if (char === "\\") { + var parsed = this.scanEscapeSequence(checks); + char = parsed.char; + jump = parsed.jump; + allowNewLine = parsed.allowNewLine; + } + if (char !== "") { + value += char; + this.skip(jump); + } + } + } + + this.skip(); + return { + type: Token.StringLiteral, + value: value, + startLine: startLine, + startChar: startChar, + isUnclosed: false, + quote: quote + }; + }, + scanRegExp: function(checks) { + var index = 0; + var length = this.input.length; + var char = this.peek(); + var value = char; + var body = ""; + var groupReferences = []; + var allFlags = ""; + var es5Flags = ""; + var malformed = false; + var isCharSet = false; + var isCharSetRange = false; + var isGroup = false; + var isQuantifiable = false; + var hasInvalidQuantifier = false; + var escapedChars = ""; + var hasUFlag = function() { return allFlags.indexOf("u") > -1; }; + var escapeSequence; + var groupCount = 0; + var terminated, malformedDesc; + + var scanRegexpEscapeSequence = function() { + var next, sequence; + index += 1; + char = this.peek(index); + + if (reg.nonzeroDigit.test(char)) { + sequence = char; + next = this.peek(index + 1); + while (reg.nonzeroDigit.test(next) || next === "0") { + index += 1; + char = next; + sequence += char; + body += char; + value += char; + next = this.peek(index + 1); + } + groupReferences.push(Number(sequence)); + return sequence; + } + + escapedChars += char; + + if (char === "u" && this.peek(index + 1) === "{") { + var x = index + 2; + sequence = "u{"; + next = this.peek(x); + while (isHex(next)) { + sequence += next; + x += 1; + next = this.peek(x); + } + + if (next !== "}") { + this.triggerAsync( + "error", + { + code: "E016", + line: this.line, + character: this.char, + data: [ "Invalid Unicode escape sequence" ] + }, + checks, + hasUFlag + ); + } else if (sequence.length > 2) { + sequence += "}"; + body += sequence; + value += sequence; + index = x + 1; + return sequence; + } + } + if (char < " ") { + malformed = true; + this.triggerAsync( + "warning", + { + code: "W048", + line: this.line, + character: this.char + }, + checks, + function() { return true; } + ); + } + if (char === "<") { + malformed = true; + this.triggerAsync( + "warning", + { + code: "W049", + line: this.line, + character: this.char, + data: [ char ] + }, + checks, + function() { return true; } + ); + } else if (char === "0" && reg.decimalDigit.test(this.peek(index + 1))) { + this.triggerAsync( + "error", + { + code: "E016", + line: this.line, + character: this.char, + data: [ "Invalid decimal escape sequence" ] + }, + checks, + hasUFlag + ); + } + + index += 1; + body += char; + value += char; + + return char; + }.bind(this); + + var checkQuantifier = function() { + var lookahead = index; + var lowerBound = ""; + var upperBound = ""; + var next; + + next = this.peek(lookahead + 1); + + while (reg.decimalDigit.test(next)) { + lookahead += 1; + lowerBound += next; + next = this.peek(lookahead + 1); + } + + if (!lowerBound) { + return false; + } + + if (next === "}") { + return true; + } + + if (next !== ",") { + return false; + } + + lookahead += 1; + next = this.peek(lookahead + 1); + + while (reg.decimalDigit.test(next)) { + lookahead += 1; + upperBound += next; + next = this.peek(lookahead + 1); + } + + if (next !== "}") { + return false; + } + + if (upperBound) { + return Number(lowerBound) <= Number(upperBound); + } + + return true; + }.bind(this); + + var translateUFlag = function(body) { + var astralSubstitute = "\uFFFF"; + + return body + .replace(/\\u\{([0-9a-fA-F]+)\}|\\u([a-fA-F0-9]{4})/g, function($0, $1, $2) { + var codePoint = parseInt($1 || $2, 16); + var literal; + + if (codePoint > 0x10FFFF) { + malformed = true; + this.trigger("error", { + code: "E016", + line: this.line, + character: this.char, + data: [ char ] + }); + + return; + } + literal = String.fromCharCode(codePoint); + + if (reg.regexpSyntaxChars.test(literal)) { + return $0; + } + + if (codePoint <= 0xFFFF) { + return String.fromCharCode(codePoint); + } + return astralSubstitute; + }.bind(this)) + .replace( + /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + astralSubstitute + ); + }.bind(this); + if (!this.prereg || char !== "/") { + return null; + } + + index += 1; + terminated = false; + + while (index < length) { + isCharSetRange &= char === "-"; + char = this.peek(index); + value += char; + body += char; + + if (isCharSet) { + if (char === "]") { + if (this.peek(index - 1) !== "\\" || this.peek(index - 2) === "\\") { + isCharSet = false; + } + } else if (char === "-") { + isCharSetRange = true; + } + } + + if (char === "\\") { + escapeSequence = scanRegexpEscapeSequence(); + + if (isCharSet && (this.peek(index) === "-" || isCharSetRange) && + reg.regexpCharClasses.test(escapeSequence)) { + this.triggerAsync( + "error", + { + code: "E016", + line: this.line, + character: this.char, + data: [ "Character class used in range" ] + }, + checks, + hasUFlag + ); + } + + continue; + } + + if (isCharSet) { + index += 1; + continue; + } + + if (char === "{" && !hasInvalidQuantifier) { + hasInvalidQuantifier = !checkQuantifier(); + } + + if (char === "[") { + isCharSet = true; + index += 1; + continue; + } else if (char === "(") { + isGroup = true; + + if (this.peek(index + 1) === "?" && + (this.peek(index + 2) === "=" || this.peek(index + 2) === "!")) { + isQuantifiable = true; + } + } else if (char === ")") { + if (isQuantifiable) { + isQuantifiable = false; + + if (reg.regexpQuantifiers.test(this.peek(index + 1))) { + this.triggerAsync( + "error", + { + code: "E016", + line: this.line, + character: this.char, + data: [ "Quantified quantifiable" ] + }, + checks, + hasUFlag + ); + } + } else { + groupCount += 1; + } + + isGroup = false; + } else if (char === "/") { + body = body.substr(0, body.length - 1); + terminated = true; + index += 1; + break; + } + + index += 1; + } + + if (!terminated) { + this.trigger("error", { + code: "E015", + line: this.line, + character: this.from + }); + + return void this.trigger("fatal", { + line: this.line, + from: this.from + }); + } + + while (index < length) { + char = this.peek(index); + if (!/[gimyus]/.test(char)) { + break; + } + if (char === "y") { + if (!state.inES6(true)) { + this.triggerAsync( + "warning", + { + code: "W119", + line: this.line, + character: this.char, + data: [ "Sticky RegExp flag", "6" ] + }, + checks, + function() { return true; } + ); + } + } else if (char === "u") { + if (!state.inES6(true)) { + this.triggerAsync( + "warning", + { + code: "W119", + line: this.line, + character: this.char, + data: [ "Unicode RegExp flag", "6" ] + }, + checks, + function() { return true; } + ); + } + + var hasInvalidEscape = (function(groupReferences, groupCount, escapedChars, reg) { + var hasInvalidGroup = groupReferences.some(function(groupReference) { + if (groupReference > groupCount) { + return true; + } + }); + + if (hasInvalidGroup) { + return true; + } + + return !escapedChars.split("").every(function(escapedChar) { + return escapedChar === "u" || + escapedChar === "/" || + escapedChar === "0" || + reg.regexpControlEscapes.test(escapedChar) || + reg.regexpCharClasses.test(escapedChar) || + reg.regexpSyntaxChars.test(escapedChar); + }); + }(groupReferences, groupCount, escapedChars, reg)); + + if (hasInvalidEscape) { + malformedDesc = "Invalid escape"; + } else if (hasInvalidQuantifier) { + malformedDesc = "Invalid quantifier"; + } + + body = translateUFlag(body); + } else if (char === "s") { + if (!state.inES9()) { + this.triggerAsync( + "warning", + { + code: "W119", + line: this.line, + character: this.char, + data: [ "DotAll RegExp flag", "9" ] + }, + checks, + function() { return true; } + ); + } + if (value.indexOf("s") > -1) { + malformedDesc = "Duplicate RegExp flag"; + } + } else { + es5Flags += char; + } + + if (allFlags.indexOf(char) > -1) { + malformedDesc = "Duplicate RegExp flag"; + } + allFlags += char; + + value += char; + allFlags += char; + index += 1; + } + + if (allFlags.indexOf("u") === -1) { + this.triggerAsync("warning", { + code: "W147", + line: this.line, + character: this.char + }, checks, function() { return state.option.regexpu; }); + } + + try { + new RegExp(body, es5Flags); + } catch (err) { + malformedDesc = err.message; + } + + if (malformedDesc) { + malformed = true; + this.trigger("error", { + code: "E016", + line: this.line, + character: this.char, + data: [ malformedDesc ] + }); + } else if (allFlags.indexOf("s") > -1 && !reg.regexpDot.test(body)) { + this.trigger("warning", { + code: "W148", + line: this.line, + character: this.char + }); + } + + return { + type: Token.RegExp, + value: value, + isMalformed: malformed + }; + }, + scanNonBreakingSpaces: function() { + return state.option.nonbsp ? + this.input.search(/(\u00A0)/) : -1; + }, + next: function(checks) { + this.from = this.char; + while (reg.whitespace.test(this.peek())) { + this.from += 1; + this.skip(); + } + + var match = this.scanComments(checks) || + this.scanStringLiteral(checks) || + this.scanTemplateLiteral(checks); + + if (match) { + return match; + } + + match = + this.scanRegExp(checks) || + this.scanPunctuator() || + this.scanKeyword() || + this.scanIdentifier(checks) || + this.scanNumericLiteral(checks); + + if (match) { + this.skip(match.tokenLength || match.value.length); + return match; + } + + return null; + }, + nextLine: function(checks) { + var char; + + if (this.line >= this.getLines().length) { + return false; + } + + this.input = this.getLines()[this.line]; + this.line += 1; + this.char = 1; + this.from = 1; + + var inputTrimmed = this.input.trim(); + + var startsWith = function() { + return _.some(arguments, function(prefix) { + return inputTrimmed.indexOf(prefix) === 0; + }); + }; + + var endsWith = function() { + return _.some(arguments, function(suffix) { + return inputTrimmed.indexOf(suffix, inputTrimmed.length - suffix.length) !== -1; + }); + }; + if (this.ignoringLinterErrors === true) { + if (!startsWith("/*", "//") && !(this.inComment && endsWith("*/"))) { + this.input = ""; + } + } + + char = this.scanNonBreakingSpaces(); + if (char >= 0) { + this.triggerAsync( + "warning", + { code: "W125", line: this.line, character: char + 1 }, + checks, + function() { return true; } + ); + } + + this.input = this.input.replace(/\t/g, state.tab); + + if (!this.ignoringLinterErrors && state.option.maxlen && + state.option.maxlen < this.input.length) { + var inComment = this.inComment || + startsWith.call(inputTrimmed, "//") || + startsWith.call(inputTrimmed, "/*"); + + var shouldTriggerError = !inComment || !reg.maxlenException.test(inputTrimmed); + + if (shouldTriggerError) { + this.triggerAsync( + "warning", + { code: "W101", line: this.line, character: this.input.length }, + checks, + function() { return true; } + ); + } + } + + return true; + }, + token: function() { + var checks = asyncTrigger(); + var token; + var create = function(type, value, isProperty, token) { + var obj; + + if (type !== "(endline)" && type !== "(end)") { + this.prereg = false; + } + + if (type === "(punctuator)") { + switch (value) { + case ".": + case ")": + case "~": + case "#": + case "]": + case "}": + case "++": + case "--": + this.prereg = false; + break; + default: + this.prereg = true; + } + + obj = Object.create(state.syntax[value] || state.syntax["(error)"]); + } + + if (type === "(identifier)") { + if (value === "return" || value === "case" || value === "yield" || + value === "typeof" || value === "instanceof" || value === "void" || + value === "await") { + this.prereg = true; + } + + if (_.has(state.syntax, value)) { + obj = Object.create(state.syntax[value] || state.syntax["(error)"]); + } + } + + if (type === "(template)" || type === "(template middle)") { + this.prereg = true; + } + + if (!obj) { + obj = Object.create(state.syntax[type]); + } + + obj.identifier = (type === "(identifier)"); + obj.type = obj.type || type; + obj.value = value; + obj.line = this.line; + obj.character = this.char; + obj.from = this.from; + if (obj.identifier && token) obj.raw_text = token.text || token.value; + if (token && token.startLine && token.startLine !== this.line) { + obj.startLine = token.startLine; + } + if (token && token.context) { + obj.context = token.context; + } + if (token && token.depth) { + obj.depth = token.depth; + } + if (token && token.isUnclosed) { + obj.isUnclosed = token.isUnclosed; + } + + if (isProperty && obj.identifier) { + obj.isProperty = isProperty; + } + + obj.check = checks.check; + + return obj; + }.bind(this); + + for (;;) { + if (!this.input.length) { + if (this.nextLine(checks)) { + return create("(endline)", ""); + } + + if (this.exhausted) { + return null; + } + + this.exhausted = true; + return create("(end)", ""); + } + + token = this.next(checks); + + if (!token) { + if (this.input.length) { + this.trigger("error", { + code: "E024", + line: this.line, + character: this.char, + data: [ this.peek() ] + }); + + this.input = ""; + } + + continue; + } + + switch (token.type) { + case Token.StringLiteral: + this.triggerAsync("String", { + line: this.line, + char: this.char, + from: this.from, + startLine: token.startLine, + startChar: token.startChar, + value: token.value, + quote: token.quote + }, checks, function() { return true; }); + + return create("(string)", token.value, null, token); + + case Token.TemplateHead: + this.trigger("TemplateHead", { + line: this.line, + char: this.char, + from: this.from, + startLine: token.startLine, + startChar: token.startChar, + value: token.value + }); + return create("(template)", token.value, null, token); + + case Token.TemplateMiddle: + this.trigger("TemplateMiddle", { + line: this.line, + char: this.char, + from: this.from, + startLine: token.startLine, + startChar: token.startChar, + value: token.value + }); + return create("(template middle)", token.value, null, token); + + case Token.TemplateTail: + this.trigger("TemplateTail", { + line: this.line, + char: this.char, + from: this.from, + startLine: token.startLine, + startChar: token.startChar, + value: token.value + }); + return create("(template tail)", token.value, null, token); + + case Token.NoSubstTemplate: + this.trigger("NoSubstTemplate", { + line: this.line, + char: this.char, + from: this.from, + startLine: token.startLine, + startChar: token.startChar, + value: token.value + }); + return create("(no subst template)", token.value, null, token); + + case Token.Identifier: + this.triggerAsync("Identifier", { + line: this.line, + char: this.char, + from: this.from, + name: token.value, + raw_name: token.text, + isProperty: state.tokens.curr.id === "." + }, checks, function() { return true; }); + case Token.Keyword: + return create("(identifier)", token.value, state.tokens.curr.id === ".", token); + + case Token.NumericLiteral: + if (token.isMalformed) { + this.trigger("warning", { + code: "W045", + line: this.line, + character: this.char, + data: [ token.value ] + }); + } + + this.triggerAsync("warning", { + code: "W114", + line: this.line, + character: this.char, + data: [ "0x-" ] + }, checks, function() { return token.base === 16 && state.jsonMode; }); + + this.triggerAsync("warning", { + code: "W115", + line: this.line, + character: this.char + }, checks, function() { + return state.isStrict() && token.base === 8 && token.isLegacy; + }); + + this.trigger("Number", { + line: this.line, + char: this.char, + from: this.from, + value: token.value, + base: token.base, + isMalformed: token.isMalformed + }); + + return create("(number)", token.value); + + case Token.RegExp: + return create("(regexp)", token.value); + + case Token.Comment: + if (token.isSpecial) { + return { + id: '(comment)', + value: token.value, + body: token.body, + type: token.commentType, + isSpecial: token.isSpecial, + line: this.line, + character: this.char, + from: this.from + }; + } + + break; + + default: + return create("(punctuator)", token.value); + } + } + } +}; + +exports.Lexer = Lexer; +exports.Context = Context; + +},{"../data/ascii-identifier-data.js":"/../../jshint/data/ascii-identifier-data.js","../data/es5-identifier-names.js":"/../../jshint/data/es5-identifier-names.js","../data/non-ascii-identifier-part-only.js":"/../../jshint/data/non-ascii-identifier-part-only.js","../data/non-ascii-identifier-start.js":"/../../jshint/data/non-ascii-identifier-start.js","./reg.js":"/../../jshint/src/reg.js","./state.js":"/../../jshint/src/state.js","events":"/../node_modules/events/events.js","underscore":"/../../jshint/node_modules/underscore/underscore.js"}],"/../../jshint/src/messages.js":[function(_dereq_,module,exports){ +"use strict"; + +var _ = _dereq_("underscore"); + +var errors = { + E001: "Bad {a}option: '{b}'.", + E002: "Bad option value.", + E003: "Expected a JSON value.", + E004: "Input is neither a string nor an array of strings.", + E005: "Input is empty.", + E006: "Unexpected early end of program.", + E007: "Missing \"use strict\" statement.", + E008: "Strict violation.", + E009: "Option 'validthis' can't be used in a global scope.", + E010: "'with' is not allowed in strict mode.", + E011: "'{a}' has already been declared.", + E012: "const '{a}' is initialized to 'undefined'.", + E013: "Attempting to override '{a}' which is a constant.", + E014: "A regular expression literal can be confused with '/='.", + E015: "Unclosed regular expression.", + E016: "Invalid regular expression.", + E017: "Unclosed comment.", + E018: "Unbegun comment.", + E019: "Unmatched '{a}'.", + E020: "Expected '{a}' to match '{b}' from line {c} and instead saw '{d}'.", + E021: "Expected '{a}' and instead saw '{b}'.", + E022: "Line breaking error '{a}'.", + E023: "Missing '{a}'.", + E024: "Unexpected '{a}'.", + E025: "Missing ':' on a case clause.", + E026: "Missing '}' to match '{' from line {a}.", + E027: "Missing ']' to match '[' from line {a}.", + E028: "Illegal comma.", + E029: "Unclosed string.", + E030: "Expected an identifier and instead saw '{a}'.", + E031: "Bad assignment.", // FIXME: Rephrase + E032: "Expected a small integer or 'false' and instead saw '{a}'.", + E033: "Expected an operator and instead saw '{a}'.", + E034: "get/set are ES5 features.", + E035: "Missing property name.", + E036: "Expected to see a statement and instead saw a block.", + E037: null, + E038: null, + E039: "Function declarations are not invocable. Wrap the whole function invocation in parens.", + E040: "Each value should have its own case label.", + E041: "Unrecoverable syntax error.", + E042: "Stopping.", + E043: "Too many errors.", + E044: null, + E045: "Invalid for each loop.", + E046: "Yield expressions may only occur within generator functions.", + E047: null, + E048: "{a} declaration not directly within block.", + E049: "A {a} cannot be named '{b}'.", + E050: "Mozilla requires the yield expression to be parenthesized here.", + E051: null, + E052: "Unclosed template literal.", + E053: "{a} declarations are only allowed at the top level of module scope.", + E054: "Class properties must be methods. Expected '(' but instead saw '{a}'.", + E055: "The '{a}' option cannot be set after any executable code.", + E056: "'{a}' was used before it was declared, which is illegal for '{b}' variables.", + E057: "Invalid meta property: '{a}.{b}'.", + E058: "Missing semicolon.", + E059: "Incompatible values for the '{a}' and '{b}' linting options.", + E060: "Non-callable values cannot be used as the second operand to instanceof.", + E061: "Invalid position for 'yield' expression (consider wrapping in parenthesis).", + E062: "Rest parameter does not a support default value.", + E063: "Super property may only be used within method bodies.", + E064: "Super call may only be used within class method bodies.", + E065: "Functions defined outside of strict mode with non-simple parameter lists may not " + + "enable strict mode.", + E066: "Asynchronous iteration is only available with for-of loops." +}; + +var warnings = { + W001: "'hasOwnProperty' is a really bad name.", + W002: "Value of '{a}' may be overwritten in IE 8 and earlier.", + W003: "'{a}' was used before it was defined.", + W004: "'{a}' is already defined.", + W005: "A dot following a number can be confused with a decimal point.", + W006: "Confusing minuses.", + W007: "Confusing plusses.", + W008: "A leading decimal point can be confused with a dot: '{a}'.", + W009: "The array literal notation [] is preferable.", + W010: "The object literal notation {} is preferable.", + W011: null, + W012: null, + W013: null, + W014: "Misleading line break before '{a}'; readers may interpret this as an expression boundary.", + W015: null, + W016: "Unexpected use of '{a}'.", + W017: "Bad operand.", + W018: "Confusing use of '{a}'.", + W019: "Use the isNaN function to compare with NaN.", + W020: "Read only.", + W021: "Reassignment of '{a}', which is a {b}. " + + "Use 'var' or 'let' to declare bindings that may change.", + W022: "Do not assign to the exception parameter.", + W023: null, + W024: "Expected an identifier and instead saw '{a}' (a reserved word).", + W025: "Missing name in function declaration.", + W026: "Inner functions should be listed at the top of the outer function.", + W027: "Unreachable '{a}' after '{b}'.", + W028: "Label '{a}' on {b} statement.", + W030: "Expected an assignment or function call and instead saw an expression.", + W031: "Do not use 'new' for side effects.", + W032: "Unnecessary semicolon.", + W033: "Missing semicolon.", + W034: "Unnecessary directive \"{a}\".", + W035: "Empty block.", + W036: "Unexpected /*member '{a}'.", + W037: "'{a}' is a statement label.", + W038: "'{a}' used out of scope.", + W039: null, + W040: "If a strict mode function is executed using function invocation, " + + "its 'this' value will be undefined.", + W041: null, + W042: "Avoid EOL escaping.", + W043: "Bad escaping of EOL. Use option multistr if needed.", + W044: "Bad or unnecessary escaping.", /* TODO(caitp): remove W044 */ + W045: "Bad number '{a}'.", + W046: "Don't use extra leading zeros '{a}'.", + W047: "A trailing decimal point can be confused with a dot: '{a}'.", + W048: "Unexpected control character in regular expression.", + W049: "Unexpected escaped character '{a}' in regular expression.", + W050: "JavaScript URL.", + W051: "Variables should not be deleted.", + W052: "Unexpected '{a}'.", + W053: "Do not use {a} as a constructor.", + W054: "The Function constructor is a form of eval.", + W055: "A constructor name should start with an uppercase letter.", + W056: "Bad constructor.", + W057: "Weird construction. Is 'new' necessary?", + W058: "Missing '()' invoking a constructor.", + W059: "Avoid arguments.{a}.", + W060: "document.write can be a form of eval.", + W061: "eval can be harmful.", + W062: "Wrap an immediate function invocation in parens " + + "to assist the reader in understanding that the expression " + + "is the result of a function, and not the function itself.", + W063: "Math is not a function.", + W064: "Missing 'new' prefix when invoking a constructor.", + W065: "Missing radix parameter.", + W066: "Implied eval. Consider passing a function instead of a string.", + W067: "Bad invocation.", + W068: "Wrapping non-IIFE function literals in parens is unnecessary.", + W069: "['{a}'] is better written in dot notation.", + W070: "Extra comma. (it breaks older versions of IE)", + W071: "This function has too many statements. ({a})", + W072: "This function has too many parameters. ({a})", + W073: "Blocks are nested too deeply. ({a})", + W074: "This function's cyclomatic complexity is too high. ({a})", + W075: "Duplicate {a} '{b}'.", + W076: "Unexpected parameter '{a}' in get {b} function.", + W077: "Expected a single parameter in set {a} function.", + W078: "Setter is defined without getter.", + W079: "Redefinition of '{a}'.", + W080: "It's not necessary to initialize '{a}' to 'undefined'.", + W081: null, + W082: "Function declarations should not be placed in blocks. " + + "Use a function expression or move the statement to the top of " + + "the outer function.", + W083: "Functions declared within loops referencing an outer scoped " + + "variable may lead to confusing semantics. ({a})", + W084: "Assignment in conditional expression", + W085: "Don't use 'with'.", + W086: "Expected a 'break' statement before '{a}'.", + W087: "Forgotten 'debugger' statement?", + W088: "Creating global 'for' variable. Should be 'for (var {a} ...'.", + // W288: "The syntax of function '{a}' is specific to TBEL, and is not supported by JS executor.", + W089: "The body of a for in should be wrapped in an if statement to filter " + + "unwanted properties from the prototype.", + W090: "'{a}' is not a statement label.", + W091: null, + W093: "Did you mean to return a conditional instead of an assignment?", + W094: "Unexpected comma.", + W095: "Expected a string and instead saw {a}.", + W096: "The '{a}' key may produce unexpected results.", + W097: "Use the function form of \"use strict\".", + W098: "'{a}' is defined but never used.", + W099: null, + W100: null, + W101: "Line is too long.", + W102: null, + W103: "The '{a}' property is deprecated.", + W104: "'{a}' is available in ES{b} (use 'esversion: {b}') or Mozilla JS extensions (use moz).", + W105: null, + W106: "Identifier '{a}' is not in camel case.", + W107: "Script URL.", + W108: "Strings must use doublequote.", + W109: "Strings must use singlequote.", + W110: "Mixed double and single quotes.", + W112: "Unclosed string.", + W113: "Control character in string: {a}.", + W114: "Avoid {a}.", + W115: "Octal literals are not allowed in strict mode.", + W116: "Expected '{a}' and instead saw '{b}'.", + W117: "'{a}' is not defined.", + W118: "'{a}' is only available in Mozilla JavaScript extensions (use moz option).", + W119: "'{a}' is only available in ES{b} (use 'esversion: {b}').", + W120: "You might be leaking a variable ({a}) here.", + W121: "Extending prototype of native object: '{a}'.", + W122: "Invalid typeof value '{a}'", + W123: "'{a}' is already defined in outer scope.", + W124: "A generator function should contain at least one yield expression.", + W125: "This line contains non-breaking spaces: http://jshint.com/docs/options/#nonbsp", + W126: "Unnecessary grouping operator.", + W127: "Unexpected use of a comma operator.", + W128: "Empty array elements require elision=true.", + W129: "'{a}' is defined in a future version of JavaScript. Use a " + + "different variable name to avoid migration issues.", + W130: "Invalid element after rest element.", + W131: "Invalid parameter after rest parameter.", + W132: "`var` declarations are forbidden. Use `let` or `const` instead.", + W133: "Invalid for-{a} loop left-hand-side: {b}.", + W134: "The '{a}' option is only available when linting ECMAScript {b} code.", + W135: "{a} may not be supported by non-browser environments.", + W136: "'{a}' must be in function scope.", + W137: "Empty destructuring: this is unnecessary and can be removed.", + W138: "Regular parameters should not come after default parameters.", + W139: "Function expressions should not be used as the second operand to instanceof.", + W140: "Missing comma.", + W141: "Empty {a}: this is unnecessary and can be removed.", + W142: "Empty {a}: consider replacing with `import '{b}';`.", + W143: "Assignment to properties of a mapped arguments object may cause " + + "unexpected changes to formal parameters.", + W144: "'{a}' is a non-standard language feature. Enable it using the '{b}' unstable option.", + W145: "Superfluous 'case' clause.", + W146: "Unnecessary `await` expression.", + W147: "Regular expressions should include the 'u' flag.", + W148: "Unnecessary RegExp 's' flag." +}; + +var info = { + I001: "Comma warnings can be turned off with 'laxcomma'.", + I002: null, + I003: "ES5 option is now set per default" +}; + +exports.errors = {}; +exports.warnings = {}; +exports.info = {}; + +_.each(errors, function(desc, code) { + exports.errors[code] = { code: code, desc: desc }; +}); + +_.each(warnings, function(desc, code) { + exports.warnings[code] = { code: code, desc: desc }; +}); + +_.each(info, function(desc, code) { + exports.info[code] = { code: code, desc: desc }; +}); + +},{"underscore":"/../../jshint/node_modules/underscore/underscore.js"}],"/../../jshint/src/name-stack.js":[function(_dereq_,module,exports){ +"use strict"; + +function NameStack() { + this._stack = []; +} + +Object.defineProperty(NameStack.prototype, "length", { + get: function() { + return this._stack.length; + } +}); +NameStack.prototype.push = function() { + this._stack.push(null); +}; +NameStack.prototype.pop = function() { + this._stack.pop(); +}; +NameStack.prototype.set = function(token) { + this._stack[this.length - 1] = token; +}; +NameStack.prototype.infer = function() { + var nameToken = this._stack[this.length - 1]; + var prefix = ""; + var type; + if (!nameToken || nameToken.type === "class") { + nameToken = this._stack[this.length - 2]; + } + + if (!nameToken) { + return "(empty)"; + } + + type = nameToken.type; + + if (type !== "(string)" && type !== "(number)" && type !== "(identifier)" && type !== "default") { + return "(expression)"; + } + + if (nameToken.accessorType) { + prefix = nameToken.accessorType + " "; + } + + return prefix + nameToken.value; +}; + +module.exports = NameStack; + +},{}],"/../../jshint/src/options.js":[function(_dereq_,module,exports){ +"use strict"; +exports.bool = { + enforcing: { + bitwise : true, + freeze : true, + camelcase : true, + curly : true, + eqeqeq : true, + futurehostile: true, + es3 : true, + es5 : true, + forin : true, + immed : true, + leanswitch : true, + newcap : true, + noarg : true, + nocomma : true, + noempty : true, + nonbsp : true, + nonew : true, + noreturnawait: true, + regexpu : true, + undef : true, + singleGroups: false, + varstmt: false, + enforceall : false, + trailingcomma: false + }, + relaxing: { + asi : true, + multistr : true, + debug : true, + boss : true, + evil : true, + funcscope : true, + globalstrict: true, + iterator : true, + notypeof : true, + plusplus : true, + proto : true, + scripturl : true, + sub : true, + supernew : true, + laxbreak : true, + laxcomma : true, + validthis : true, + withstmt : true, + moz : true, + noyield : true, + eqnull : true, + lastsemic : true, + loopfunc : true, + expr : true, + esnext : true, + elision : true, + }, + environments: { + mootools : true, + couch : true, + jasmine : true, + jquery : true, + node : true, + qunit : true, + rhino : true, + shelljs : true, + prototypejs : true, + yui : true, + mocha : true, + module : true, + wsh : true, + worker : true, + nonstandard : true, + browser : true, + browserify : true, + devel : true, + dojo : true, + typed : true, + phantom : true + }, + obsolete: { + onecase : true, // if one case switch statements should be allowed + regexp : true, // if the . should not be allowed in regexp literals + regexdash : true // if unescaped first/last dash (-) inside brackets + } +}; +exports.val = { + maxlen : false, + indent : false, + maxerr : false, + predef : false, + globals : false, + quotmark : false, + + scope : false, + maxstatements: false, + maxdepth : false, + maxparams : false, + maxcomplexity: false, + shadow : false, + strict : true, + unused : true, + latedef : false, + + ignore : false, // start/end ignoring lines of code, bypassing the lexer + + ignoreDelimiters: false, // array of start/end delimiters used to ignore + esversion: 5 +}; +exports.unstable = { + bigint: true +}; +exports.inverted = { + bitwise : true, + forin : true, + newcap : true, + plusplus: true, + regexp : true, + undef : true, + eqeqeq : true, + strict : true +}; + +exports.validNames = Object.keys(exports.val) + .concat(Object.keys(exports.bool.relaxing)) + .concat(Object.keys(exports.bool.enforcing)) + .concat(Object.keys(exports.bool.obsolete)) + .concat(Object.keys(exports.bool.environments)) + .concat(["unstable"]); + +exports.unstableNames = Object.keys(exports.unstable); +exports.renamed = { + eqeq : "eqeqeq", + windows: "wsh", + sloppy : "strict" +}; + +exports.removed = { + nomen: true, + onevar: true, + passfail: true, + white: true, + gcl: true, + smarttabs: true, + trailing: true +}; +exports.noenforceall = { + varstmt: true, + strict: true, + regexpu: true +}; + +},{}],"/../../jshint/src/prod-params.js":[function(_dereq_,module,exports){ +module.exports = { + export: 1, + noin: 2, + initial: 4, + + preAsync: 8, + + async: 16, + tryClause: 32, + yield: 64 +}; + +},{}],"/../../jshint/src/reg.js":[function(_dereq_,module,exports){ + +"use strict"; +exports.unsafeString = + /@cc|<\/?|script|\]\s*\]|<\s*!|</i; +exports.needEsc = + /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/; + +exports.needEscGlobal = + /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; +exports.starSlash = /\*\//; +exports.identifier = /^([a-zA-Z_$][a-zA-Z0-9_$]*)$/; +exports.javascriptURL = /^(?:javascript|jscript|ecmascript|vbscript|livescript)\s*:/i; +exports.fallsThrough = /^\s*falls?\sthrough\s*$/; +exports.maxlenException = /^(?:(?:\/\/|\/\*|\*) ?)?[^ ]+$/; +exports.whitespace = /[ \f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/; + +exports.nonzeroDigit = /^[1-9]$/; + +exports.decimalDigit = /^[0-9]$/; + +exports.regexpSyntaxChars = /[\^$\\.*+?()[\]{}|]/; + +exports.regexpQuantifiers = /[*+?{]/; + +exports.regexpControlEscapes = /[fnrtv]/; + +exports.regexpCharClasses = /[dDsSwW]/; +exports.regexpDot = /(^|[^\\])(\\\\)*\./; + +},{}],"/../../jshint/src/scope-manager.js":[function(_dereq_,module,exports){ +"use strict"; + +var _ = _dereq_("underscore"); +_.slice = _dereq_("lodash.slice"); +var events = _dereq_("events"); +var marker = {}; +var scopeManager = function(state, predefined, exported, declared) { + + var _current; + var _scopeStack = []; + + function _newScope(type) { + _current = { + "(bindings)": Object.create(null), + "(usages)": Object.create(null), + "(labels)": Object.create(null), + "(parent)": _current, + "(type)": type, + "(params)": (type === "functionparams" || type === "catchparams") ? [] : null + }; + _scopeStack.push(_current); + } + + _newScope("global"); + _current["(predefined)"] = predefined; + + var _currentFunctBody = _current; // this is the block after the params = function + + var usedPredefinedAndGlobals = Object.create(null); + var impliedGlobals = Object.create(null); + var unuseds = []; + var emitter = new events.EventEmitter(); + + function warning(code, token) { + emitter.emit("warning", { + code: code, + token: token, + data: _.slice(arguments, 2) + }); + } + + function error(code, token) { + emitter.emit("warning", { + code: code, + token: token, + data: _.slice(arguments, 2) + }); + } + + function _setupUsages(bindingName) { + if (!_current["(usages)"][bindingName]) { + _current["(usages)"][bindingName] = { + "(modified)": [], + "(reassigned)": [], + "(tokens)": [] + }; + } + } + + var _getUnusedOption = function(unused_opt) { + if (unused_opt === undefined) { + unused_opt = state.option.unused; + } + + if (unused_opt === true) { + unused_opt = "last-param"; + } + + return unused_opt; + }; + + var _warnUnused = function(name, tkn, type, unused_opt) { + var line = tkn.line; + var chr = tkn.from; + var raw_name = tkn.raw_text || name; + + unused_opt = _getUnusedOption(unused_opt); + + var warnable_types = { + "vars": ["var"], + "last-param": ["var", "param"], + "strict": ["var", "param", "last-param"] + }; + + if (unused_opt) { + if (warnable_types[unused_opt] && warnable_types[unused_opt].indexOf(type) !== -1) { + warning("W098", { line: line, from: chr }, raw_name); + } + } + if (unused_opt || type === "var") { + unuseds.push({ + name: name, + line: line, + character: chr + }); + } + }; + function _checkForUnused() { + if (_current["(type)"] === "functionparams") { + _checkParams(); + return; + } + var currentBindings = _current["(bindings)"]; + for (var bindingName in currentBindings) { + if (currentBindings[bindingName]["(type)"] !== "exception" && + currentBindings[bindingName]["(unused)"]) { + _warnUnused(bindingName, currentBindings[bindingName]["(token)"], "var"); + } + } + } + function _checkParams() { + var params = _current["(params)"]; + + if (!params) { + return; + } + + var param = params.pop(); + var unused_opt; + + while (param) { + var binding = _current["(bindings)"][param]; + + unused_opt = _getUnusedOption(state.funct["(unusedOption)"]); + if (param === "undefined") + return; + + if (binding["(unused)"]) { + _warnUnused(param, binding["(token)"], "param", state.funct["(unusedOption)"]); + } else if (unused_opt === "last-param") { + return; + } + + param = params.pop(); + } + } + function _getBinding(bindingName) { + for (var i = _scopeStack.length - 1 ; i >= 0; --i) { + var scopeBindings = _scopeStack[i]["(bindings)"]; + if (scopeBindings[bindingName]) { + return scopeBindings; + } + } + } + function usedSoFarInCurrentFunction(bindingName) { + for (var i = _scopeStack.length - 1; i >= 0; i--) { + var current = _scopeStack[i]; + if (current["(usages)"][bindingName]) { + return current["(usages)"][bindingName]; + } + if (current === _currentFunctBody) { + break; + } + } + return false; + } + + function _checkOuterShadow(bindingName, token) { + if (state.option.shadow !== "outer") { + return; + } + + var isGlobal = _currentFunctBody["(type)"] === "global", + isNewFunction = _current["(type)"] === "functionparams"; + + var outsideCurrentFunction = !isGlobal; + for (var i = 0; i < _scopeStack.length; i++) { + var stackItem = _scopeStack[i]; + + if (!isNewFunction && _scopeStack[i + 1] === _currentFunctBody) { + outsideCurrentFunction = false; + } + if (outsideCurrentFunction && stackItem["(bindings)"][bindingName]) { + warning("W123", token, bindingName); + } + if (stackItem["(labels)"][bindingName]) { + warning("W123", token, bindingName); + } + } + } + + function _latedefWarning(type, bindingName, token) { + var isFunction; + + if (state.option.latedef) { + isFunction = type === "function" || type === "generator function" || + type === "async function"; + if ((state.option.latedef === true && isFunction) || !isFunction) { + warning("W003", token, bindingName); + } + } + } + + var scopeManagerInst = { + + on: function(names, listener) { + names.split(" ").forEach(function(name) { + emitter.on(name, listener); + }); + }, + + isPredefined: function(bindingName) { + return !this.has(bindingName) && _.has(_scopeStack[0]["(predefined)"], bindingName); + }, + stack: function(type) { + var previousScope = _current; + _newScope(type); + + if (!type && previousScope["(type)"] === "functionparams") { + + _current["(isFuncBody)"] = true; + _currentFunctBody = _current; + } + }, + unstack: function() { + var subScope = _scopeStack.length > 1 ? _scopeStack[_scopeStack.length - 2] : null; + var isUnstackingFunctionBody = _current === _currentFunctBody, + isUnstackingFunctionParams = _current["(type)"] === "functionparams", + isUnstackingFunctionOuter = _current["(type)"] === "functionouter"; + + var i, j, isImmutable, isFunction; + var currentUsages = _current["(usages)"]; + var currentBindings = _current["(bindings)"]; + var usedBindingNameList = Object.keys(currentUsages); + if (currentUsages.__proto__ && usedBindingNameList.indexOf("__proto__") === -1) { + usedBindingNameList.push("__proto__"); + } + + for (i = 0; i < usedBindingNameList.length; i++) { + var usedBindingName = usedBindingNameList[i]; + + var usage = currentUsages[usedBindingName]; + var usedBinding = currentBindings[usedBindingName]; + if (usedBinding) { + var usedBindingType = usedBinding["(type)"]; + isImmutable = usedBindingType === "const" || usedBindingType === "import"; + + if (usedBinding["(useOutsideOfScope)"] && !state.option.funcscope) { + var usedTokens = usage["(tokens)"]; + for (j = 0; j < usedTokens.length; j++) { + if (usedBinding["(function)"] === usedTokens[j]["(function)"]) { + error("W038", usedTokens[j], usedBindingName); + } + } + } + _current["(bindings)"][usedBindingName]["(unused)"] = false; + if (isImmutable && usage["(modified)"]) { + for (j = 0; j < usage["(modified)"].length; j++) { + error("E013", usage["(modified)"][j], usedBindingName); + } + } + + isFunction = usedBindingType === "function" || + usedBindingType === "generator function" || + usedBindingType === "async function"; + if ((isFunction || usedBindingType === "class") && usage["(reassigned)"]) { + for (j = 0; j < usage["(reassigned)"].length; j++) { + if (!usage["(reassigned)"][j].ignoreW021) { + warning("W021", usage["(reassigned)"][j], usedBindingName, usedBindingType); + } + } + } + continue; + } + + if (subScope) { + var bindingType = this.bindingtype(usedBindingName); + isImmutable = bindingType === "const" || + (bindingType === null && _scopeStack[0]["(predefined)"][usedBindingName] === false); + if (isUnstackingFunctionOuter && !isImmutable) { + if (!state.funct["(outerMutables)"]) { + state.funct["(outerMutables)"] = []; + } + state.funct["(outerMutables)"].push(usedBindingName); + } + if (!subScope["(usages)"][usedBindingName]) { + subScope["(usages)"][usedBindingName] = usage; + if (isUnstackingFunctionBody) { + subScope["(usages)"][usedBindingName]["(onlyUsedSubFunction)"] = true; + } + } else { + var subScopeUsage = subScope["(usages)"][usedBindingName]; + subScopeUsage["(modified)"] = subScopeUsage["(modified)"].concat(usage["(modified)"]); + subScopeUsage["(tokens)"] = subScopeUsage["(tokens)"].concat(usage["(tokens)"]); + subScopeUsage["(reassigned)"] = + subScopeUsage["(reassigned)"].concat(usage["(reassigned)"]); + } + } else { + if (typeof _current["(predefined)"][usedBindingName] === "boolean") { + delete declared[usedBindingName]; + usedPredefinedAndGlobals[usedBindingName] = marker; + if (_current["(predefined)"][usedBindingName] === false && usage["(reassigned)"]) { + for (j = 0; j < usage["(reassigned)"].length; j++) { + if (!usage["(reassigned)"][j].ignoreW020) { + warning("W020", usage["(reassigned)"][j]); + } + } + } + } + else { + for (j = 0; j < usage["(tokens)"].length; j++) { + var undefinedToken = usage["(tokens)"][j]; + if (!undefinedToken.forgiveUndef) { + if (state.option.undef && !undefinedToken.ignoreUndef) { + warning("W117", undefinedToken, usedBindingName); + } + if (impliedGlobals[usedBindingName]) { + impliedGlobals[usedBindingName].line.push(undefinedToken.line); + } else { + impliedGlobals[usedBindingName] = { + name: usedBindingName, + line: [undefinedToken.line] + }; + } + } + } + } + } + } + if (!subScope) { + Object.keys(declared) + .forEach(function(bindingNotUsed) { + _warnUnused(bindingNotUsed, declared[bindingNotUsed], "var"); + }); + } + if (subScope && !isUnstackingFunctionBody && + !isUnstackingFunctionParams && !isUnstackingFunctionOuter) { + var bindingNames = Object.keys(currentBindings); + for (i = 0; i < bindingNames.length; i++) { + + var defBindingName = bindingNames[i]; + var defBinding = currentBindings[defBindingName]; + + if (!defBinding["(blockscoped)"] && defBinding["(type)"] !== "exception") { + var shadowed = subScope["(bindings)"][defBindingName]; + if (shadowed) { + shadowed["(unused)"] &= defBinding["(unused)"]; + } else { + defBinding["(useOutsideOfScope)"] = + _currentFunctBody["(type)"] !== "global" && + !this.funct.has(defBindingName, { excludeCurrent: true }); + + subScope["(bindings)"][defBindingName] = defBinding; + } + + delete currentBindings[defBindingName]; + } + } + } + + _checkForUnused(); + + _scopeStack.pop(); + if (isUnstackingFunctionBody) { + _currentFunctBody = _scopeStack[_.findLastIndex(_scopeStack, function(scope) { + return scope["(isFuncBody)"] || scope["(type)"] === "global"; + })]; + } + + _current = subScope; + }, + addParam: function(bindingName, token, type) { + type = type || "param"; + + if (type === "exception") { + var previouslyDefinedBindingType = this.funct.bindingtype(bindingName); + if (previouslyDefinedBindingType && previouslyDefinedBindingType !== "exception") { + if (!state.option.node) { + warning("W002", state.tokens.next, bindingName); + } + } + + if (state.isStrict() && (bindingName === "arguments" || bindingName === "eval")) { + warning("E008", token); + } + } + if (_.has(_current["(bindings)"], bindingName)) { + _current["(bindings)"][bindingName].duplicated = true; + } else { + _checkOuterShadow(bindingName, token); + + _current["(bindings)"][bindingName] = { + "(type)" : type, + "(token)": token, + "(unused)": true }; + + _current["(params)"].push(bindingName); + } + + if (_.has(_current["(usages)"], bindingName)) { + var usage = _current["(usages)"][bindingName]; + if (usage["(onlyUsedSubFunction)"]) { + _latedefWarning(type, bindingName, token); + } else { + warning("E056", token, bindingName, type); + } + } + }, + + validateParams: function(isArrow) { + var isStrict = state.isStrict(); + var currentFunctParamScope = _currentFunctBody["(parent)"]; + var isSimple = state.funct['(hasSimpleParams)']; + var isMethod = state.funct["(method)"]; + + if (!currentFunctParamScope["(params)"]) { + return; + } + + currentFunctParamScope["(params)"].forEach(function(bindingName) { + var binding = currentFunctParamScope["(bindings)"][bindingName]; + + if (binding.duplicated) { + if (isStrict || isArrow || isMethod || !isSimple) { + warning("E011", binding["(token)"], bindingName); + } else if (state.option.shadow !== true) { + warning("W004", binding["(token)"], bindingName); + } + } + + if (isStrict && (bindingName === "arguments" || bindingName === "eval")) { + warning("E008", binding["(token)"]); + } + }); + }, + + getUsedOrDefinedGlobals: function() { + var list = Object.keys(usedPredefinedAndGlobals); + if (usedPredefinedAndGlobals.__proto__ === marker && + list.indexOf("__proto__") === -1) { + list.push("__proto__"); + } + + return list; + }, + getImpliedGlobals: function() { + var values = _.values(impliedGlobals); + var hasProto = false; + if (impliedGlobals.__proto__) { + hasProto = values.some(function(value) { + return value.name === "__proto__"; + }); + if (!hasProto) { + values.push(impliedGlobals.__proto__); + } + } + + return values; + }, + getUnuseds: function() { + return unuseds; + }, + has: function(bindingName) { + return Boolean(_getBinding(bindingName)); + }, + bindingtype: function(bindingName) { + var scopeBindings = _getBinding(bindingName); + if (scopeBindings) { + return scopeBindings[bindingName]["(type)"]; + } + return null; + }, + addExported: function(bindingName) { + var globalBindings = _scopeStack[0]["(bindings)"]; + if (_.has(declared, bindingName)) { + delete declared[bindingName]; + } else if (_.has(globalBindings, bindingName)) { + globalBindings[bindingName]["(unused)"] = false; + } else { + for (var i = 1; i < _scopeStack.length; i++) { + var scope = _scopeStack[i]; + if (!scope["(type)"]) { + if (_.has(scope["(bindings)"], bindingName) && + !scope["(bindings)"][bindingName]["(blockscoped)"]) { + scope["(bindings)"][bindingName]["(unused)"] = false; + return; + } + } else { + break; + } + } + exported[bindingName] = true; + } + }, + setExported: function(bindingName, token) { + this.block.use(bindingName, token); + }, + initialize: function(bindingName) { + if (_current["(bindings)"][bindingName]) { + _current["(bindings)"][bindingName]["(initialized)"] = true; + } + }, + addbinding: function(bindingName, opts) { + + var type = opts.type; + var token = opts.token; + var isblockscoped = type === "let" || type === "const" || + type === "class" || type === "import" || type === "generator function" || + type === "async function" || type === "async generator function"; + var ishoisted = type === "function" || type === "generator function" || + type === "async function" || type === "import"; + var isexported = (isblockscoped ? _current : _currentFunctBody)["(type)"] === "global" && + _.has(exported, bindingName); + _checkOuterShadow(bindingName, token); + + if (state.isStrict() && (bindingName === "arguments" || bindingName === "eval")) { + warning("E008", token); + } + + if (isblockscoped) { + + var declaredInCurrentScope = _current["(bindings)"][bindingName]; + if (!declaredInCurrentScope && _current === _currentFunctBody && + _current["(type)"] !== "global") { + declaredInCurrentScope = !!_currentFunctBody["(parent)"]["(bindings)"][bindingName]; + } + if (!declaredInCurrentScope && _current["(usages)"][bindingName]) { + var usage = _current["(usages)"][bindingName]; + if (usage["(onlyUsedSubFunction)"] || ishoisted) { + _latedefWarning(type, bindingName, token); + } else if (!ishoisted) { + warning("E056", token, bindingName, type); + } + } + if (declaredInCurrentScope && + (!ishoisted || (_current["(type)"] !== "global" || type === "import"))) { + warning("E011", token, bindingName); + } + else if (state.option.shadow === "outer") { + if (scopeManagerInst.funct.has(bindingName)) { + warning("W004", token, bindingName); + } + } + + scopeManagerInst.block.add( + bindingName, type, token, !isexported, opts.initialized + ); + + } else { + + var declaredInCurrentFunctionScope = scopeManagerInst.funct.has(bindingName); + if (!declaredInCurrentFunctionScope && usedSoFarInCurrentFunction(bindingName)) { + _latedefWarning(type, bindingName, token); + } + if (scopeManagerInst.funct.has(bindingName, { onlyBlockscoped: true })) { + warning("E011", token, bindingName); + } else if (state.option.shadow !== true) { + if (declaredInCurrentFunctionScope && bindingName !== "__proto__") { + if (_currentFunctBody["(type)"] !== "global") { + warning("W004", token, bindingName); + } + } + } + + scopeManagerInst.funct.add(bindingName, type, token, !isexported); + + if (_currentFunctBody["(type)"] === "global" && !state.impliedClosure()) { + usedPredefinedAndGlobals[bindingName] = marker; + } + } + }, + + funct: { + bindingtype: function(bindingName, options) { + var onlyBlockscoped = options && options.onlyBlockscoped; + var excludeParams = options && options.excludeParams; + var currentScopeIndex = _scopeStack.length - (options && options.excludeCurrent ? 2 : 1); + for (var i = currentScopeIndex; i >= 0; i--) { + var current = _scopeStack[i]; + if (current["(bindings)"][bindingName] && + (!onlyBlockscoped || current["(bindings)"][bindingName]["(blockscoped)"])) { + return current["(bindings)"][bindingName]["(type)"]; + } + var scopeCheck = excludeParams ? _scopeStack[ i - 1 ] : current; + if (scopeCheck && scopeCheck["(type)"] === "functionparams") { + return null; + } + } + return null; + }, + hasLabel: function(labelName) { + for (var i = _scopeStack.length - 1; i >= 0; i--) { + var current = _scopeStack[i]; + + if (current["(labels)"][labelName]) { + return true; + } + if (current["(type)"] === "functionparams") { + return false; + } + } + return false; + }, + has: function(bindingName, options) { + return Boolean(this.bindingtype(bindingName, options)); + }, + add: function(bindingName, type, tok, unused) { + _current["(bindings)"][bindingName] = { + "(type)" : type, + "(token)": tok, + "(blockscoped)": false, + "(function)": _currentFunctBody, + "(unused)": unused }; + } + }, + + block: { + isGlobal: function() { + return _current["(type)"] === "global"; + }, + use: function(bindingName, token) { + var paramScope = _currentFunctBody["(parent)"]; + if (paramScope && paramScope["(bindings)"][bindingName] && + paramScope["(bindings)"][bindingName]["(type)"] === "param") { + if (!scopeManagerInst.funct.has(bindingName, + { excludeParams: true, onlyBlockscoped: true })) { + paramScope["(bindings)"][bindingName]["(unused)"] = false; + } + } + + if (token && (state.ignored.W117 || state.option.undef === false)) { + token.ignoreUndef = true; + } + + _setupUsages(bindingName); + + _current["(usages)"][bindingName]["(onlyUsedSubFunction)"] = false; + + if (token) { + token["(function)"] = _currentFunctBody; + _current["(usages)"][bindingName]["(tokens)"].push(token); + } + var binding = _current["(bindings)"][bindingName]; + if (binding && binding["(blockscoped)"] && !binding["(initialized)"]) { + error("E056", token, bindingName, binding["(type)"]); + } + }, + + reassign: function(bindingName, token) { + token.ignoreW020 = state.ignored.W020; + token.ignoreW021 = state.ignored.W021; + + this.modify(bindingName, token); + + _current["(usages)"][bindingName]["(reassigned)"].push(token); + }, + + modify: function(bindingName, token) { + + _setupUsages(bindingName); + + _current["(usages)"][bindingName]["(onlyUsedSubFunction)"] = false; + _current["(usages)"][bindingName]["(modified)"].push(token); + }, + add: function(bindingName, type, tok, unused, initialized) { + _current["(bindings)"][bindingName] = { + "(type)" : type, + "(token)": tok, + "(initialized)": !!initialized, + "(blockscoped)": true, + "(unused)": unused }; + }, + + addLabel: function(labelName, opts) { + var token = opts.token; + if (scopeManagerInst.funct.hasLabel(labelName)) { + warning("E011", token, labelName); + } + else if (state.option.shadow === "outer") { + if (scopeManagerInst.funct.has(labelName)) { + warning("W004", token, labelName); + } else { + _checkOuterShadow(labelName, token); + } + } + _current["(labels)"][labelName] = token; + } + } + }; + return scopeManagerInst; +}; + +module.exports = scopeManager; + +},{"events":"/../node_modules/events/events.js","lodash.slice":"/../../jshint/node_modules/lodash.slice/index.js","underscore":"/../../jshint/node_modules/underscore/underscore.js"}],"/../../jshint/src/state.js":[function(_dereq_,module,exports){ +"use strict"; +var NameStack = _dereq_("./name-stack.js"); + +var state = { + syntax: {}, + isStrict: function() { + return this.directive["use strict"] || this.inClassBody || + this.option.module || this.option.strict === "implied"; + }, + stmtMissingStrict: function() { + if (this.option.strict === "global") { + return true; + } + + if (this.option.strict === false) { + return false; + } + + if (this.option.globalstrict) { + return true; + } + + return false; + }, + + allowsGlobalUsd: function() { + return this.option.strict === "global" || this.option.globalstrict || + this.option.module || this.impliedClosure(); + }, + impliedClosure: function() { + return this.option.node || this.option.phantom || this.option.browserify; + }, + + inMoz: function() { + return this.option.moz; + }, + inES10: function() { + return this.esVersion >= 10; + }, + inES9: function() { + return this.esVersion >= 9; + }, + inES8: function() { + return this.esVersion >= 8; + }, + inES7: function() { + return this.esVersion >= 7; + }, + inES6: function(strict) { + if (!strict && this.option.moz) { + return true; + } + + return this.esVersion >= 6; + }, + inES5: function() { + return !this.esVersion || this.esVersion >= 5 || this.option.moz; + }, + inferEsVersion: function() { + var badOpt = null; + + if (this.option.esversion) { + if (this.option.es3) { + badOpt = "es3"; + } else if (this.option.es5) { + badOpt = "es5"; + } else if (this.option.esnext) { + badOpt = "esnext"; + } + + if (badOpt) { + return badOpt; + } + + if (this.option.esversion === 2015) { + this.esVersion = 6; + } else { + this.esVersion = this.option.esversion; + } + } else if (this.option.es3) { + this.esVersion = 3; + } else if (this.option.esnext) { + this.esVersion = 6; + } + + return null; + }, + + reset: function() { + this.tokens = { + prev: null, + next: null, + curr: null + }; + + this.option = { unstable: {} }; + this.esVersion = 5; + this.funct = null; + this.ignored = {}; + this.directive = Object.create(null); + this.jsonMode = false; + this.lines = []; + this.tab = ""; + this.cache = {}; // Node.JS doesn't have Map. Sniff. + this.ignoredLines = {}; + this.forinifcheckneeded = false; + this.nameStack = new NameStack(); + this.inClassBody = false; + } +}; + +exports.state = state; + +},{"./name-stack.js":"/../../jshint/src/name-stack.js"}],"/../../jshint/src/style.js":[function(_dereq_,module,exports){ +"use strict"; + +exports.register = function(linter) { + + linter.on("Identifier", function style_scanProto(data) { + if (linter.getOption("proto")) { + return; + } + + if (data.name === "__proto__") { + linter.warn("W103", { + line: data.line, + char: data.char, + data: [ data.name, "6" ] + }); + } + }); + + linter.on("Identifier", function style_scanIterator(data) { + if (linter.getOption("iterator")) { + return; + } + + if (data.name === "__iterator__") { + linter.warn("W103", { + line: data.line, + char: data.char, + data: [ data.name ] + }); + } + }); + + linter.on("Identifier", function style_scanCamelCase(data) { + if (!linter.getOption("camelcase")) { + return; + } + + if (data.name.replace(/^_+|_+$/g, "").indexOf("_") > -1 && !data.name.match(/^[A-Z0-9_]*$/)) { + linter.warn("W106", { + line: data.line, + char: data.char, + data: [ data.name ] + }); + } + }); + + linter.on("String", function style_scanQuotes(data) { + var quotmark = linter.getOption("quotmark"); + var code; + + if (!quotmark) { + return; + } + + if (quotmark === "single" && data.quote !== "'") { + code = "W109"; + } + + if (quotmark === "double" && data.quote !== "\"") { + code = "W108"; + } + + if (quotmark === true) { + if (!linter.getCache("quotmark")) { + linter.setCache("quotmark", data.quote); + } + + if (linter.getCache("quotmark") !== data.quote) { + code = "W110"; + } + } + + if (code) { + linter.warn(code, { + line: data.line, + char: data.char, + }); + } + }); + + linter.on("Number", function style_scanNumbers(data) { + if (data.value.charAt(0) === ".") { + linter.warn("W008", { + line: data.line, + char: data.char, + data: [ data.value ] + }); + } + + if (data.value.substr(data.value.length - 1) === ".") { + linter.warn("W047", { + line: data.line, + char: data.char, + data: [ data.value ] + }); + } + + if (/^00+/.test(data.value)) { + linter.warn("W046", { + line: data.line, + char: data.char, + data: [ data.value ] + }); + } + }); + + linter.on("String", function style_scanJavaScriptURLs(data) { + var re = /^(?:javascript|jscript|ecmascript|vbscript|livescript)\s*:/i; + + if (linter.getOption("scripturl")) { + return; + } + + if (re.test(data.value)) { + linter.warn("W107", { + line: data.line, + char: data.char + }); + } + }); +}; + +},{}],"/../../jshint/src/vars.js":[function(_dereq_,module,exports){ + +"use strict"; + +exports.reservedVars = { + NaN : false, + undefined : false +}; + +exports.ecmaIdentifiers = { + 3: { + Array : false, + Boolean : false, + Date : false, + decodeURI : false, + decodeURIComponent : false, + encodeURI : false, + encodeURIComponent : false, + Error : false, + "eval" : false, + EvalError : false, + Function : false, + hasOwnProperty : false, + Infinity : false, + isFinite : false, + isNaN : false, + Math : false, + Number : false, + Object : false, + parseInt : false, + parseFloat : false, + RangeError : false, + ReferenceError : false, + RegExp : false, + String : false, + SyntaxError : false, + TypeError : false, + URIError : false + }, + 5: { + JSON : false + }, + 6: { + ArrayBuffer : false, + DataView : false, + Float32Array : false, + Float64Array : false, + Int8Array : false, + Int16Array : false, + Int32Array : false, + Map : false, + Promise : false, + Proxy : false, + Reflect : false, + Set : false, + Symbol : false, + Uint8Array : false, + Uint16Array : false, + Uint32Array : false, + Uint8ClampedArray : false, + WeakMap : false, + WeakSet : false + }, + 8: { + Atomics : false, + SharedArrayBuffer : false + } +}; + +exports.browser = { + Audio : false, + Blob : false, + addEventListener : false, // EventTarget + applicationCache : false, + atob : false, // WindowOrWorkerGlobalScope + blur : false, + btoa : false, // WindowOrWorkerGlobalScope + cancelAnimationFrame : false, + CanvasGradient : false, + CanvasPattern : false, + CanvasRenderingContext2D: false, + CSS : false, + CSSImportRule : false, + CSSGroupingRule : false, + CSSMarginRule : false, + CSSMediaRule : false, + CSSNamespaceRule : false, + CSSPageRule : false, + CSSRule : false, + CSSRuleList : false, + CSSStyleDeclaration : false, + CSSStyleRule : false, + CSSStyleSheet : false, + clearInterval : false, // WindowOrWorkerGlobalScope + clearTimeout : false, // WindowOrWorkerGlobalScope + close : false, + closed : false, + Comment : false, + CompositionEvent : false, + createImageBitmap : false, // WindowOrWorkerGlobalScope + CustomEvent : false, + DOMParser : false, + defaultStatus : false, + dispatchEvent : false, // EventTarget + Document : false, + document : false, + DocumentFragment : false, + Element : false, + ElementTimeControl : false, + Event : false, + event : false, + fetch : false, + File : false, + FileList : false, + FileReader : false, + FormData : false, + focus : false, + frames : false, + getComputedStyle : false, + Headers : false, + HTMLAnchorElement : false, + HTMLAreaElement : false, + HTMLAudioElement : false, + HTMLBaseElement : false, + HTMLBlockquoteElement: false, + HTMLBodyElement : false, + HTMLBRElement : false, + HTMLButtonElement : false, + HTMLCanvasElement : false, + HTMLCollection : false, + HTMLDataElement : false, + HTMLDataListElement : false, + HTMLDetailsElement : false, + HTMLDialogElement : false, + HTMLDirectoryElement : false, + HTMLDivElement : false, + HTMLDListElement : false, + HTMLElement : false, + HTMLEmbedElement : false, + HTMLFieldSetElement : false, + HTMLFontElement : false, + HTMLFormElement : false, + HTMLFrameElement : false, + HTMLFrameSetElement : false, + HTMLHeadElement : false, + HTMLHeadingElement : false, + HTMLHRElement : false, + HTMLHtmlElement : false, + HTMLIFrameElement : false, + HTMLImageElement : false, + HTMLInputElement : false, + HTMLIsIndexElement : false, + HTMLLabelElement : false, + HTMLLayerElement : false, + HTMLLegendElement : false, + HTMLLIElement : false, + HTMLLinkElement : false, + HTMLMapElement : false, + HTMLMarqueeElement : false, + HTMLMediaElement : false, + HTMLMenuElement : false, + HTMLMetaElement : false, + HTMLMeterElement : false, + HTMLModElement : false, + HTMLObjectElement : false, + HTMLOListElement : false, + HTMLOptGroupElement : false, + HTMLOptionElement : false, + HTMLParagraphElement : false, + HTMLParamElement : false, + HTMLPictureElement : false, + HTMLPreElement : false, + HTMLProgressElement : false, + HTMLQuoteElement : false, + HTMLScriptElement : false, + HTMLSelectElement : false, + HTMLSlotElement : false, + HTMLSourceElement : false, + HTMLStyleElement : false, + HTMLTableCaptionElement: false, + HTMLTableCellElement : false, + HTMLTableColElement : false, + HTMLTableElement : false, + HTMLTableRowElement : false, + HTMLTableSectionElement: false, + HTMLTemplateElement : false, + HTMLTextAreaElement : false, + HTMLTimeElement : false, + HTMLTitleElement : false, + HTMLTrackElement : false, + HTMLUListElement : false, + HTMLVideoElement : false, + history : false, + Image : false, + IntersectionObserver : false, + Intl : false, + length : false, + localStorage : false, + location : false, + matchMedia : false, + MediaList : false, + MediaRecorder : false, + MessageChannel : false, + MessageEvent : false, + MessagePort : false, + MouseEvent : false, + moveBy : false, + moveTo : false, + MutationObserver : false, + name : false, + Node : false, + NodeFilter : false, + NodeList : false, + Notification : false, + navigator : false, + onbeforeunload : true, + onblur : true, + onerror : true, + onfocus : true, + onload : true, + onresize : true, + onunload : true, + open : false, + openDatabase : false, + opener : false, + Option : false, + origin : false, // WindowOrWorkerGlobalScope + parent : false, + performance : false, + print : false, + queueMicrotask : false, // WindowOrWorkerGlobalScope + Range : false, + requestAnimationFrame : false, + removeEventListener : false, // EventTarget + Request : false, + resizeBy : false, + resizeTo : false, + Response : false, + screen : false, + scroll : false, + scrollBy : false, + scrollTo : false, + sessionStorage : false, + setInterval : false, // WindowOrWorkerGlobalScope + setTimeout : false, // WindowOrWorkerGlobalScope + SharedWorker : false, + status : false, + Storage : false, + StyleSheet : false, + SVGAElement : false, + SVGAltGlyphDefElement: false, + SVGAltGlyphElement : false, + SVGAltGlyphItemElement: false, + SVGAngle : false, + SVGAnimateColorElement: false, + SVGAnimateElement : false, + SVGAnimateMotionElement: false, + SVGAnimateTransformElement: false, + SVGAnimatedAngle : false, + SVGAnimatedBoolean : false, + SVGAnimatedEnumeration: false, + SVGAnimatedInteger : false, + SVGAnimatedLength : false, + SVGAnimatedLengthList: false, + SVGAnimatedNumber : false, + SVGAnimatedNumberList: false, + SVGAnimatedPathData : false, + SVGAnimatedPoints : false, + SVGAnimatedPreserveAspectRatio: false, + SVGAnimatedRect : false, + SVGAnimatedString : false, + SVGAnimatedTransformList: false, + SVGAnimationElement : false, + SVGCSSRule : false, + SVGCircleElement : false, + SVGClipPathElement : false, + SVGColor : false, + SVGColorProfileElement: false, + SVGColorProfileRule : false, + SVGComponentTransferFunctionElement: false, + SVGCursorElement : false, + SVGDefsElement : false, + SVGDescElement : false, + SVGDocument : false, + SVGElement : false, + SVGElementInstance : false, + SVGElementInstanceList: false, + SVGEllipseElement : false, + SVGExternalResourcesRequired: false, + SVGFEBlendElement : false, + SVGFEColorMatrixElement: false, + SVGFEComponentTransferElement: false, + SVGFECompositeElement: false, + SVGFEConvolveMatrixElement: false, + SVGFEDiffuseLightingElement: false, + SVGFEDisplacementMapElement: false, + SVGFEDistantLightElement: false, + SVGFEFloodElement : false, + SVGFEFuncAElement : false, + SVGFEFuncBElement : false, + SVGFEFuncGElement : false, + SVGFEFuncRElement : false, + SVGFEGaussianBlurElement: false, + SVGFEImageElement : false, + SVGFEMergeElement : false, + SVGFEMergeNodeElement: false, + SVGFEMorphologyElement: false, + SVGFEOffsetElement : false, + SVGFEPointLightElement: false, + SVGFESpecularLightingElement: false, + SVGFESpotLightElement: false, + SVGFETileElement : false, + SVGFETurbulenceElement: false, + SVGFilterElement : false, + SVGFilterPrimitiveStandardAttributes: false, + SVGFitToViewBox : false, + SVGFontElement : false, + SVGFontFaceElement : false, + SVGFontFaceFormatElement: false, + SVGFontFaceNameElement: false, + SVGFontFaceSrcElement: false, + SVGFontFaceUriElement: false, + SVGForeignObjectElement: false, + SVGGElement : false, + SVGGlyphElement : false, + SVGGlyphRefElement : false, + SVGGradientElement : false, + SVGHKernElement : false, + SVGICCColor : false, + SVGImageElement : false, + SVGLangSpace : false, + SVGLength : false, + SVGLengthList : false, + SVGLineElement : false, + SVGLinearGradientElement: false, + SVGLocatable : false, + SVGMPathElement : false, + SVGMarkerElement : false, + SVGMaskElement : false, + SVGMatrix : false, + SVGMetadataElement : false, + SVGMissingGlyphElement: false, + SVGNumber : false, + SVGNumberList : false, + SVGPaint : false, + SVGPathElement : false, + SVGPathSeg : false, + SVGPathSegArcAbs : false, + SVGPathSegArcRel : false, + SVGPathSegClosePath : false, + SVGPathSegCurvetoCubicAbs: false, + SVGPathSegCurvetoCubicRel: false, + SVGPathSegCurvetoCubicSmoothAbs: false, + SVGPathSegCurvetoCubicSmoothRel: false, + SVGPathSegCurvetoQuadraticAbs: false, + SVGPathSegCurvetoQuadraticRel: false, + SVGPathSegCurvetoQuadraticSmoothAbs: false, + SVGPathSegCurvetoQuadraticSmoothRel: false, + SVGPathSegLinetoAbs : false, + SVGPathSegLinetoHorizontalAbs: false, + SVGPathSegLinetoHorizontalRel: false, + SVGPathSegLinetoRel : false, + SVGPathSegLinetoVerticalAbs: false, + SVGPathSegLinetoVerticalRel: false, + SVGPathSegList : false, + SVGPathSegMovetoAbs : false, + SVGPathSegMovetoRel : false, + SVGPatternElement : false, + SVGPoint : false, + SVGPointList : false, + SVGPolygonElement : false, + SVGPolylineElement : false, + SVGPreserveAspectRatio: false, + SVGRadialGradientElement: false, + SVGRect : false, + SVGRectElement : false, + SVGRenderingIntent : false, + SVGSVGElement : false, + SVGScriptElement : false, + SVGSetElement : false, + SVGStopElement : false, + SVGStringList : false, + SVGStylable : false, + SVGStyleElement : false, + SVGSwitchElement : false, + SVGSymbolElement : false, + SVGTRefElement : false, + SVGTSpanElement : false, + SVGTests : false, + SVGTextContentElement: false, + SVGTextElement : false, + SVGTextPathElement : false, + SVGTextPositioningElement: false, + SVGTitleElement : false, + SVGTransform : false, + SVGTransformList : false, + SVGTransformable : false, + SVGURIReference : false, + SVGUnitTypes : false, + SVGUseElement : false, + SVGVKernElement : false, + SVGViewElement : false, + SVGViewSpec : false, + SVGZoomAndPan : false, + Text : false, + TextDecoder : false, + TextEncoder : false, + TimeEvent : false, + top : false, + URL : false, + WebGLActiveInfo : false, + WebGLBuffer : false, + WebGLContextEvent : false, + WebGLFramebuffer : false, + WebGLProgram : false, + WebGLRenderbuffer : false, + WebGLRenderingContext: false, + WebGLShader : false, + WebGLShaderPrecisionFormat: false, + WebGLTexture : false, + WebGLUniformLocation : false, + WebSocket : false, + window : false, + Window : false, + Worker : false, + XDomainRequest : false, + XMLDocument : false, + XMLHttpRequest : false, + XMLSerializer : false, + XPathEvaluator : false, + XPathException : false, + XPathExpression : false, + XPathNamespace : false, + XPathNSResolver : false, + XPathResult : false +}; + +exports.devel = { + alert : false, + confirm: false, + console: false, + Debug : false, + opera : false, + prompt : false +}; + +exports.worker = { + addEventListener : true, // EventTarget + atob : true, // WindowOrWorkerGlobalScope + btoa : true, // WindowOrWorkerGlobalScope + clearInterval : true, // WindowOrWorkerGlobalScope + clearTimeout : true, // WindowOrWorkerGlobalScope + createImageBitmap : true, // WindowOrWorkerGlobalScope + dispatchEvent : true, // EventTarget + importScripts : true, + onmessage : true, + origin : true, // WindowOrWorkerGlobalScope + postMessage : true, + queueMicrotask : true, // WindowOrWorkerGlobalScope + removeEventListener : true, // EventTarget + self : true, + setInterval : true, // WindowOrWorkerGlobalScope + setTimeout : true, // WindowOrWorkerGlobalScope + FileReaderSync : true +}; +exports.nonstandard = { + escape : false, + unescape: false +}; + +exports.couch = { + "require" : false, + respond : false, + getRow : false, + emit : false, + send : false, + start : false, + sum : false, + log : false, + exports : false, + module : false, + provides : false +}; + +exports.node = { + __filename : false, + __dirname : false, + arguments : false, + GLOBAL : false, + global : false, + module : false, + require : false, + + Buffer : true, + console : true, + exports : true, + process : true, + setTimeout : true, + clearTimeout : true, + setInterval : true, + clearInterval : true, + setImmediate : true, // v0.9.1+ + clearImmediate: true // v0.9.1+ +}; + +exports.browserify = { + __filename : false, + __dirname : false, + global : false, + module : false, + require : false, + Buffer : true, + exports : true, + process : true +}; + +exports.phantom = { + phantom : true, + require : true, + WebPage : true, + console : true, // in examples, but undocumented + exports : true // v1.7+ +}; + +exports.qunit = { + asyncTest : false, + deepEqual : false, + equal : false, + expect : false, + module : false, + notDeepEqual : false, + notEqual : false, + notOk : false, + notPropEqual : false, + notStrictEqual : false, + ok : false, + propEqual : false, + QUnit : false, + raises : false, + start : false, + stop : false, + strictEqual : false, + test : false, + "throws" : false +}; + +exports.rhino = { + arguments : false, + defineClass : false, + deserialize : false, + gc : false, + help : false, + importClass : false, + importPackage: false, + "java" : false, + load : false, + loadClass : false, + Packages : false, + print : false, + quit : false, + readFile : false, + readUrl : false, + runCommand : false, + seal : false, + serialize : false, + spawn : false, + sync : false, + toint32 : false, + version : false +}; + +exports.shelljs = { + target : false, + echo : false, + exit : false, + cd : false, + pwd : false, + ls : false, + find : false, + cp : false, + rm : false, + mv : false, + mkdir : false, + test : false, + cat : false, + sed : false, + grep : false, + which : false, + dirs : false, + pushd : false, + popd : false, + env : false, + exec : false, + chmod : false, + config : false, + error : false, + tempdir : false +}; + +exports.typed = { + ArrayBuffer : false, + ArrayBufferView : false, + DataView : false, + Float32Array : false, + Float64Array : false, + Int16Array : false, + Int32Array : false, + Int8Array : false, + Uint16Array : false, + Uint32Array : false, + Uint8Array : false, + Uint8ClampedArray : false +}; + +exports.wsh = { + ActiveXObject : true, + Enumerator : true, + GetObject : true, + ScriptEngine : true, + ScriptEngineBuildVersion : true, + ScriptEngineMajorVersion : true, + ScriptEngineMinorVersion : true, + VBArray : true, + WSH : true, + WScript : true, + XDomainRequest : true +}; + +exports.dojo = { + dojo : false, + dijit : false, + dojox : false, + define : false, + "require": false +}; + +exports.jquery = { + "$" : false, + jQuery : false +}; + +exports.mootools = { + "$" : false, + "$$" : false, + Asset : false, + Browser : false, + Chain : false, + Class : false, + Color : false, + Cookie : false, + Core : false, + Document : false, + DomReady : false, + DOMEvent : false, + DOMReady : false, + Drag : false, + Element : false, + Elements : false, + Event : false, + Events : false, + Fx : false, + Group : false, + Hash : false, + HtmlTable : false, + IFrame : false, + IframeShim : false, + InputValidator: false, + instanceOf : false, + Keyboard : false, + Locale : false, + Mask : false, + MooTools : false, + Native : false, + Options : false, + OverText : false, + Request : false, + Scroller : false, + Slick : false, + Slider : false, + Sortables : false, + Spinner : false, + Swiff : false, + Tips : false, + Type : false, + typeOf : false, + URI : false, + Window : false +}; + +exports.prototypejs = { + "$" : false, + "$$" : false, + "$A" : false, + "$F" : false, + "$H" : false, + "$R" : false, + "$break" : false, + "$continue" : false, + "$w" : false, + Abstract : false, + Ajax : false, + Class : false, + Enumerable : false, + Element : false, + Event : false, + Field : false, + Form : false, + Hash : false, + Insertion : false, + ObjectRange : false, + PeriodicalExecuter: false, + Position : false, + Prototype : false, + Selector : false, + Template : false, + Toggle : false, + Try : false, + Autocompleter : false, + Builder : false, + Control : false, + Draggable : false, + Draggables : false, + Droppables : false, + Effect : false, + Sortable : false, + SortableObserver : false, + Sound : false, + Scriptaculous : false +}; + +exports.yui = { + YUI : false, + Y : false, + YUI_config: false +}; + +exports.mocha = { + mocha : false, + describe : false, + xdescribe : false, + it : false, + xit : false, + context : false, + xcontext : false, + before : false, + after : false, + beforeEach : false, + afterEach : false, + suite : false, + test : false, + setup : false, + teardown : false, + suiteSetup : false, + suiteTeardown : false +}; + +exports.jasmine = { + jasmine : false, + describe : false, + xdescribe : false, + it : false, + xit : false, + beforeEach : false, + afterEach : false, + setFixtures : false, + loadFixtures: false, + spyOn : false, + expect : false, + runs : false, + waitsFor : false, + waits : false, + beforeAll : false, + afterAll : false, + fail : false, + fdescribe : false, + fit : false, + pending : false, + spyOnProperty: false +}; + +},{}],"/../node_modules/assert/assert.js":[function(_dereq_,module,exports){ +(function (global){ +'use strict'; + +var objectAssign = _dereq_('object-assign'); +function compare(a, b) { + if (a === b) { + return 0; + } + + var x = a.length; + var y = b.length; + + for (var i = 0, len = Math.min(x, y); i < len; ++i) { + if (a[i] !== b[i]) { + x = a[i]; + y = b[i]; + break; + } + } + + if (x < y) { + return -1; + } + if (y < x) { + return 1; + } + return 0; +} +function isBuffer(b) { + if (global.Buffer && typeof global.Buffer.isBuffer === 'function') { + return global.Buffer.isBuffer(b); + } + return !!(b != null && b._isBuffer); +} + +var util = _dereq_('util/'); +var hasOwn = Object.prototype.hasOwnProperty; +var pSlice = Array.prototype.slice; +var functionsHaveNames = (function () { + return function foo() {}.name === 'foo'; +}()); +function pToString (obj) { + return Object.prototype.toString.call(obj); +} +function isView(arrbuf) { + if (isBuffer(arrbuf)) { + return false; + } + if (typeof global.ArrayBuffer !== 'function') { + return false; + } + if (typeof ArrayBuffer.isView === 'function') { + return ArrayBuffer.isView(arrbuf); + } + if (!arrbuf) { + return false; + } + if (arrbuf instanceof DataView) { + return true; + } + if (arrbuf.buffer && arrbuf.buffer instanceof ArrayBuffer) { + return true; + } + return false; +} + +var assert = module.exports = ok; + +var regex = /\s*function\s+([^\(\s]*)\s*/; +function getName(func) { + if (!util.isFunction(func)) { + return; + } + if (functionsHaveNames) { + return func.name; + } + var str = func.toString(); + var match = str.match(regex); + return match && match[1]; +} +assert.AssertionError = function AssertionError(options) { + this.name = 'AssertionError'; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + if (options.message) { + this.message = options.message; + this.generatedMessage = false; + } else { + this.message = getMessage(this); + this.generatedMessage = true; + } + var stackStartFunction = options.stackStartFunction || fail; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, stackStartFunction); + } else { + var err = new Error(); + if (err.stack) { + var out = err.stack; + var fn_name = getName(stackStartFunction); + var idx = out.indexOf('\n' + fn_name); + if (idx >= 0) { + var next_line = out.indexOf('\n', idx + 1); + out = out.substring(next_line + 1); + } + + this.stack = out; + } + } +}; +util.inherits(assert.AssertionError, Error); + +function truncate(s, n) { + if (typeof s === 'string') { + return s.length < n ? s : s.slice(0, n); + } else { + return s; + } +} +function inspect(something) { + if (functionsHaveNames || !util.isFunction(something)) { + return util.inspect(something); + } + var rawname = getName(something); + var name = rawname ? ': ' + rawname : ''; + return '[Function' + name + ']'; +} +function getMessage(self) { + return truncate(inspect(self.actual), 128) + ' ' + + self.operator + ' ' + + truncate(inspect(self.expected), 128); +} + +function fail(actual, expected, message, operator, stackStartFunction) { + throw new assert.AssertionError({ + message: message, + actual: actual, + expected: expected, + operator: operator, + stackStartFunction: stackStartFunction + }); +} +assert.fail = fail; + +function ok(value, message) { + if (!value) fail(value, true, message, '==', assert.ok); +} +assert.ok = ok; + +assert.equal = function equal(actual, expected, message) { + if (actual != expected) fail(actual, expected, message, '==', assert.equal); +}; + +assert.notEqual = function notEqual(actual, expected, message) { + if (actual == expected) { + fail(actual, expected, message, '!=', assert.notEqual); + } +}; + +assert.deepEqual = function deepEqual(actual, expected, message) { + if (!_deepEqual(actual, expected, false)) { + fail(actual, expected, message, 'deepEqual', assert.deepEqual); + } +}; + +assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) { + if (!_deepEqual(actual, expected, true)) { + fail(actual, expected, message, 'deepStrictEqual', assert.deepStrictEqual); + } +}; + +function _deepEqual(actual, expected, strict, memos) { + if (actual === expected) { + return true; + } else if (isBuffer(actual) && isBuffer(expected)) { + return compare(actual, expected) === 0; + } else if (util.isDate(actual) && util.isDate(expected)) { + return actual.getTime() === expected.getTime(); + } else if (util.isRegExp(actual) && util.isRegExp(expected)) { + return actual.source === expected.source && + actual.global === expected.global && + actual.multiline === expected.multiline && + actual.lastIndex === expected.lastIndex && + actual.ignoreCase === expected.ignoreCase; + } else if ((actual === null || typeof actual !== 'object') && + (expected === null || typeof expected !== 'object')) { + return strict ? actual === expected : actual == expected; + } else if (isView(actual) && isView(expected) && + pToString(actual) === pToString(expected) && + !(actual instanceof Float32Array || + actual instanceof Float64Array)) { + return compare(new Uint8Array(actual.buffer), + new Uint8Array(expected.buffer)) === 0; + } else if (isBuffer(actual) !== isBuffer(expected)) { + return false; + } else { + memos = memos || {actual: [], expected: []}; + + var actualIndex = memos.actual.indexOf(actual); + if (actualIndex !== -1) { + if (actualIndex === memos.expected.indexOf(expected)) { + return true; + } + } + + memos.actual.push(actual); + memos.expected.push(expected); + + return objEquiv(actual, expected, strict, memos); + } +} + +function isArguments(object) { + return Object.prototype.toString.call(object) == '[object Arguments]'; +} + +function objEquiv(a, b, strict, actualVisitedObjects) { + if (a === null || a === undefined || b === null || b === undefined) + return false; + if (util.isPrimitive(a) || util.isPrimitive(b)) + return a === b; + if (strict && Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) + return false; + var aIsArgs = isArguments(a); + var bIsArgs = isArguments(b); + if ((aIsArgs && !bIsArgs) || (!aIsArgs && bIsArgs)) + return false; + if (aIsArgs) { + a = pSlice.call(a); + b = pSlice.call(b); + return _deepEqual(a, b, strict); + } + var ka = objectKeys(a); + var kb = objectKeys(b); + var key, i; + if (ka.length !== kb.length) + return false; + ka.sort(); + kb.sort(); + for (i = ka.length - 1; i >= 0; i--) { + if (ka[i] !== kb[i]) + return false; + } + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!_deepEqual(a[key], b[key], strict, actualVisitedObjects)) + return false; + } + return true; +} + +assert.notDeepEqual = function notDeepEqual(actual, expected, message) { + if (_deepEqual(actual, expected, false)) { + fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); + } +}; + +assert.notDeepStrictEqual = notDeepStrictEqual; +function notDeepStrictEqual(actual, expected, message) { + if (_deepEqual(actual, expected, true)) { + fail(actual, expected, message, 'notDeepStrictEqual', notDeepStrictEqual); + } +} + +assert.strictEqual = function strictEqual(actual, expected, message) { + if (actual !== expected) { + fail(actual, expected, message, '===', assert.strictEqual); + } +}; + +assert.notStrictEqual = function notStrictEqual(actual, expected, message) { + if (actual === expected) { + fail(actual, expected, message, '!==', assert.notStrictEqual); + } +}; + +function expectedException(actual, expected) { + if (!actual || !expected) { + return false; + } + + if (Object.prototype.toString.call(expected) == '[object RegExp]') { + return expected.test(actual); + } + + try { + if (actual instanceof expected) { + return true; + } + } catch (e) { + } + + if (Error.isPrototypeOf(expected)) { + return false; + } + + return expected.call({}, actual) === true; +} + +function _tryBlock(block) { + var error; + try { + block(); + } catch (e) { + error = e; + } + return error; +} + +function _throws(shouldThrow, block, expected, message) { + var actual; + + if (typeof block !== 'function') { + throw new TypeError('"block" argument must be a function'); + } + + if (typeof expected === 'string') { + message = expected; + expected = null; + } + + actual = _tryBlock(block); + + message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + + (message ? ' ' + message : '.'); + + if (shouldThrow && !actual) { + fail(actual, expected, 'Missing expected exception' + message); + } + + var userProvidedMessage = typeof message === 'string'; + var isUnwantedException = !shouldThrow && util.isError(actual); + var isUnexpectedException = !shouldThrow && actual && !expected; + + if ((isUnwantedException && + userProvidedMessage && + expectedException(actual, expected)) || + isUnexpectedException) { + fail(actual, expected, 'Got unwanted exception' + message); + } + + if ((shouldThrow && actual && expected && + !expectedException(actual, expected)) || (!shouldThrow && actual)) { + throw actual; + } +} + +assert.throws = function(block, /*optional*/error, /*optional*/message) { + _throws(true, block, error, message); +}; +assert.doesNotThrow = function(block, /*optional*/error, /*optional*/message) { + _throws(false, block, error, message); +}; + +assert.ifError = function(err) { if (err) throw err; }; +function strict(value, message) { + if (!value) fail(value, true, message, '==', strict); +} +assert.strict = objectAssign(strict, assert, { + equal: assert.strictEqual, + deepEqual: assert.deepStrictEqual, + notEqual: assert.notStrictEqual, + notDeepEqual: assert.notDeepStrictEqual +}); +assert.strict.strict = assert.strict; + +var objectKeys = Object.keys || function (obj) { + var keys = []; + for (var key in obj) { + if (hasOwn.call(obj, key)) keys.push(key); + } + return keys; +}; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"object-assign":"/../node_modules/object-assign/index.js","util/":"/../node_modules/assert/node_modules/util/util.js"}],"/../node_modules/assert/node_modules/inherits/inherits_browser.js":[function(_dereq_,module,exports){ +if (typeof Object.create === 'function') { + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + }; +} else { + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } +} + +},{}],"/../node_modules/assert/node_modules/util/support/isBufferBrowser.js":[function(_dereq_,module,exports){ +module.exports = function isBuffer(arg) { + return arg && typeof arg === 'object' + && typeof arg.copy === 'function' + && typeof arg.fill === 'function' + && typeof arg.readUInt8 === 'function'; +} +},{}],"/../node_modules/assert/node_modules/util/util.js":[function(_dereq_,module,exports){ +(function (process,global){ + +var formatRegExp = /%[sdj%]/g; +exports.format = function(f) { + if (!isString(f)) { + var objects = []; + for (var i = 0; i < arguments.length; i++) { + objects.push(inspect(arguments[i])); + } + return objects.join(' '); + } + + var i = 1; + var args = arguments; + var len = args.length; + var str = String(f).replace(formatRegExp, function(x) { + if (x === '%%') return '%'; + if (i >= len) return x; + switch (x) { + case '%s': return String(args[i++]); + case '%d': return Number(args[i++]); + case '%j': + try { + return JSON.stringify(args[i++]); + } catch (_) { + return '[Circular]'; + } + default: + return x; + } + }); + for (var x = args[i]; i < len; x = args[++i]) { + if (isNull(x) || !isObject(x)) { + str += ' ' + x; + } else { + str += ' ' + inspect(x); + } + } + return str; +}; +exports.deprecate = function(fn, msg) { + if (isUndefined(global.process)) { + return function() { + return exports.deprecate(fn, msg).apply(this, arguments); + }; + } + + if (process.noDeprecation === true) { + return fn; + } + + var warned = false; + function deprecated() { + if (!warned) { + if (process.throwDeprecation) { + throw new Error(msg); + } else if (process.traceDeprecation) { + console.trace(msg); + } else { + console.error(msg); + } + warned = true; + } + return fn.apply(this, arguments); + } + + return deprecated; +}; + + +var debugs = {}; +var debugEnviron; +exports.debuglog = function(set) { + if (isUndefined(debugEnviron)) + debugEnviron = process.env.NODE_DEBUG || ''; + set = set.toUpperCase(); + if (!debugs[set]) { + if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) { + var pid = process.pid; + debugs[set] = function() { + var msg = exports.format.apply(exports, arguments); + console.error('%s %d: %s', set, pid, msg); + }; + } else { + debugs[set] = function() {}; + } + } + return debugs[set]; +}; +function inspect(obj, opts) { + var ctx = { + seen: [], + stylize: stylizeNoColor + }; + if (arguments.length >= 3) ctx.depth = arguments[2]; + if (arguments.length >= 4) ctx.colors = arguments[3]; + if (isBoolean(opts)) { + ctx.showHidden = opts; + } else if (opts) { + exports._extend(ctx, opts); + } + if (isUndefined(ctx.showHidden)) ctx.showHidden = false; + if (isUndefined(ctx.depth)) ctx.depth = 2; + if (isUndefined(ctx.colors)) ctx.colors = false; + if (isUndefined(ctx.customInspect)) ctx.customInspect = true; + if (ctx.colors) ctx.stylize = stylizeWithColor; + return formatValue(ctx, obj, ctx.depth); +} +exports.inspect = inspect; +inspect.colors = { + 'bold' : [1, 22], + 'italic' : [3, 23], + 'underline' : [4, 24], + 'inverse' : [7, 27], + 'white' : [37, 39], + 'grey' : [90, 39], + 'black' : [30, 39], + 'blue' : [34, 39], + 'cyan' : [36, 39], + 'green' : [32, 39], + 'magenta' : [35, 39], + 'red' : [31, 39], + 'yellow' : [33, 39] +}; +inspect.styles = { + 'special': 'cyan', + 'number': 'yellow', + 'boolean': 'yellow', + 'undefined': 'grey', + 'null': 'bold', + 'string': 'green', + 'date': 'magenta', + 'regexp': 'red' +}; + + +function stylizeWithColor(str, styleType) { + var style = inspect.styles[styleType]; + + if (style) { + return '\u001b[' + inspect.colors[style][0] + 'm' + str + + '\u001b[' + inspect.colors[style][1] + 'm'; + } else { + return str; + } +} + + +function stylizeNoColor(str, styleType) { + return str; +} + + +function arrayToHash(array) { + var hash = {}; + + array.forEach(function(val, idx) { + hash[val] = true; + }); + + return hash; +} + + +function formatValue(ctx, value, recurseTimes) { + if (ctx.customInspect && + value && + isFunction(value.inspect) && + value.inspect !== exports.inspect && + !(value.constructor && value.constructor.prototype === value)) { + var ret = value.inspect(recurseTimes, ctx); + if (!isString(ret)) { + ret = formatValue(ctx, ret, recurseTimes); + } + return ret; + } + var primitive = formatPrimitive(ctx, value); + if (primitive) { + return primitive; + } + var keys = Object.keys(value); + var visibleKeys = arrayToHash(keys); + + if (ctx.showHidden) { + keys = Object.getOwnPropertyNames(value); + } + if (isError(value) + && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) { + return formatError(value); + } + if (keys.length === 0) { + if (isFunction(value)) { + var name = value.name ? ': ' + value.name : ''; + return ctx.stylize('[Function' + name + ']', 'special'); + } + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); + } + if (isDate(value)) { + return ctx.stylize(Date.prototype.toString.call(value), 'date'); + } + if (isError(value)) { + return formatError(value); + } + } + + var base = '', array = false, braces = ['{', '}']; + if (isArray(value)) { + array = true; + braces = ['[', ']']; + } + if (isFunction(value)) { + var n = value.name ? ': ' + value.name : ''; + base = ' [Function' + n + ']'; + } + if (isRegExp(value)) { + base = ' ' + RegExp.prototype.toString.call(value); + } + if (isDate(value)) { + base = ' ' + Date.prototype.toUTCString.call(value); + } + if (isError(value)) { + base = ' ' + formatError(value); + } + + if (keys.length === 0 && (!array || value.length == 0)) { + return braces[0] + base + braces[1]; + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); + } else { + return ctx.stylize('[Object]', 'special'); + } + } + + ctx.seen.push(value); + + var output; + if (array) { + output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); + } else { + output = keys.map(function(key) { + return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); + }); + } + + ctx.seen.pop(); + + return reduceToSingleString(output, base, braces); +} + + +function formatPrimitive(ctx, value) { + if (isUndefined(value)) + return ctx.stylize('undefined', 'undefined'); + if (isString(value)) { + var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; + return ctx.stylize(simple, 'string'); + } + if (isNumber(value)) + return ctx.stylize('' + value, 'number'); + if (isBoolean(value)) + return ctx.stylize('' + value, 'boolean'); + if (isNull(value)) + return ctx.stylize('null', 'null'); +} + + +function formatError(value) { + return '[' + Error.prototype.toString.call(value) + ']'; +} + + +function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { + var output = []; + for (var i = 0, l = value.length; i < l; ++i) { + if (hasOwnProperty(value, String(i))) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + String(i), true)); + } else { + output.push(''); + } + } + keys.forEach(function(key) { + if (!key.match(/^\d+$/)) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + key, true)); + } + }); + return output; +} + + +function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { + var name, str, desc; + desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; + if (desc.get) { + if (desc.set) { + str = ctx.stylize('[Getter/Setter]', 'special'); + } else { + str = ctx.stylize('[Getter]', 'special'); + } + } else { + if (desc.set) { + str = ctx.stylize('[Setter]', 'special'); + } + } + if (!hasOwnProperty(visibleKeys, key)) { + name = '[' + key + ']'; + } + if (!str) { + if (ctx.seen.indexOf(desc.value) < 0) { + if (isNull(recurseTimes)) { + str = formatValue(ctx, desc.value, null); + } else { + str = formatValue(ctx, desc.value, recurseTimes - 1); + } + if (str.indexOf('\n') > -1) { + if (array) { + str = str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n').substr(2); + } else { + str = '\n' + str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n'); + } + } + } else { + str = ctx.stylize('[Circular]', 'special'); + } + } + if (isUndefined(name)) { + if (array && key.match(/^\d+$/)) { + return str; + } + name = JSON.stringify('' + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2); + name = ctx.stylize(name, 'name'); + } else { + name = name.replace(/'/g, "\\'") + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); + name = ctx.stylize(name, 'string'); + } + } + + return name + ': ' + str; +} + + +function reduceToSingleString(output, base, braces) { + var numLinesEst = 0; + var length = output.reduce(function(prev, cur) { + numLinesEst++; + if (cur.indexOf('\n') >= 0) numLinesEst++; + return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; + }, 0); + + if (length > 60) { + return braces[0] + + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; + } + + return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; +} +function isArray(ar) { + return Array.isArray(ar); +} +exports.isArray = isArray; + +function isBoolean(arg) { + return typeof arg === 'boolean'; +} +exports.isBoolean = isBoolean; + +function isNull(arg) { + return arg === null; +} +exports.isNull = isNull; + +function isNullOrUndefined(arg) { + return arg == null; +} +exports.isNullOrUndefined = isNullOrUndefined; + +function isNumber(arg) { + return typeof arg === 'number'; +} +exports.isNumber = isNumber; + +function isString(arg) { + return typeof arg === 'string'; +} +exports.isString = isString; + +function isSymbol(arg) { + return typeof arg === 'symbol'; +} +exports.isSymbol = isSymbol; + +function isUndefined(arg) { + return arg === void 0; +} +exports.isUndefined = isUndefined; + +function isRegExp(re) { + return isObject(re) && objectToString(re) === '[object RegExp]'; +} +exports.isRegExp = isRegExp; + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} +exports.isObject = isObject; + +function isDate(d) { + return isObject(d) && objectToString(d) === '[object Date]'; +} +exports.isDate = isDate; + +function isError(e) { + return isObject(e) && + (objectToString(e) === '[object Error]' || e instanceof Error); +} +exports.isError = isError; + +function isFunction(arg) { + return typeof arg === 'function'; +} +exports.isFunction = isFunction; + +function isPrimitive(arg) { + return arg === null || + typeof arg === 'boolean' || + typeof arg === 'number' || + typeof arg === 'string' || + typeof arg === 'symbol' || // ES6 symbol + typeof arg === 'undefined'; +} +exports.isPrimitive = isPrimitive; + +exports.isBuffer = _dereq_('./support/isBuffer'); + +function objectToString(o) { + return Object.prototype.toString.call(o); +} + + +function pad(n) { + return n < 10 ? '0' + n.toString(10) : n.toString(10); +} + + +var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec']; +function timestamp() { + var d = new Date(); + var time = [pad(d.getHours()), + pad(d.getMinutes()), + pad(d.getSeconds())].join(':'); + return [d.getDate(), months[d.getMonth()], time].join(' '); +} +exports.log = function() { + console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); +}; +exports.inherits = _dereq_('inherits'); + +exports._extend = function(origin, add) { + if (!add || !isObject(add)) return origin; + + var keys = Object.keys(add); + var i = keys.length; + while (i--) { + origin[keys[i]] = add[keys[i]]; + } + return origin; +}; + +function hasOwnProperty(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +}).call(this,_dereq_('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./support/isBuffer":"/../node_modules/assert/node_modules/util/support/isBufferBrowser.js","_process":"/../node_modules/process/browser.js","inherits":"/../node_modules/assert/node_modules/inherits/inherits_browser.js"}],"/../node_modules/events/events.js":[function(_dereq_,module,exports){ + +var objectCreate = Object.create || objectCreatePolyfill +var objectKeys = Object.keys || objectKeysPolyfill +var bind = Function.prototype.bind || functionBindPolyfill + +function EventEmitter() { + if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) { + this._events = objectCreate(null); + this._eventsCount = 0; + } + + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; +var defaultMaxListeners = 200; + +var hasDefineProperty; +try { + var o = {}; + if (Object.defineProperty) Object.defineProperty(o, 'x', { value: 0 }); + hasDefineProperty = o.x === 0; +} catch (err) { hasDefineProperty = false } +if (hasDefineProperty) { + Object.defineProperty(EventEmitter, 'defaultMaxListeners', { + enumerable: true, + get: function() { + return defaultMaxListeners; + }, + set: function(arg) { + if (typeof arg !== 'number' || arg < 0 || arg !== arg) + throw new TypeError('"defaultMaxListeners" must be a positive number'); + defaultMaxListeners = arg; + } + }); +} else { + EventEmitter.defaultMaxListeners = defaultMaxListeners; +} +EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { + if (typeof n !== 'number' || n < 0 || isNaN(n)) + throw new TypeError('"n" argument must be a positive number'); + this._maxListeners = n; + return this; +}; + +function $getMaxListeners(that) { + if (that._maxListeners === undefined) + return EventEmitter.defaultMaxListeners; + return that._maxListeners; +} + +EventEmitter.prototype.getMaxListeners = function getMaxListeners() { + return $getMaxListeners(this); +}; +function emitNone(handler, isFn, self) { + if (isFn) + handler.call(self); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self); + } +} +function emitOne(handler, isFn, self, arg1) { + if (isFn) + handler.call(self, arg1); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self, arg1); + } +} +function emitTwo(handler, isFn, self, arg1, arg2) { + if (isFn) + handler.call(self, arg1, arg2); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self, arg1, arg2); + } +} +function emitThree(handler, isFn, self, arg1, arg2, arg3) { + if (isFn) + handler.call(self, arg1, arg2, arg3); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self, arg1, arg2, arg3); + } +} + +function emitMany(handler, isFn, self, args) { + if (isFn) + handler.apply(self, args); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].apply(self, args); + } +} + +EventEmitter.prototype.emit = function emit(type) { + var er, handler, len, args, i, events; + var doError = (type === 'error'); + + events = this._events; + if (events) + doError = (doError && events.error == null); + else if (!doError) + return false; + if (doError) { + if (arguments.length > 1) + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } else { + var err = new Error('Unhandled "error" event. (' + er + ')'); + err.context = er; + throw err; + } + return false; + } + + handler = events[type]; + + if (!handler) + return false; + + var isFn = typeof handler === 'function'; + len = arguments.length; + switch (len) { + case 1: + emitNone(handler, isFn, this); + break; + case 2: + emitOne(handler, isFn, this, arguments[1]); + break; + case 3: + emitTwo(handler, isFn, this, arguments[1], arguments[2]); + break; + case 4: + emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); + break; + default: + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + emitMany(handler, isFn, this, args); + } + + return true; +}; + +function _addListener(target, type, listener, prepend) { + var m; + var events; + var existing; + + if (typeof listener !== 'function') + throw new TypeError('"listener" argument must be a function'); + + events = target._events; + if (!events) { + events = target._events = objectCreate(null); + target._eventsCount = 0; + } else { + if (events.newListener) { + target.emit('newListener', type, + listener.listener ? listener.listener : listener); + events = target._events; + } + existing = events[type]; + } + + if (!existing) { + existing = events[type] = listener; + ++target._eventsCount; + } else { + if (typeof existing === 'function') { + existing = events[type] = + prepend ? [listener, existing] : [existing, listener]; + } else { + if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } + } + if (!existing.warned) { + m = $getMaxListeners(target); + if (m && m > 0 && existing.length > m) { + existing.warned = true; + var w = new Error('Possible EventEmitter memory leak detected. ' + + existing.length + ' "' + String(type) + '" listeners ' + + 'added. Use emitter.setMaxListeners() to ' + + 'increase limit.'); + w.name = 'MaxListenersExceededWarning'; + w.emitter = target; + w.type = type; + w.count = existing.length; + if (typeof console === 'object' && console.warn) { + console.warn('%s: %s', w.name, w.message); + } + } + } + } + + return target; +} + +EventEmitter.prototype.addListener = function addListener(type, listener) { + return _addListener(this, type, listener, false); +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.prependListener = + function prependListener(type, listener) { + return _addListener(this, type, listener, true); + }; + +function onceWrapper() { + if (!this.fired) { + this.target.removeListener(this.type, this.wrapFn); + this.fired = true; + switch (arguments.length) { + case 0: + return this.listener.call(this.target); + case 1: + return this.listener.call(this.target, arguments[0]); + case 2: + return this.listener.call(this.target, arguments[0], arguments[1]); + case 3: + return this.listener.call(this.target, arguments[0], arguments[1], + arguments[2]); + default: + var args = new Array(arguments.length); + for (var i = 0; i < args.length; ++i) + args[i] = arguments[i]; + this.listener.apply(this.target, args); + } + } +} + +function _onceWrap(target, type, listener) { + var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; + var wrapped = bind.call(onceWrapper, state); + wrapped.listener = listener; + state.wrapFn = wrapped; + return wrapped; +} + +EventEmitter.prototype.once = function once(type, listener) { + if (typeof listener !== 'function') + throw new TypeError('"listener" argument must be a function'); + this.on(type, _onceWrap(this, type, listener)); + return this; +}; + +EventEmitter.prototype.prependOnceListener = + function prependOnceListener(type, listener) { + if (typeof listener !== 'function') + throw new TypeError('"listener" argument must be a function'); + this.prependListener(type, _onceWrap(this, type, listener)); + return this; + }; +EventEmitter.prototype.removeListener = + function removeListener(type, listener) { + var list, events, position, i, originalListener; + + if (typeof listener !== 'function') + throw new TypeError('"listener" argument must be a function'); + + events = this._events; + if (!events) + return this; + + list = events[type]; + if (!list) + return this; + + if (list === listener || list.listener === listener) { + if (--this._eventsCount === 0) + this._events = objectCreate(null); + else { + delete events[type]; + if (events.removeListener) + this.emit('removeListener', type, list.listener || listener); + } + } else if (typeof list !== 'function') { + position = -1; + + for (i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || list[i].listener === listener) { + originalListener = list[i].listener; + position = i; + break; + } + } + + if (position < 0) + return this; + + if (position === 0) + list.shift(); + else + spliceOne(list, position); + + if (list.length === 1) + events[type] = list[0]; + + if (events.removeListener) + this.emit('removeListener', type, originalListener || listener); + } + + return this; + }; + +EventEmitter.prototype.removeAllListeners = + function removeAllListeners(type) { + var listeners, events, i; + + events = this._events; + if (!events) + return this; + if (!events.removeListener) { + if (arguments.length === 0) { + this._events = objectCreate(null); + this._eventsCount = 0; + } else if (events[type]) { + if (--this._eventsCount === 0) + this._events = objectCreate(null); + else + delete events[type]; + } + return this; + } + if (arguments.length === 0) { + var keys = objectKeys(events); + var key; + for (i = 0; i < keys.length; ++i) { + key = keys[i]; + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = objectCreate(null); + this._eventsCount = 0; + return this; + } + + listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners) { + for (i = listeners.length - 1; i >= 0; i--) { + this.removeListener(type, listeners[i]); + } + } + + return this; + }; + +function _listeners(target, type, unwrap) { + var events = target._events; + + if (!events) + return []; + + var evlistener = events[type]; + if (!evlistener) + return []; + + if (typeof evlistener === 'function') + return unwrap ? [evlistener.listener || evlistener] : [evlistener]; + + return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); +} + +EventEmitter.prototype.listeners = function listeners(type) { + return _listeners(this, type, true); +}; + +EventEmitter.prototype.rawListeners = function rawListeners(type) { + return _listeners(this, type, false); +}; + +EventEmitter.listenerCount = function(emitter, type) { + if (typeof emitter.listenerCount === 'function') { + return emitter.listenerCount(type); + } else { + return listenerCount.call(emitter, type); + } +}; + +EventEmitter.prototype.listenerCount = listenerCount; +function listenerCount(type) { + var events = this._events; + + if (events) { + var evlistener = events[type]; + + if (typeof evlistener === 'function') { + return 1; + } else if (evlistener) { + return evlistener.length; + } + } + + return 0; +} + +EventEmitter.prototype.eventNames = function eventNames() { + return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; +}; +function spliceOne(list, index) { + for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) + list[i] = list[k]; + list.pop(); +} + +function arrayClone(arr, n) { + var copy = new Array(n); + for (var i = 0; i < n; ++i) + copy[i] = arr[i]; + return copy; +} + +function unwrapListeners(arr) { + var ret = new Array(arr.length); + for (var i = 0; i < ret.length; ++i) { + ret[i] = arr[i].listener || arr[i]; + } + return ret; +} + +function objectCreatePolyfill(proto) { + var F = function() {}; + F.prototype = proto; + return new F; +} +function objectKeysPolyfill(obj) { + var keys = []; + for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) { + keys.push(k); + } + return k; +} +function functionBindPolyfill(context) { + var fn = this; + return function () { + return fn.apply(context, arguments); + }; +} + +},{}],"/../node_modules/object-assign/index.js":[function(_dereq_,module,exports){ + +'use strict'; +var getOwnPropertySymbols = Object.getOwnPropertySymbols; +var hasOwnProperty = Object.prototype.hasOwnProperty; +var propIsEnumerable = Object.prototype.propertyIsEnumerable; + +function toObject(val) { + if (val === null || val === undefined) { + throw new TypeError('Object.assign cannot be called with null or undefined'); + } + + return Object(val); +} + +function shouldUseNative() { + try { + if (!Object.assign) { + return false; + } + var test1 = new String('abc'); // eslint-disable-line no-new-wrappers + test1[5] = 'de'; + if (Object.getOwnPropertyNames(test1)[0] === '5') { + return false; + } + var test2 = {}; + for (var i = 0; i < 10; i++) { + test2['_' + String.fromCharCode(i)] = i; + } + var order2 = Object.getOwnPropertyNames(test2).map(function (n) { + return test2[n]; + }); + if (order2.join('') !== '0123456789') { + return false; + } + var test3 = {}; + 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { + test3[letter] = letter; + }); + if (Object.keys(Object.assign({}, test3)).join('') !== + 'abcdefghijklmnopqrst') { + return false; + } + + return true; + } catch (err) { + return false; + } +} + +module.exports = shouldUseNative() ? Object.assign : function (target, source) { + var from; + var to = toObject(target); + var symbols; + + for (var s = 1; s < arguments.length; s++) { + from = Object(arguments[s]); + + for (var key in from) { + if (hasOwnProperty.call(from, key)) { + to[key] = from[key]; + } + } + + if (getOwnPropertySymbols) { + symbols = getOwnPropertySymbols(from); + for (var i = 0; i < symbols.length; i++) { + if (propIsEnumerable.call(from, symbols[i])) { + to[symbols[i]] = from[symbols[i]]; + } + } + } + } + + return to; +}; + +},{}],"/../node_modules/process/browser.js":[function(_dereq_,module,exports){ +var process = module.exports = {}; + +var cachedSetTimeout; +var cachedClearTimeout; + +function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); +} +function defaultClearTimeout () { + throw new Error('clearTimeout has not been defined'); +} +(function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } +} ()) +function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + return setTimeout(fun, 0); + } + if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + return cachedSetTimeout(fun, 0); + } catch(e){ + try { + return cachedSetTimeout.call(null, fun, 0); + } catch(e){ + return cachedSetTimeout.call(this, fun, 0); + } + } + + +} +function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + return clearTimeout(marker); + } + if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + return cachedClearTimeout(marker); + } catch (e){ + try { + return cachedClearTimeout.call(null, marker); + } catch (e){ + return cachedClearTimeout.call(this, marker); + } + } + + + +} +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } +}; +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; +process.prependListener = noop; +process.prependOnceListener = noop; + +process.listeners = function (name) { return [] } + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],"/../node_modules/util/node_modules/inherits/inherits_browser.js":[function(_dereq_,module,exports){ +arguments[4]["/../node_modules/assert/node_modules/inherits/inherits_browser.js"][0].apply(exports,arguments) +},{}],"/../node_modules/util/support/isBufferBrowser.js":[function(_dereq_,module,exports){ +arguments[4]["/../node_modules/assert/node_modules/util/support/isBufferBrowser.js"][0].apply(exports,arguments) +},{}],"/../node_modules/util/util.js":[function(_dereq_,module,exports){ +arguments[4]["/../node_modules/assert/node_modules/util/util.js"][0].apply(exports,arguments) +},{"./support/isBuffer":"/../node_modules/util/support/isBufferBrowser.js","_process":"/../node_modules/process/browser.js","inherits":"/../node_modules/util/node_modules/inherits/inherits_browser.js"}]},{},["/../../jshint/src/jshint.js"]); + +}); + +ace.define("ace/mode/tbel_worker",[], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var Mirror = require("../worker/mirror").Mirror; +var lint = require("./tbel/jshint").JSHINT; + +function startRegex(arr) { + return RegExp("^(" + arr.join("|") + ")"); +} + +var disabledWarningsRe = startRegex([ + "Bad for in variable '(.+)'.", + 'Missing "use strict"' +]); +var errorsRe = startRegex([ + "Unexpected", + "Expected ", + "Confusing (plus|minus)", + "\\{a\\} unterminated regular expression", + "Unclosed ", + "Unmatched ", + "Unbegun comment", + "Bad invocation", + "Missing space after", + "Missing operator at" +]); +var infoRe = startRegex([ + "Expected an assignment", + "Bad escapement of EOL", + "Unexpected comma", + "Unexpected space", + "Missing radix parameter.", + "A leading decimal point can", + "\\['{a}'\\] is better written in dot notation.", + "'{a}' used out of scope" +]); + +var TbelWorker = exports.TbelWorker = function(sender) { + Mirror.call(this, sender); + this.setTimeout(500); + this.setOptions(); +}; + +oop.inherits(TbelWorker, Mirror); + +(function() { + this.setOptions = function(options) { + this.options = options || { + esnext: true, + moz: true, + devel: true, + browser: true, + node: true, + laxcomma: true, + laxbreak: true, + lastsemic: true, + onevar: false, + passfail: false, + maxerr: 100, + expr: true, + multistr: true, + globalstrict: true + }; + this.doc.getValue() && this.deferredUpdate.schedule(100); + }; + + this.changeOptions = function(newOptions) { + oop.mixin(this.options, newOptions); + this.doc.getValue() && this.deferredUpdate.schedule(100); + }; + + this.onUpdate = function() { + var value = this.doc.getValue(); + value = value.replace(/^#!.*\n/, "\n"); + if (!value) + return this.sender.emit("annotate", []); + + var errors = []; + lint(value, this.options, this.options.globals); + var results = lint.errors; + + var errorAdded = false; + for (var i = 0; i < results.length; i++) { + var error = results[i]; + if (!error) + continue; + var raw = error.raw; + var type = "warning"; + + if (raw == "Missing semicolon.") { + var str = error.evidence.substr(error.character); + str = str.charAt(str.search(/\S/)); + if (str && /[\w\d{(['"]/.test(str)) { + error.reason = 'Missing ";" before statement'; + type = "error"; + } else { + type = "info"; + } + } + else if (disabledWarningsRe.test(raw)) { + continue; + } + else if (infoRe.test(raw)) { + type = "info"; + } + else if (errorsRe.test(raw)) { + errorAdded = true; + type = "error"; + } + else if (raw == "'{a}' is not defined.") { + type = "warning"; + } + else if (raw == "'{a}' is defined but never used.") { + type = "info"; + } + + errors.push({ + row: error.line-1, + column: error.character-1, + text: error.reason, + type: type, + raw: raw + }); + + if (errorAdded) { + } + } + + this.sender.emit("annotate", errors); + }; + +}).call(TbelWorker.prototype); + +}); diff --git a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts index 7112283b36..b9d44c64cf 100644 --- a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index fd2e200090..202b25d539 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -23,6 +23,14 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; import { CustomerId } from '@shared/models/id/customer-id'; import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models'; +import { AlarmCommentId } from '@shared/models/id/alarm-comment-id'; +import { UserId } from '@shared/models/id/user-id'; +import { AlarmFilter } from '@shared/models/query/query.models'; + +export enum AlarmsMode { + ALL, + ENTITY +} export enum AlarmSeverity { CRITICAL = 'CRITICAL', @@ -89,6 +97,7 @@ export const alarmSeverityColors = new Map( export interface Alarm extends BaseData { tenantId: TenantId; customerId: CustomerId; + assigneeId: UserId; type: string; originator: EntityId; severity: AlarmSeverity; @@ -97,12 +106,43 @@ export interface Alarm extends BaseData { endTs: number; ackTs: number; clearTs: number; + assignTs: number; propagate: boolean; details?: any; } +export enum AlarmCommentType { + SYSTEM = 'SYSTEM', + OTHER = 'OTHER' +} + +export interface AlarmComment extends BaseData { + alarmId: AlarmId; + userId?: UserId; + type: AlarmCommentType; + comment: { + text: string; + edited?: boolean; + editedOn?: number; + } +} + +export interface AlarmCommentInfo extends AlarmComment { + firstName?: string; + lastName?: string; + email?: string; +} + export interface AlarmInfo extends Alarm { originatorName: string; + originatorLabel: string; + assignee: AlarmAssignee; +} + +export interface AlarmAssignee { + firstName: string; + lastName: string; + email: string; } export interface AlarmDataInfo extends AlarmInfo { @@ -115,12 +155,20 @@ export const simulatedAlarm: AlarmInfo = { id: new AlarmId(NULL_UUID), tenantId: new TenantId(NULL_UUID), customerId: new CustomerId(NULL_UUID), + assigneeId: new UserId(NULL_UUID), createdTime: new Date().getTime(), startTs: new Date().getTime(), endTs: 0, ackTs: 0, clearTs: 0, + assignTs: 0, originatorName: 'Simulated', + originatorLabel: 'Simulated', + assignee: { + firstName: "", + lastName: "", + email: "test@example.com", + }, originator: { entityType: EntityType.DEVICE, id: '1' @@ -172,11 +220,22 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { name: 'alarm.clear-time', time: true }, + assignTime: { + keyName: 'assignTime', + value: 'assignTs', + name: 'alarm.assign-time', + time: true + }, originator: { keyName: 'originator', value: 'originatorName', name: 'alarm.originator' }, + originatorLabel: { + keyName: 'originatorLabel', + value: 'originatorLabel', + name: 'alarm.originator-label' + }, originatorType: { keyName: 'originatorType', value: 'originator.entityType', @@ -196,6 +255,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { keyName: 'status', value: 'status', name: 'alarm.status' + }, + assignee: { + keyName: 'assignee', + value: 'assignee', + name: 'alarm.assignee' } }; @@ -206,19 +270,21 @@ export class AlarmQuery { searchStatus: AlarmSearchStatus; status: AlarmStatus; fetchOriginator: boolean; + assigneeId?: UserId; constructor(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, - fetchOriginator: boolean) { + fetchOriginator: boolean, assigneeId?: UserId) { this.affectedEntityId = entityId; this.pageLink = pageLink; this.searchStatus = searchStatus; this.status = status; this.fetchOriginator = fetchOriginator; + this.assigneeId = assigneeId; } public toQuery(): string { - let query = `/${this.affectedEntityId.entityType}/${this.affectedEntityId.id}`; + let query = this.affectedEntityId ? `/${this.affectedEntityId.entityType}/${this.affectedEntityId.id}` : ''; query += this.pageLink.toQuery(); if (this.searchStatus) { query += `&searchStatus=${this.searchStatus}`; @@ -228,6 +294,48 @@ export class AlarmQuery { if (typeof this.fetchOriginator !== 'undefined' && this.fetchOriginator !== null) { query += `&fetchOriginator=${this.fetchOriginator}`; } + if (typeof this.assigneeId !== 'undefined' && this.assigneeId !== null) { + query += `&assigneeId=${this.assigneeId.id}`; + } + return query; + } + +} + +export class AlarmQueryV2 { + + affectedEntityId: EntityId; + pageLink: TimePageLink; + typeList: string[]; + statusList: AlarmSearchStatus[]; + severityList: AlarmSeverity[]; + assigneeId?: UserId; + + constructor(entityId: EntityId, pageLink: TimePageLink, + alarmFilter: AlarmFilter) { + this.affectedEntityId = entityId; + this.pageLink = pageLink; + this.typeList = alarmFilter.typeList; + this.statusList = alarmFilter.statusList; + this.severityList = alarmFilter.severityList; + this.assigneeId = alarmFilter.assigneeId; + } + + public toQuery(): string { + let query = this.affectedEntityId ? `/${this.affectedEntityId.entityType}/${this.affectedEntityId.id}` : ''; + query += this.pageLink.toQuery(); + if (this.typeList && this.typeList.length) { + query += `&typeList=${this.typeList.join(',')}`; + } + if (this.statusList && this.statusList.length) { + query += `&statusList=${this.statusList.join(',')}`; + } + if (this.severityList && this.severityList.length) { + query += `&severityList=${this.severityList.join(',')}`; + } + if (typeof this.assigneeId !== 'undefined' && this.assigneeId !== null) { + query += `&assigneeId=${this.assigneeId.id}`; + } return query; } diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index f67c0d50a7..ee64df5288 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -86,22 +86,38 @@ export interface StateEntityFilter { } export interface AssetTypeFilter { + /** + * @deprecated + */ assetType?: string; + assetTypes?: string[]; assetNameFilter?: string; } export interface DeviceTypeFilter { + /** + * @deprecated + */ deviceType?: string; + deviceTypes?: string[]; deviceNameFilter?: string; } export interface EdgeTypeFilter { + /** + * @deprecated + */ edgeType?: string; + edgeTypes?: string[]; edgeNameFilter?: string; } export interface EntityViewFilter { + /** + * @deprecated + */ entityViewType?: string; + entityViewTypes?: string[]; entityViewNameFilter?: string; } @@ -127,7 +143,7 @@ export interface EntitySearchQueryFilter { fetchLastLevelOnly?: boolean; } -// tslint:disable-next-line:no-empty-interface +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ApiUsageStateFilter { } diff --git a/ui-ngx/src/app/shared/models/api-usage.models.ts b/ui-ngx/src/app/shared/models/api-usage.models.ts new file mode 100644 index 0000000000..bbc0ff5597 --- /dev/null +++ b/ui-ngx/src/app/shared/models/api-usage.models.ts @@ -0,0 +1,47 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export enum ApiUsageStateValue { + ENABLED = 'ENABLED', + WARNING = 'WARNING', + DISABLED = 'DISABLED' +} + +export const ApiUsageStateValueTranslationMap = new Map([ + [ApiUsageStateValue.ENABLED, 'notification.enabled'], + [ApiUsageStateValue.WARNING, 'notification.warning'], + [ApiUsageStateValue.DISABLED, 'notification.disabled'], +]); + +export enum ApiFeature { + TRANSPORT = 'TRANSPORT', + DB = 'DB', + RE = 'RE', + JS = 'JS', + EMAIL = 'EMAIL', + SMS = 'SMS', + ALARM = 'ALARM' +} + +export const ApiFeatureTranslationMap = new Map([ + [ApiFeature.TRANSPORT, 'api-usage.device-api'], + [ApiFeature.DB, 'api-usage.telemetry-persistence'], + [ApiFeature.RE, 'api-usage.rule-engine-executions'], + [ApiFeature.JS, 'api-usage.javascript-executions'], + [ApiFeature.EMAIL, 'api-usage.email-messages'], + [ApiFeature.SMS, 'api-usage.sms-messages'], + [ApiFeature.ALARM, 'api-usage.alarm'], +]); diff --git a/ui-ngx/src/app/shared/models/asset.models.ts b/ui-ngx/src/app/shared/models/asset.models.ts index 029c560310..a1ab16fdc8 100644 --- a/ui-ngx/src/app/shared/models/asset.models.ts +++ b/ui-ngx/src/app/shared/models/asset.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -35,9 +35,11 @@ export interface AssetProfile extends BaseData, ExportableEntity defaultRuleChainId?: RuleChainId; defaultDashboardId?: DashboardId; defaultQueueName?: string; + defaultEdgeRuleChainId?: RuleChainId; } export interface AssetProfileInfo extends EntityInfoData { + tenantId?: TenantId; image?: string; defaultDashboardId?: DashboardId; } diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index 056c6aa569..ce4d380567 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -47,6 +47,11 @@ export enum ActionType { RELATIONS_DELETED = 'RELATIONS_DELETED', ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', + ALARM_ASSIGNED = 'ALARM_ASSIGNED', + ALARM_UNASSIGNED = 'ALARM_UNASSIGNED', + ADDED_COMMENT = 'ADDED_COMMENT', + UPDATED_COMMENT = 'UPDATED_COMMENT', + DELETED_COMMENT = 'DELETED_COMMENT', LOGIN = 'LOGIN', LOGOUT = 'LOGOUT', LOCKOUT = 'LOCKOUT', @@ -57,7 +62,8 @@ export enum ActionType { TIMESERIES_UPDATED = 'TIMESERIES_UPDATED', TIMESERIES_DELETED = 'TIMESERIES_DELETED', ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE', - UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE' + UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE', + SMS_SENT = 'SMS_SENT' } export enum ActionStatus { @@ -85,6 +91,11 @@ export const actionTypeTranslations = new Map( [ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'], [ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'], [ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'], + [ActionType.ALARM_ASSIGNED, 'audit-log.type-alarm-assign'], + [ActionType.ALARM_UNASSIGNED, 'audit-log.type-alarm-unassign'], + [ActionType.ADDED_COMMENT, 'audit-log.type-added-comment'], + [ActionType.UPDATED_COMMENT, 'audit-log.type-updated-comment'], + [ActionType.DELETED_COMMENT, 'audit-log.type-deleted-comment'], [ActionType.LOGIN, 'audit-log.type-login'], [ActionType.LOGOUT, 'audit-log.type-logout'], [ActionType.LOCKOUT, 'audit-log.type-lockout'], @@ -95,7 +106,8 @@ export const actionTypeTranslations = new Map( [ActionType.TIMESERIES_UPDATED, 'audit-log.type-timeseries-updated'], [ActionType.TIMESERIES_DELETED, 'audit-log.type-timeseries-deleted'], [ActionType.ASSIGNED_TO_EDGE, 'audit-log.type-assigned-to-edge'], - [ActionType.UNASSIGNED_FROM_EDGE, 'audit-log.type-unassigned-from-edge'] + [ActionType.UNASSIGNED_FROM_EDGE, 'audit-log.type-unassigned-from-edge'], + [ActionType.SMS_SENT, 'audit-log.type-sms-sent'], ] ); diff --git a/ui-ngx/src/app/shared/models/authority.enum.ts b/ui-ngx/src/app/shared/models/authority.enum.ts index 683c153b07..c736fe9cb6 100644 --- a/ui-ngx/src/app/shared/models/authority.enum.ts +++ b/ui-ngx/src/app/shared/models/authority.enum.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/base-data.ts b/ui-ngx/src/app/shared/models/base-data.ts index 7b3ea9a642..16022641aa 100644 --- a/ui-ngx/src/app/shared/models/base-data.ts +++ b/ui-ngx/src/app/shared/models/base-data.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -27,6 +27,17 @@ export interface BaseData { label?: string; } +export function sortEntitiesByIds>(entities: T[], entityIds: string[]): T[] { + entities.sort((entity1, entity2) => { + const id1 = entity1.id.id; + const id2 = entity2.id.id; + const index1 = entityIds.indexOf(id1); + const index2 = entityIds.indexOf(id2); + return index1 - index2; + }); + return entities; +} + export interface ExportableEntity { createdTime?: number; id?: T; diff --git a/ui-ngx/src/app/shared/models/beautify.models.ts b/ui-ngx/src/app/shared/models/beautify.models.ts index 6e2affc6fe..ad7c6427c0 100644 --- a/ui-ngx/src/app/shared/models/beautify.models.ts +++ b/ui-ngx/src/app/shared/models/beautify.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/component-descriptor.models.ts b/ui-ngx/src/app/shared/models/component-descriptor.models.ts index 47e7d5af42..3ce15d15aa 100644 --- a/ui-ngx/src/app/shared/models/component-descriptor.models.ts +++ b/ui-ngx/src/app/shared/models/component-descriptor.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -30,9 +30,16 @@ export enum ComponentScope { TENANT = 'TENANT' } +export enum ComponentClusteringMode { + USER_PREFERENCE = 'USER_PREFERENCE', + ENABLED = 'ENABLED', + SINGLETON = 'SINGLETON' +} + export interface ComponentDescriptor { type: ComponentType | RuleNodeType; scope?: ComponentScope; + clusteringMode: ComponentClusteringMode; name: string; clazz: string; configurationDescriptor?: any; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 6d2a098ca6..8648973c72 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -67,7 +67,8 @@ export const MediaBreakpoints = { 'gt-sm': 'screen and (min-width: 960px)', 'gt-md': 'screen and (min-width: 1280px)', 'gt-lg': 'screen and (min-width: 1920px)', - 'gt-xl': 'screen and (min-width: 5001px)' + 'gt-xl': 'screen and (min-width: 5001px)', + 'md-lg': 'screen and (min-width: 960px) and (max-width: 1819px)' }; export const helpBaseUrl = 'https://thingsboard.io'; @@ -76,6 +77,7 @@ export const HelpLinks = { linksMap: { outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings', smsProviderSettings: helpBaseUrl + '/docs/user-guide/ui/sms-provider-settings', + slackSettings: helpBaseUrl + '/docs/user-guide/ui/slack-settings', securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings', oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/', twoFactorAuthSettings: helpBaseUrl + '/docs/', @@ -85,6 +87,9 @@ export const HelpLinks = { ruleNodeGpsGeofencingFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#gps-geofencing-filter-node', ruleNodeJsFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node', ruleNodeJsSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#switch-node', + ruleNodeAssetProfileSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#asset-profile-switch', + ruleNodeDeviceProfileSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#device-profile-switch', + ruleNodeCheckAlarmStatus: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-alarm-status', ruleNodeMessageTypeFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node', ruleNodeMessageTypeSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node', ruleNodeOriginatorTypeFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-filter-node', @@ -128,6 +133,7 @@ export const HelpLinks = { ruleNodeSendEmail: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#send-email-node', ruleNodeSendSms: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#send-sms-node', ruleNodeMath: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#math-function-node', + ruleNodeCalculateDelta: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#calculate-delta', tenants: helpBaseUrl + '/docs/user-guide/ui/tenants', tenantProfiles: helpBaseUrl + '/docs/user-guide/ui/tenant-profiles', customers: helpBaseUrl + '/docs/user-guide/ui/customers', @@ -140,7 +146,7 @@ export const HelpLinks = { entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning', rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', - resources: helpBaseUrl + '/docs/user-guide/ui/resources', + lwm2mResourceLibrary: helpBaseUrl + '/docs/reference/lwm2m-api', dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', otaUpdates: helpBaseUrl + '/docs/user-guide/ota-updates', widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles', @@ -155,7 +161,12 @@ export const HelpLinks = { queue: helpBaseUrl + '/docs/user-guide/queue', repositorySettings: helpBaseUrl + '/docs/user-guide/version-control/#git-settings-configuration', autoCommitSettings: helpBaseUrl + '/docs/user-guide/version-control/#auto-commit', - twoFactorAuthentication: helpBaseUrl + '/docs/user-guide/two-factor-authentication' + twoFactorAuthentication: helpBaseUrl + '/docs/user-guide/two-factor-authentication', + sentNotification: helpBaseUrl + '/docs/user-guide/notifications/#send-notification', + templateNotifications: helpBaseUrl + '/docs/user-guide/notifications/#templates', + recipientNotifications: helpBaseUrl + '/docs/user-guide/notifications/#recipients', + ruleNotifications: helpBaseUrl + '/docs/user-guide/notifications/#rules', + jwtSecuritySettings: helpBaseUrl + '/docs/user-guide/ui/jwt-security-settings/', } }; diff --git a/ui-ngx/src/app/shared/models/contact-based.model.ts b/ui-ngx/src/app/shared/models/contact-based.model.ts index f39f1fa290..d7da687098 100644 --- a/ui-ngx/src/app/shared/models/contact-based.model.ts +++ b/ui-ngx/src/app/shared/models/contact-based.model.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/country.models.ts b/ui-ngx/src/app/shared/models/country.models.ts index afba058c6d..f243ba3a5c 100644 --- a/ui-ngx/src/app/shared/models/country.models.ts +++ b/ui-ngx/src/app/shared/models/country.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/customer.model.ts b/ui-ngx/src/app/shared/models/customer.model.ts index 67c1429189..d26725814f 100644 --- a/ui-ngx/src/app/shared/models/customer.model.ts +++ b/ui-ngx/src/app/shared/models/customer.model.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index bf71997e35..f3f2038fea 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -52,6 +52,7 @@ export interface GridSettings { backgroundColor?: string; columns?: number; margin?: number; + outerMargin?: boolean; backgroundSizeMode?: string; backgroundImageUrl?: string; autoFillHeight?: boolean; diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index d06963544e..f7802b3a8b 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -35,6 +35,9 @@ import { getDefaultProfileObserveAttrConfig, PowerMode } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; +import { EdgeId } from '@shared/models/id/edge-id'; export enum DeviceProfileType { DEFAULT = 'DEFAULT', @@ -62,7 +65,8 @@ export enum CoapTransportDeviceType { export enum DeviceProvisionType { DISABLED = 'DISABLED', ALLOW_CREATE_NEW_DEVICES = 'ALLOW_CREATE_NEW_DEVICES', - CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES' + CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES', + X509_CERTIFICATE_CHAIN = 'X509_CERTIFICATE_CHAIN' } export interface DeviceConfigurationFormInfo { @@ -110,7 +114,8 @@ export const deviceProvisionTypeTranslationMap = new Map { let configuration: DeviceProfileConfiguration = null; if (type) { switch (type) { @@ -332,9 +342,9 @@ export function createDeviceProfileConfiguration(type: DeviceProfileType): Devic } } return configuration; -} +}; -export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration { +export const createDeviceConfiguration = (type: DeviceProfileType): DeviceConfiguration => { let configuration: DeviceConfiguration = null; if (type) { switch (type) { @@ -345,9 +355,9 @@ export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfig } } return configuration; -} +}; -export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration { +export const createDeviceProfileTransportConfiguration = (type: DeviceTransportType): DeviceProfileTransportConfiguration => { let transportConfiguration: DeviceProfileTransportConfiguration = null; if (type) { switch (type) { @@ -359,6 +369,9 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT const mqttTransportConfiguration: MqttDeviceProfileTransportConfiguration = { deviceTelemetryTopic: 'v1/devices/me/telemetry', deviceAttributesTopic: 'v1/devices/me/attributes', + deviceAttributesSubscribeTopic: 'v1/devices/me/attributes', + sparkplug: false, + sparkplugAttributesMetricNames: ['Node Control/*', 'Device Control/*', 'Properties/*'], sendAckOnValidationException: false, transportPayloadTypeConfiguration: { transportPayloadType: TransportPayloadType.JSON, @@ -399,9 +412,9 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT } } return transportConfiguration; -} +}; -export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration { +export const createDeviceTransportConfiguration = (type: DeviceTransportType): DeviceTransportConfiguration => { let transportConfiguration: DeviceTransportConfiguration = null; if (type) { switch (type) { @@ -437,7 +450,7 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D } } return transportConfiguration; -} +}; export enum AlarmConditionType { SIMPLE = 'SIMPLE', @@ -480,7 +493,7 @@ export const AlarmScheduleTypeTranslationMap = new Map + !(!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length); + +export const alarmRuleValidator = (control: AbstractControl): ValidationErrors | null => { const alarmRule: AlarmRule = control.value; return alarmRuleValid(alarmRule) ? null : {alarmRule: true}; -} - -function alarmRuleValid(alarmRule: AlarmRule): boolean { - if (!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length) { - return false; - } - return true; -} +}; export interface DeviceProfileAlarm { id: string; @@ -528,7 +539,7 @@ export interface DeviceProfileAlarm { propagateRelationTypes?: Array; } -export function deviceProfileAlarmValidator(control: AbstractControl): ValidationErrors | null { +export const deviceProfileAlarmValidator = (control: AbstractControl): ValidationErrors | null => { const deviceProfileAlarm: DeviceProfileAlarm = control.value; if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType && deviceProfileAlarm.createRules) { @@ -553,7 +564,7 @@ export function deviceProfileAlarmValidator(control: AbstractControl): Validatio } } return {deviceProfileAlarm: true}; -} +}; export interface DeviceProfileData { @@ -579,9 +590,11 @@ export interface DeviceProfile extends BaseData, ExportableEnti firmwareId?: OtaPackageId; softwareId?: OtaPackageId; profileData: DeviceProfileData; + defaultEdgeRuleChainId?: RuleChainId; } export interface DeviceProfileInfo extends EntityInfoData { + tenantId?: TenantId; type: DeviceProfileType; transportType: DeviceTransportType; image?: string; @@ -705,6 +718,47 @@ export interface DeviceInfo extends Device { customerTitle: string; customerIsPublic: boolean; deviceProfileName: string; + active: boolean; +} + +export interface DeviceInfoFilter { + customerId?: CustomerId; + edgeId?: EdgeId; + type?: string; + deviceProfileId?: DeviceProfileId; + active?: boolean; +} + +export class DeviceInfoQuery { + + pageLink: PageLink; + deviceInfoFilter: DeviceInfoFilter; + + constructor(pageLink: PageLink, deviceInfoFilter: DeviceInfoFilter) { + this.pageLink = pageLink; + this.deviceInfoFilter = deviceInfoFilter; + } + + public toQuery(): string { + let query; + if (this.deviceInfoFilter.customerId) { + query = `/customer/${this.deviceInfoFilter.customerId.id}/deviceInfos`; + } else if (this.deviceInfoFilter.edgeId) { + query = `/edge/${this.deviceInfoFilter.edgeId.id}/devices`; + } else { + query = '/tenant/deviceInfos'; + } + query += this.pageLink.toQuery(); + if (isNotEmptyStr(this.deviceInfoFilter.type)) { + query += `&type=${this.deviceInfoFilter.type}`; + } else if (this.deviceInfoFilter.deviceProfileId) { + query += `&deviceProfileId=${this.deviceInfoFilter.deviceProfileId.id}`; + } + if (isDefinedAndNotNull(this.deviceInfoFilter.active)) { + query += `&active=${this.deviceInfoFilter.active}`; + } + return query; + } } export enum DeviceCredentialsType { @@ -750,13 +804,11 @@ export interface DeviceCredentialMQTTBasic { password: string; } -export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic { - return { - clientId: '', - userName: '', - password: '' - }; -} +export const getDeviceCredentialMQTTDefault = (): DeviceCredentialMQTTBasic => ({ + clientId: '', + userName: '', + password: '' +}); export interface DeviceSearchQuery extends EntitySearchQuery { deviceTypes: Array; @@ -787,44 +839,23 @@ export const dayOfWeekTranslations = new Array( 'device-profile.schedule-day.sunday' ); -export function getDayString(day: number): string { - switch (day) { - case 0: - return 'device-profile.schedule-day.monday'; - case 1: - return this.translate.instant('device-profile.schedule-day.tuesday'); - case 2: - return this.translate.instant('device-profile.schedule-day.wednesday'); - case 3: - return this.translate.instant('device-profile.schedule-day.thursday'); - case 4: - return this.translate.instant('device-profile.schedule-day.friday'); - case 5: - return this.translate.instant('device-profile.schedule-day.saturday'); - case 6: - return this.translate.instant('device-profile.schedule-day.sunday'); - } -} - -export function timeOfDayToUTCTimestamp(date: Date | number): number { +export const timeOfDayToUTCTimestamp = (date: Date | number): number => { if (typeof date === 'number' || date === null) { return 0; } return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); -} +}; -export function utcTimestampToTimeOfDay(time = 0): Date { - return new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000); -} +export const utcTimestampToTimeOfDay = (time = 0): Date => new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000); -function timeOfDayToMoment(date: Date | number): _moment.Moment { +const timeOfDayToMoment = (date: Date | number): _moment.Moment => { if (typeof date === 'number' || date === null) { return _moment([1970, 0, 1, 0, 0, 0, 0]); } return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]); -} +}; -export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string { +export const getAlarmScheduleRangeText = (startsOn: Date | number, endsOn: Date | number): string => { const start = timeOfDayToMoment(startsOn); const end = timeOfDayToMoment(endsOn); if (start < end) { @@ -834,4 +865,4 @@ export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date } return `12:00 AM – ${end.format('hh:mm A')}` + ` and ${start.format('hh:mm A')} – 12:00 PM`; -} +}; diff --git a/ui-ngx/src/app/shared/models/edge.models.ts b/ui-ngx/src/app/shared/models/edge.models.ts index 59f9d54318..372a910402 100644 --- a/ui-ngx/src/app/shared/models/edge.models.ts +++ b/ui-ngx/src/app/shared/models/edge.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -167,3 +167,7 @@ export interface EdgeEvent extends BaseData { uid: string; body: string; } + +export interface EdgeInstallInstructions { + dockerInstallInstructions: string; +} diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 83a7ebebf8..1a5f187459 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -38,7 +38,12 @@ export enum EntityType { TB_RESOURCE = 'TB_RESOURCE', OTA_PACKAGE = 'OTA_PACKAGE', RPC = 'RPC', - QUEUE = 'QUEUE' + QUEUE = 'QUEUE', + NOTIFICATION = 'NOTIFICATION', + NOTIFICATION_REQUEST = 'NOTIFICATION_REQUEST', + NOTIFICATION_RULE = 'NOTIFICATION_RULE', + NOTIFICATION_TARGET = 'NOTIFICATION_TARGET', + NOTIFICATION_TEMPLATE = 'NOTIFICATION_TEMPLATE' } export enum AliasEntityType { @@ -251,7 +256,7 @@ export const entityTypeTranslations = new Map([ [EntityType.CUSTOMER, '/customers'], [EntityType.USER, '/users'], [EntityType.DASHBOARD, '/dashboards'], - [EntityType.ASSET, '/assets'], - [EntityType.DEVICE, '/devices'], + [EntityType.ASSET, '/entities/assets'], + [EntityType.DEVICE, '/entities/devices'], [EntityType.DEVICE_PROFILE, '/profiles/deviceProfiles'], [EntityType.ASSET_PROFILE, '/profiles/assetProfiles'], [EntityType.RULE_CHAIN, '/ruleChains'], - [EntityType.EDGE, '/edgeInstances'], - [EntityType.ENTITY_VIEW, '/entityViews'], - [EntityType.TB_RESOURCE, '/settings/resources-library'], - [EntityType.OTA_PACKAGE, '/otaUpdates'], + [EntityType.EDGE, '/edgeManagement/instances'], + [EntityType.ENTITY_VIEW, '/entities/entityViews'], + [EntityType.TB_RESOURCE, '/resources/resources-library'], + [EntityType.OTA_PACKAGE, '/features/otaUpdates'], [EntityType.QUEUE, '/settings/queues'] ]); diff --git a/ui-ngx/src/app/shared/models/entity-view.models.ts b/ui-ngx/src/app/shared/models/entity-view.models.ts index 9605edfc85..dd16914bc1 100644 --- a/ui-ngx/src/app/shared/models/entity-view.models.ts +++ b/ui-ngx/src/app/shared/models/entity-view.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 8bcfa6c34a..548b912b04 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/error.models.ts b/ui-ngx/src/app/shared/models/error.models.ts index ddc0b676ce..794d8f6891 100644 --- a/ui-ngx/src/app/shared/models/error.models.ts +++ b/ui-ngx/src/app/shared/models/error.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/event.models.ts b/ui-ngx/src/app/shared/models/event.models.ts index 89e93d4fa1..9664c88fcf 100644 --- a/ui-ngx/src/app/shared/models/event.models.ts +++ b/ui-ngx/src/app/shared/models/event.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/alarm-comment-id.ts b/ui-ngx/src/app/shared/models/id/alarm-comment-id.ts new file mode 100644 index 0000000000..c161e3095c --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/alarm-comment-id.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HasUUID } from '@shared/models/id/has-uuid'; + +export class AlarmCommentId implements HasUUID { + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/alarm-id.ts b/ui-ngx/src/app/shared/models/id/alarm-id.ts index 95d0deb18f..3cd21f1157 100644 --- a/ui-ngx/src/app/shared/models/id/alarm-id.ts +++ b/ui-ngx/src/app/shared/models/id/alarm-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/asset-id.ts b/ui-ngx/src/app/shared/models/id/asset-id.ts index 2a8a911428..9adf209128 100644 --- a/ui-ngx/src/app/shared/models/id/asset-id.ts +++ b/ui-ngx/src/app/shared/models/id/asset-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/asset-profile-id.ts b/ui-ngx/src/app/shared/models/id/asset-profile-id.ts index 8fc021a16f..55d36db2fa 100644 --- a/ui-ngx/src/app/shared/models/id/asset-profile-id.ts +++ b/ui-ngx/src/app/shared/models/id/asset-profile-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/audit-log-id.ts b/ui-ngx/src/app/shared/models/id/audit-log-id.ts index ba5e022dbf..bccaa6ed17 100644 --- a/ui-ngx/src/app/shared/models/id/audit-log-id.ts +++ b/ui-ngx/src/app/shared/models/id/audit-log-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/customer-id.ts b/ui-ngx/src/app/shared/models/id/customer-id.ts index e5a6a49601..8e55155c53 100644 --- a/ui-ngx/src/app/shared/models/id/customer-id.ts +++ b/ui-ngx/src/app/shared/models/id/customer-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/dashboard-id.ts b/ui-ngx/src/app/shared/models/id/dashboard-id.ts index 54ae24c2d5..a83900193d 100644 --- a/ui-ngx/src/app/shared/models/id/dashboard-id.ts +++ b/ui-ngx/src/app/shared/models/id/dashboard-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/device-credentials-id.ts b/ui-ngx/src/app/shared/models/id/device-credentials-id.ts index 1f428c0be8..21b73659a6 100644 --- a/ui-ngx/src/app/shared/models/id/device-credentials-id.ts +++ b/ui-ngx/src/app/shared/models/id/device-credentials-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/device-id.ts b/ui-ngx/src/app/shared/models/id/device-id.ts index 2432215133..71cc69de5a 100644 --- a/ui-ngx/src/app/shared/models/id/device-id.ts +++ b/ui-ngx/src/app/shared/models/id/device-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/device-profile-id.ts b/ui-ngx/src/app/shared/models/id/device-profile-id.ts index a50def52ad..a9596461eb 100644 --- a/ui-ngx/src/app/shared/models/id/device-profile-id.ts +++ b/ui-ngx/src/app/shared/models/id/device-profile-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/edge-id.ts b/ui-ngx/src/app/shared/models/id/edge-id.ts index ef21c384e7..5d26c14fee 100644 --- a/ui-ngx/src/app/shared/models/id/edge-id.ts +++ b/ui-ngx/src/app/shared/models/id/edge-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/entity-id.ts b/ui-ngx/src/app/shared/models/id/entity-id.ts index b208de441b..dc71119cbc 100644 --- a/ui-ngx/src/app/shared/models/id/entity-id.ts +++ b/ui-ngx/src/app/shared/models/id/entity-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/entity-view-id.ts b/ui-ngx/src/app/shared/models/id/entity-view-id.ts index d48f29955f..4ee80a8790 100644 --- a/ui-ngx/src/app/shared/models/id/entity-view-id.ts +++ b/ui-ngx/src/app/shared/models/id/entity-view-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/event-id.ts b/ui-ngx/src/app/shared/models/id/event-id.ts index 9d84b95a91..af15972856 100644 --- a/ui-ngx/src/app/shared/models/id/event-id.ts +++ b/ui-ngx/src/app/shared/models/id/event-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/has-uuid.ts b/ui-ngx/src/app/shared/models/id/has-uuid.ts index 2c6cb83715..e53f008c1f 100644 --- a/ui-ngx/src/app/shared/models/id/has-uuid.ts +++ b/ui-ngx/src/app/shared/models/id/has-uuid.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/notification-id.ts b/ui-ngx/src/app/shared/models/id/notification-id.ts new file mode 100644 index 0000000000..a3286342bb --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/notification-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class NotificationId implements EntityId { + entityType = EntityType.NOTIFICATION; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/notification-request-id.ts b/ui-ngx/src/app/shared/models/id/notification-request-id.ts new file mode 100644 index 0000000000..518ccc884d --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/notification-request-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class NotificationRequestId implements EntityId { + entityType = EntityType.NOTIFICATION_REQUEST; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/notification-rule-id.ts b/ui-ngx/src/app/shared/models/id/notification-rule-id.ts new file mode 100644 index 0000000000..7c1941db65 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/notification-rule-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class NotificationRuleId implements EntityId { + entityType = EntityType.NOTIFICATION_RULE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/notification-target-id.ts b/ui-ngx/src/app/shared/models/id/notification-target-id.ts new file mode 100644 index 0000000000..ad0ac93f4f --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/notification-target-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class NotificationTargetId implements EntityId { + entityType = EntityType.NOTIFICATION_TARGET; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/notification-template-id.ts b/ui-ngx/src/app/shared/models/id/notification-template-id.ts new file mode 100644 index 0000000000..be93db5d21 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/notification-template-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class NotificationTemplateId implements EntityId { + entityType = EntityType.NOTIFICATION_TEMPLATE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/ota-package-id.ts b/ui-ngx/src/app/shared/models/id/ota-package-id.ts index 41b3f9b7bb..1bf241ee9a 100644 --- a/ui-ngx/src/app/shared/models/id/ota-package-id.ts +++ b/ui-ngx/src/app/shared/models/id/ota-package-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/public-api.ts b/ui-ngx/src/app/shared/models/id/public-api.ts index f30a5ffbcf..fd6e5854d8 100644 --- a/ui-ngx/src/app/shared/models/id/public-api.ts +++ b/ui-ngx/src/app/shared/models/id/public-api.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -26,6 +26,11 @@ export * from './entity-id'; export * from './entity-view-id'; export * from './event-id'; export * from './has-uuid'; +export * from './notification-id'; +export * from './notification-request-id'; +export * from './notification-rule-id'; +export * from './notification-target-id'; +export * from './notification-template-id'; export * from './ota-package-id'; export * from './rpc-id'; export * from './rule-chain-id'; diff --git a/ui-ngx/src/app/shared/models/id/queue-id.ts b/ui-ngx/src/app/shared/models/id/queue-id.ts index b36a12547b..743583e32e 100644 --- a/ui-ngx/src/app/shared/models/id/queue-id.ts +++ b/ui-ngx/src/app/shared/models/id/queue-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/rpc-id.ts b/ui-ngx/src/app/shared/models/id/rpc-id.ts index a3b9cfec91..7173237ac0 100644 --- a/ui-ngx/src/app/shared/models/id/rpc-id.ts +++ b/ui-ngx/src/app/shared/models/id/rpc-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/rule-chain-id.ts b/ui-ngx/src/app/shared/models/id/rule-chain-id.ts index ebcc47d415..27623bd15a 100644 --- a/ui-ngx/src/app/shared/models/id/rule-chain-id.ts +++ b/ui-ngx/src/app/shared/models/id/rule-chain-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/rule-node-id.ts b/ui-ngx/src/app/shared/models/id/rule-node-id.ts index 68ebfc2925..73d57f68f2 100644 --- a/ui-ngx/src/app/shared/models/id/rule-node-id.ts +++ b/ui-ngx/src/app/shared/models/id/rule-node-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/tb-resource-id.ts b/ui-ngx/src/app/shared/models/id/tb-resource-id.ts index f36aa36890..57376181e0 100644 --- a/ui-ngx/src/app/shared/models/id/tb-resource-id.ts +++ b/ui-ngx/src/app/shared/models/id/tb-resource-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/tenant-id.ts b/ui-ngx/src/app/shared/models/id/tenant-id.ts index 078dd0b399..59864eb9d9 100644 --- a/ui-ngx/src/app/shared/models/id/tenant-id.ts +++ b/ui-ngx/src/app/shared/models/id/tenant-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/tenant-profile-id.ts b/ui-ngx/src/app/shared/models/id/tenant-profile-id.ts index 0cef7d7d13..a829b4e82c 100644 --- a/ui-ngx/src/app/shared/models/id/tenant-profile-id.ts +++ b/ui-ngx/src/app/shared/models/id/tenant-profile-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/user-id.ts b/ui-ngx/src/app/shared/models/id/user-id.ts index 392378cd66..59c46be97f 100644 --- a/ui-ngx/src/app/shared/models/id/user-id.ts +++ b/ui-ngx/src/app/shared/models/id/user-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/widget-type-id.ts b/ui-ngx/src/app/shared/models/id/widget-type-id.ts index 38575843be..298ef70ae1 100644 --- a/ui-ngx/src/app/shared/models/id/widget-type-id.ts +++ b/ui-ngx/src/app/shared/models/id/widget-type-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts b/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts index 9b6f7cee44..fc16726820 100644 --- a/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts +++ b/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/login.models.ts b/ui-ngx/src/app/shared/models/login.models.ts index d3f1598fc5..0da429f239 100644 --- a/ui-ngx/src/app/shared/models/login.models.ts +++ b/ui-ngx/src/app/shared/models/login.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts b/ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts index 9c6b5cce6a..26cff38c28 100644 --- a/ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts +++ b/ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/material.models.ts b/ui-ngx/src/app/shared/models/material.models.ts index 5defa7d044..afeea18cfc 100644 --- a/ui-ngx/src/app/shared/models/material.models.ts +++ b/ui-ngx/src/app/shared/models/material.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,9 +14,7 @@ /// limitations under the License. /// -import * as tinycolor_ from 'tinycolor2'; - -const tinycolor = tinycolor_; +import tinycolor from 'tinycolor2'; export interface MaterialColorItem { value: string; diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts new file mode 100644 index 0000000000..59af9295e1 --- /dev/null +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -0,0 +1,577 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NotificationId } from '@shared/models/id/notification-id'; +import { NotificationRequestId } from '@shared/models/id/notification-request-id'; +import { UserId } from '@shared/models/id/user-id'; +import { BaseData, ExportableEntity } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { NotificationTargetId } from '@shared/models/id/notification-target-id'; +import { NotificationTemplateId } from '@shared/models/id/notification-template-id'; +import { EntityId } from '@shared/models/id/entity-id'; +import { NotificationRuleId } from '@shared/models/id/notification-rule-id'; +import { AlarmSearchStatus, AlarmSeverity, AlarmStatus } from '@shared/models/alarm.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { ApiFeature, ApiUsageStateValue } from '@shared/models/api-usage.models'; + +export interface Notification { + readonly id: NotificationId; + readonly requestId: NotificationRequestId; + readonly recipientId: UserId; + readonly type: NotificationType; + readonly subject: string; + readonly text: string; + readonly info?: NotificationInfo; + readonly status: NotificationStatus; + readonly createdTime: number; + readonly additionalConfig?: WebDeliveryMethodAdditionalConfig; +} + +export interface NotificationInfo { + description: string; + type: string; + alarmSeverity?: AlarmSeverity; + alarmStatus?: AlarmStatus; + alarmType?: string; + stateEntityId?: EntityId; +} + +export interface NotificationRequest extends Omit, 'label'> { + tenantId?: TenantId; + targets: Array; + templateId?: NotificationTemplateId; + template?: NotificationTemplate; + info?: NotificationInfo; + deliveryMethods: Array; + originatorEntityId: EntityId; + status: NotificationRequestStatus; + stats: NotificationRequestStats; + additionalConfig: NotificationRequestConfig; +} + +export interface NotificationRequestInfo extends NotificationRequest { + templateName: string; + deliveryMethods: NotificationDeliveryMethod[]; +} + +export interface NotificationRequestPreview { + totalRecipientsCount: number; + recipientsCountByTarget: { [key in string]: number }; + processedTemplates: { [key in NotificationDeliveryMethod]: DeliveryMethodNotificationTemplate }; + recipientsPreview: Array; +} + +export interface NotificationRequestStats { + sent: Map; + errors: { [key in NotificationDeliveryMethod]: {[errorKey in string]: string}}; + processedRecipients: Map>; +} + +export interface NotificationRequestConfig { + sendingDelayInSec: number; +} + +export interface NotificationSettings { + deliveryMethodsConfigs: { [key in NotificationDeliveryMethod]: NotificationDeliveryMethodConfig }; +} + +export interface NotificationDeliveryMethodConfig extends Partial{ + enabled: boolean; + method: NotificationDeliveryMethod; +} + +interface SlackNotificationDeliveryMethodConfig { + botToken: string; +} + +export interface SlackConversation { + id: string; + title: string; + name: string; + wholeName: string; + email: string; + type: string; +} + +export interface NotificationRule extends Omit, 'label'>, ExportableEntity { + tenantId: TenantId; + enabled: boolean; + templateId: NotificationTemplateId; + triggerType: TriggerType; + triggerConfig: NotificationRuleTriggerConfig; + recipientsConfig: NotificationRuleRecipientConfig; + additionalConfig: {description: string}; +} + +export type NotificationRuleTriggerConfig = Partial; + +export interface AlarmNotificationRuleTriggerConfig { + alarmTypes?: Array; + alarmSeverities?: Array; + notifyOn: Array; + clearRule?: ClearRule; +} + +interface ClearRule { + alarmStatuses: Array; +} + +export interface DeviceInactivityNotificationRuleTriggerConfig { + devices?: Array; + deviceProfiles?: Array; +} + +export interface EntityActionNotificationRuleTriggerConfig { + entityTypes: EntityType[]; + created: boolean; + updated: boolean; + deleted: boolean; +} + +export interface AlarmCommentNotificationRuleTriggerConfig { + alarmTypes?: Array; + alarmSeverities?: Array; + alarmStatuses?: Array; + onlyUserComments?: boolean; + notifyOnCommentUpdate?: boolean; +} + +export interface AlarmAssignmentNotificationRuleTriggerConfig { + alarmTypes?: Array; + alarmSeverities?: Array; + alarmStatuses?: Array; + notifyOn: Array; +} + +export interface RuleEngineLifecycleEventNotificationRuleTriggerConfig { + ruleChains?: Array; + ruleChainEvents?: Array; + onlyRuleChainLifecycleFailures: boolean; + trackRuleNodeEvents: boolean; + ruleNodeEvents: Array; + onlyRuleNodeLifecycleFailures: ComponentLifecycleEvent; +} + +export interface EntitiesLimitNotificationRuleTriggerConfig { + entityTypes: EntityType[]; + threshold: number; +} + +export interface ApiUsageLimitNotificationRuleTriggerConfig { + apiFeatures: ApiFeature[]; + notifyOn: ApiUsageStateValue[]; +} + +export enum ComponentLifecycleEvent { + STARTED = 'STARTED', + UPDATED = 'UPDATED', + STOPPED = 'STOPPED' +} + +export const ComponentLifecycleEventTranslationMap = new Map([ + [ComponentLifecycleEvent.STARTED, 'event.started'], + [ComponentLifecycleEvent.UPDATED, 'event.updated'], + [ComponentLifecycleEvent.STOPPED, 'event.stopped'] +]); + +export enum AlarmAction { + CREATED = 'CREATED', + SEVERITY_CHANGED = 'SEVERITY_CHANGED', + ACKNOWLEDGED = 'ACKNOWLEDGED', + CLEARED = 'CLEARED' +} + +export const AlarmActionTranslationMap = new Map([ + [AlarmAction.CREATED, 'notification.notify-alarm-action.created'], + [AlarmAction.SEVERITY_CHANGED, 'notification.notify-alarm-action.severity-changed'], + [AlarmAction.ACKNOWLEDGED, 'notification.notify-alarm-action.acknowledged'], + [AlarmAction.CLEARED, 'notification.notify-alarm-action.cleared'] +]); + +export enum AlarmAssignmentAction { + ASSIGNED = 'ASSIGNED', + UNASSIGNED = 'UNASSIGNED' +} + +export const AlarmAssignmentActionTranslationMap = new Map([ + [AlarmAssignmentAction.ASSIGNED, 'notification.notify-alarm-action.assigned'], + [AlarmAssignmentAction.UNASSIGNED, 'notification.notify-alarm-action.unassigned'] +]); + +export enum DeviceEvent { + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE' +} + +export const DeviceEventTranslationMap = new Map([ + [DeviceEvent.ACTIVE, 'notification.active'], + [DeviceEvent.INACTIVE, 'notification.inactive'] +]); + +export interface NotificationRuleRecipientConfig { + targets?: Array; + escalationTable?: {[key: number]: Array}; +} + +export interface NonConfirmedNotificationEscalation { + delayInSec: number; + targets: Array; +} + +export interface NotificationTarget extends Omit, 'label'>, ExportableEntity { + tenantId: TenantId; + configuration: NotificationTargetConfig; +} + +export interface NotificationTargetConfig extends Partial { + description?: string; + type: NotificationTargetType; +} +export interface PlatformUsersNotificationTargetConfig { + usersFilter: UsersFilter; +} + +export interface UsersFilter extends + Partial{ + type: NotificationTargetConfigType; +} + +interface UserListFilter { + usersIds: Array; +} + +interface CustomerUsersFilter { + customerId: string; +} + +interface TenantAdministratorsFilter { + tenantsIds?: Array; + tenantProfilesIds?: Array; +} + +export interface SlackNotificationTargetConfig { + conversationType: SlackChanelType; + conversation: SlackConversation; +} +export enum NotificationTargetType { + PLATFORM_USERS = 'PLATFORM_USERS', + SLACK = 'SLACK' +} + +export const NotificationTargetTypeTranslationMap = new Map([ + [NotificationTargetType.PLATFORM_USERS, 'notification.platform-users'], + [NotificationTargetType.SLACK, 'notification.delivery-method.slack'] +]); + +export interface NotificationTemplate extends Omit, 'label'>, ExportableEntity { + tenantId: TenantId; + notificationType: NotificationType; + configuration: NotificationTemplateConfig; +} + +interface NotificationTemplateConfig { + deliveryMethodsTemplates: { + [key in NotificationDeliveryMethod]: DeliveryMethodNotificationTemplate + }; +} + +export interface DeliveryMethodNotificationTemplate extends + Partial{ + body?: string; + enabled: boolean; + method: NotificationDeliveryMethod; +} + +interface WebDeliveryMethodNotificationTemplate { + subject?: string; + additionalConfig: WebDeliveryMethodAdditionalConfig; +} + +interface WebDeliveryMethodAdditionalConfig { + icon: { + enabled: boolean; + icon: string; + color: string; + }; + actionButtonConfig: { + enabled: boolean; + text: string; + linkType: ActionButtonLinkType; + link?: string; + dashboardId?: string; + dashboardState?: string; + setEntityIdInState?: boolean; + }; +} + +interface EmailDeliveryMethodNotificationTemplate { + subject: string; +} + +interface SlackDeliveryMethodNotificationTemplate { + conversationType: SlackChanelType; + conversationId: string; +} + +export enum NotificationStatus { + SENT = 'SENT', + READ = 'READ' +} + +export enum NotificationDeliveryMethod { + WEB = 'WEB', + SMS = 'SMS', + EMAIL = 'EMAIL', + SLACK = 'SLACK' +} + +export const NotificationDeliveryMethodTranslateMap = new Map([ + [NotificationDeliveryMethod.WEB, 'notification.delivery-method.web'], + [NotificationDeliveryMethod.SMS, 'notification.delivery-method.sms'], + [NotificationDeliveryMethod.EMAIL, 'notification.delivery-method.email'], + [NotificationDeliveryMethod.SLACK, 'notification.delivery-method.slack'] +]); + +export enum NotificationRequestStatus { + PROCESSING = 'PROCESSING', + SCHEDULED = 'SCHEDULED', + SENT = 'SENT' +} + +export const NotificationRequestStatusTranslateMap = new Map([ + [NotificationRequestStatus.PROCESSING, 'notification.request-status.processing'], + [NotificationRequestStatus.SCHEDULED, 'notification.request-status.scheduled'], + [NotificationRequestStatus.SENT, 'notification.request-status.sent'] +]); + +export enum SlackChanelType { + PUBLIC_CHANNEL = 'PUBLIC_CHANNEL', + PRIVATE_CHANNEL = 'PRIVATE_CHANNEL', + DIRECT= 'DIRECT' +} + +export const SlackChanelTypesTranslateMap = new Map([ + [SlackChanelType.DIRECT, 'notification.slack-chanel-types.direct'], + [SlackChanelType.PUBLIC_CHANNEL, 'notification.slack-chanel-types.public-channel'], + [SlackChanelType.PRIVATE_CHANNEL, 'notification.slack-chanel-types.private-channel'] +]); + +export enum NotificationTargetConfigType { + ALL_USERS = 'ALL_USERS', + TENANT_ADMINISTRATORS = 'TENANT_ADMINISTRATORS', + CUSTOMER_USERS = 'CUSTOMER_USERS', + USER_LIST = 'USER_LIST', + ORIGINATOR_ENTITY_OWNER_USERS = 'ORIGINATOR_ENTITY_OWNER_USERS', + AFFECTED_USER = 'AFFECTED_USER', + SYSTEM_ADMINISTRATORS = 'SYSTEM_ADMINISTRATORS', + AFFECTED_TENANT_ADMINISTRATORS = 'AFFECTED_TENANT_ADMINISTRATORS' +} + +export interface NotificationTargetConfigTypeInfo { + name: string; + hint?: string; +} + +export const NotificationTargetConfigTypeInfoMap = new Map([ + [NotificationTargetConfigType.ALL_USERS, + { + name: 'notification.recipient-type.all-users' + } + ], + [NotificationTargetConfigType.TENANT_ADMINISTRATORS, + { + name: 'notification.recipient-type.tenant-administrators' + } + ], + [NotificationTargetConfigType.CUSTOMER_USERS, + { + name: 'notification.recipient-type.customer-users' + } + ], + [NotificationTargetConfigType.USER_LIST, + { + name: 'notification.recipient-type.user-list' + } + ], + [NotificationTargetConfigType.ORIGINATOR_ENTITY_OWNER_USERS, + { + name: 'notification.recipient-type.users-entity-owner', + hint: 'notification.recipient-type.users-entity-owner-hint' + } + ], + [NotificationTargetConfigType.AFFECTED_USER, + { + name: 'notification.recipient-type.affected-user', + hint: 'notification.recipient-type.affected-user-hint' + } + ], + [NotificationTargetConfigType.SYSTEM_ADMINISTRATORS, + { + name: 'notification.recipient-type.system-administrators' + } + ], + [NotificationTargetConfigType.AFFECTED_TENANT_ADMINISTRATORS, + { + name: 'notification.recipient-type.affected-tenant-administrators' + } + ] +]); + +export enum NotificationType { + GENERAL = 'GENERAL', + ALARM = 'ALARM', + DEVICE_ACTIVITY = 'DEVICE_ACTIVITY', + ENTITY_ACTION = 'ENTITY_ACTION', + ALARM_COMMENT = 'ALARM_COMMENT', + ALARM_ASSIGNMENT = 'ALARM_ASSIGNMENT', + RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT = 'RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT', + ENTITIES_LIMIT = 'ENTITIES_LIMIT', + API_USAGE_LIMIT = 'API_USAGE_LIMIT', + NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION', + RULE_NODE = 'RULE_NODE' +} + +export const NotificationTypeIcons = new Map([ + [NotificationType.ALARM, 'warning'], + [NotificationType.DEVICE_ACTIVITY, 'phonelink_off'], + [NotificationType.ENTITY_ACTION, 'devices'], + [NotificationType.ALARM_COMMENT, 'comment'], + [NotificationType.ALARM_ASSIGNMENT, 'assignment_turned_in'], + [NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, 'settings_ethernet'], + [NotificationType.ENTITIES_LIMIT, 'data_thresholding'], + [NotificationType.API_USAGE_LIMIT, 'insert_chart'], +]); + +export const AlarmSeverityNotificationColors = new Map( + [ + [AlarmSeverity.CRITICAL, '#D12730'], + [AlarmSeverity.MAJOR, '#FEAC0C'], + [AlarmSeverity.MINOR, '#F2DA05'], + [AlarmSeverity.WARNING, '#F66716'], + [AlarmSeverity.INDETERMINATE, '#00000061'] + ] +); + +export enum ActionButtonLinkType { + LINK = 'LINK', + DASHBOARD = 'DASHBOARD' +} + +export const ActionButtonLinkTypeTranslateMap = new Map([ + [ActionButtonLinkType.LINK, 'notification.link-type.link'], + [ActionButtonLinkType.DASHBOARD, 'notification.link-type.dashboard'] +]); + +export interface NotificationTemplateTypeTranslate { + name: string; + helpId?: string; +} + +export const NotificationTemplateTypeTranslateMap = new Map([ + [NotificationType.GENERAL, + { + name: 'notification.template-type.general', + helpId: 'notification/general' + } + ], + [NotificationType.ALARM, + { + name: 'notification.template-type.alarm', + helpId: 'notification/alarm' + } + ], + [NotificationType.DEVICE_ACTIVITY, + { + name: 'notification.template-type.device-activity', + helpId: 'notification/device_activity' + } + ], + [NotificationType.ENTITY_ACTION, + { + name: 'notification.template-type.entity-action', + helpId: 'notification/entity_action' + } + ], + [NotificationType.ALARM_COMMENT, + { + name: 'notification.template-type.alarm-comment', + helpId: 'notification/alarm_comment' + } + ], + [NotificationType.ALARM_ASSIGNMENT, + { + name: 'notification.template-type.alarm-assignment', + helpId: 'notification/alarm_assignment' + } + ], + [NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, + { + name: 'notification.template-type.rule-engine-lifecycle-event', + helpId: 'notification/rule_engine_lifecycle_event' + } + ], + [NotificationType.ENTITIES_LIMIT, + { + name: 'notification.template-type.entities-limit', + helpId: 'notification/entities_limit' + } + ], + [NotificationType.API_USAGE_LIMIT, + { + name: 'notification.template-type.api-usage-limit', + helpId: 'notification/api_usage_limit' + } + ], + [NotificationType.NEW_PLATFORM_VERSION, + { + name: 'notification.template-type.new-platform-version', + helpId: 'notification/new_platform_version' + } + ], + [NotificationType.RULE_NODE, + { + name: 'notification.template-type.rule-node', + helpId: 'notification/rule_node' + } + ] +]); + +export enum TriggerType { + ALARM = 'ALARM', + DEVICE_ACTIVITY = 'DEVICE_ACTIVITY', + ENTITY_ACTION = 'ENTITY_ACTION', + ALARM_COMMENT = 'ALARM_COMMENT', + ALARM_ASSIGNMENT = 'ALARM_ASSIGNMENT', + RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT = 'RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT', + ENTITIES_LIMIT = 'ENTITIES_LIMIT', + API_USAGE_LIMIT = 'API_USAGE_LIMIT', + NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION' +} + +export const TriggerTypeTranslationMap = new Map([ + [TriggerType.ALARM, 'notification.trigger.alarm'], + [TriggerType.DEVICE_ACTIVITY, 'notification.trigger.device-activity'], + [TriggerType.ENTITY_ACTION, 'notification.trigger.entity-action'], + [TriggerType.ALARM_COMMENT, 'notification.trigger.alarm-comment'], + [TriggerType.ALARM_ASSIGNMENT, 'notification.trigger.alarm-assignment'], + [TriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, 'notification.trigger.rule-engine-lifecycle-event'], + [TriggerType.ENTITIES_LIMIT, 'notification.trigger.entities-limit'], + [TriggerType.API_USAGE_LIMIT, 'notification.trigger.api-usage-limit'], + [TriggerType.NEW_PLATFORM_VERSION, 'notification.trigger.new-platform-version'], +]); diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts index b774546547..483a9553ab 100644 --- a/ui-ngx/src/app/shared/models/oauth2.models.ts +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/ota-package.models.ts b/ui-ngx/src/app/shared/models/ota-package.models.ts index 34963be66b..3af7b9ae30 100644 --- a/ui-ngx/src/app/shared/models/ota-package.models.ts +++ b/ui-ngx/src/app/shared/models/ota-package.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/page/page-data.ts b/ui-ngx/src/app/shared/models/page/page-data.ts index bc42f93d79..6c144325ee 100644 --- a/ui-ngx/src/app/shared/models/page/page-data.ts +++ b/ui-ngx/src/app/shared/models/page/page-data.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index 683b83298c..586b246f1e 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { getDescendantProp, isObject } from '@core/utils'; import { SortDirection } from '@angular/material/sort'; +import { EntitiesTableAction } from '@home/models/entity/entity-table-component.models'; export const MAX_SAFE_PAGE_SIZE = 2147483647; @@ -27,6 +28,7 @@ export interface PageQueryParam extends Partial{ textSearch?: string; pageSize?: number; page?: number; + action?: EntitiesTableAction; } export function defaultPageLinkSearchFunction(searchProperty?: string): PageLinkSearchFunction { diff --git a/ui-ngx/src/app/shared/models/page/public-api.ts b/ui-ngx/src/app/shared/models/page/public-api.ts index eefdd67f71..3bf44ef8e7 100644 --- a/ui-ngx/src/app/shared/models/page/public-api.ts +++ b/ui-ngx/src/app/shared/models/page/public-api.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/page/sort-order.ts b/ui-ngx/src/app/shared/models/page/sort-order.ts index 253d3b313b..f21840c6eb 100644 --- a/ui-ngx/src/app/shared/models/page/sort-order.ts +++ b/ui-ngx/src/app/shared/models/page/sort-order.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index 8c97669481..dbc5e11213 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ export * from './telemetry/telemetry.models'; export * from './time/time.models'; export * from './alarm.models'; export * from './alias.models'; +export * from './api-usage.models'; export * from './asset.models'; export * from './audit-log.models'; export * from './authority.enum'; @@ -38,6 +39,9 @@ export * from './error.models'; export * from './event.models'; export * from './login.models'; export * from './material.models'; +export * from './notification.models'; +export * from './websocket/notification-ws.models'; +export * from './websocket/websocket.models'; export * from './oauth2.models'; export * from './queue.models'; export * from './relation.models'; @@ -48,6 +52,8 @@ export * from './rule-node.models'; export * from './settings.models'; export * from './tenant.model'; export * from './user.model'; +export * from './user-settings.models'; export * from './widget.models'; export * from './widgets-bundle.model'; export * from './window-message.model'; +export * from './usage.models'; 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 c1bf255cd3..7c978fb4c9 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core'; import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models'; import { Filter } from '@material-ui/icons'; import { DatePipe } from '@angular/common'; +import { UserId } from '../id/user-id'; export enum EntityKeyType { ATTRIBUTE = 'ATTRIBUTE', @@ -705,7 +706,7 @@ export interface EntityDataPageLink { dynamic?: boolean; } -export interface AlarmDataPageLink extends EntityDataPageLink { +export interface AlarmFilter { startTs?: number; endTs?: number; timeWindow?: number; @@ -713,8 +714,17 @@ export interface AlarmDataPageLink extends EntityDataPageLink { statusList?: Array; severityList?: Array; searchPropagatedAlarms?: boolean; + assigneeId?: UserId; } +export interface AlarmFilterConfig extends AlarmFilter { + assignedToCurrentUser?: boolean; +} + +export type AlarmCountQuery = EntityCountQuery & AlarmFilter; + +export type AlarmDataPageLink = EntityDataPageLink & AlarmFilter; + export function entityDataPageLinkSortDirection(pageLink: EntityDataPageLink): SortDirection { if (pageLink.sortOrder) { return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection; @@ -739,6 +749,14 @@ export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPag export const singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1); +export const singleEntityFilterFromDeviceId = (deviceId: string): EntityFilter => ({ + type: AliasFilterType.singleEntity, + singleEntity: { + entityType: EntityType.DEVICE, + id: deviceId + } +}); + export interface EntityCountQuery { entityFilter: EntityFilter; keyFilters?: Array; @@ -843,7 +861,8 @@ export function updateDatasourceFromEntityInfo(datasource: Datasource, entity: E }; datasource.entityId = entity.id; datasource.entityType = entity.entityType; - if (datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) { + if (datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount + || datasource.type === DatasourceType.alarmCount) { if (datasource.type === DatasourceType.entity) { datasource.entityName = entity.name; datasource.entityLabel = entity.label; diff --git a/ui-ngx/src/app/shared/models/queue.models.ts b/ui-ngx/src/app/shared/models/queue.models.ts index 5814a6eeb9..76ee0bf022 100644 --- a/ui-ngx/src/app/shared/models/queue.models.ts +++ b/ui-ngx/src/app/shared/models/queue.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts index e42b915e9e..c51c60664c 100644 --- a/ui-ngx/src/app/shared/models/relation.models.ts +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 778cfb57a4..871bf147ea 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/rpc.models.ts b/ui-ngx/src/app/shared/models/rpc.models.ts index b0c97062ae..cbdda5902a 100644 --- a/ui-ngx/src/app/shared/models/rpc.models.ts +++ b/ui-ngx/src/app/shared/models/rpc.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts index 01f9d268c3..fbf6f57268 100644 --- a/ui-ngx/src/app/shared/models/rule-chain.models.ts +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import { TenantId } from '@shared/models/id/tenant-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { RuleNodeId } from '@shared/models/id/rule-node-id'; import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models'; -import { ComponentType } from '@shared/models/component-descriptor.models'; +import { ComponentClusteringMode, ComponentType } from '@shared/models/component-descriptor.models'; export interface RuleChain extends BaseData, ExportableEntity { tenantId: TenantId; @@ -64,6 +64,7 @@ export const ruleNodeTypeComponentTypes: ComponentType[] = export const unknownNodeComponent: RuleNodeComponentDescriptor = { type: RuleNodeType.UNKNOWN, name: 'unknown', + clusteringMode: ComponentClusteringMode.ENABLED, clazz: 'tb.internal.Unknown', configurationDescriptor: { nodeDefinition: { @@ -80,6 +81,7 @@ export const unknownNodeComponent: RuleNodeComponentDescriptor = { export const inputNodeComponent: RuleNodeComponentDescriptor = { type: RuleNodeType.INPUT, + clusteringMode: ComponentClusteringMode.ENABLED, name: 'Input', clazz: 'tb.internal.Input' }; diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 0d224c52e5..dbb065f7f2 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import { PageComponent } from '@shared/components/page.component'; import { AfterViewInit, EventEmitter, Inject, OnInit, Directive } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { AbstractControl, FormGroup } from '@angular/forms'; +import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { RuleChainType } from '@shared/models/rule-chain.models'; export interface RuleNodeConfiguration { @@ -36,6 +36,7 @@ export interface RuleNode extends BaseData { type: string; name: string; debugMode: boolean; + singletonMode: boolean; configuration: RuleNodeConfiguration; additionalInfo?: any; } @@ -77,7 +78,7 @@ export interface IRuleNodeConfigurationComponent { } @Directive() -// tslint:disable-next-line:directive-class-suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix export abstract class RuleNodeConfigurationComponent extends PageComponent implements IRuleNodeConfigurationComponent, OnInit, AfterViewInit { @@ -179,7 +180,7 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple protected onValidate() {} - protected abstract configForm(): FormGroup; + protected abstract configForm(): UntypedFormGroup; protected abstract onConfigurationSet(configuration: RuleNodeConfiguration); @@ -308,6 +309,7 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor { export interface FcRuleNodeType extends FcNode { component?: RuleNodeComponentDescriptor; + singletonMode?: boolean; nodeClass?: string; icon?: string; iconUrl?: string; @@ -373,6 +375,10 @@ export enum MessageType { ATTRIBUTES_DELETED = 'ATTRIBUTES_DELETED', ALARM_ACKNOWLEDGED = 'ALARM_ACKNOWLEDGED', ALARM_CLEARED = 'ALARM_CLEARED', + ALARM_ASSIGNED = 'ALARM_ASSIGNED', + ALARM_UNASSIGNED = 'ALARM_UNASSIGNED', + COMMENT_CREATED = 'COMMENT_CREATED', + COMMENT_UPDATED = 'COMMENT_UPDATED', ENTITY_ASSIGNED_FROM_TENANT = 'ENTITY_ASSIGNED_FROM_TENANT', ENTITY_ASSIGNED_TO_TENANT = 'ENTITY_ASSIGNED_TO_TENANT', TIMESERIES_UPDATED = 'TIMESERIES_UPDATED', @@ -406,6 +412,10 @@ export const messageTypeNames = new Map( [MessageType.ATTRIBUTES_DELETED, 'Attributes Deleted'], [MessageType.ALARM_ACKNOWLEDGED, 'Alarm Acknowledged'], [MessageType.ALARM_CLEARED, 'Alarm Cleared'], + [MessageType.ALARM_ASSIGNED, 'Alarm Assigned'], + [MessageType.ALARM_UNASSIGNED, 'Alarm Unassigned'], + [MessageType.COMMENT_CREATED, 'Comment Created'], + [MessageType.COMMENT_UPDATED, 'Comment Updated'], [MessageType.ENTITY_ASSIGNED_FROM_TENANT, 'Entity Assigned From Tenant'], [MessageType.ENTITY_ASSIGNED_TO_TENANT, 'Entity Assigned To Tenant'], [MessageType.TIMESERIES_UPDATED, 'Timeseries Updated'], @@ -422,6 +432,9 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.geo.TbGpsGeofencingFilterNode': 'ruleNodeGpsGeofencingFilter', 'org.thingsboard.rule.engine.filter.TbJsFilterNode': 'ruleNodeJsFilter', 'org.thingsboard.rule.engine.filter.TbJsSwitchNode': 'ruleNodeJsSwitch', + 'org.thingsboard.rule.engine.filter.TbAssetTypeSwitchNode': 'ruleNodeAssetProfileSwitch', + 'org.thingsboard.rule.engine.filter.TbDeviceTypeSwitchNode': 'ruleNodeDeviceProfileSwitch', + 'org.thingsboard.rule.engine.filter.TbCheckAlarmStatusNode': 'ruleNodeCheckAlarmStatus', 'org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode': 'ruleNodeMessageTypeFilter', 'org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode': 'ruleNodeMessageTypeSwitch', 'org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode': 'ruleNodeOriginatorTypeFilter', @@ -435,6 +448,7 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode': 'ruleNodeRelatedAttributes', 'org.thingsboard.rule.engine.metadata.TbGetTenantAttributeNode': 'ruleNodeTenantAttributes', 'org.thingsboard.rule.engine.metadata.TbGetTenantDetailsNode': 'ruleNodeTenantDetails', + 'org.thingsboard.rule.engine.metadata.CalculateDeltaNode': 'ruleNodeCalculateDelta', 'org.thingsboard.rule.engine.transform.TbChangeOriginatorNode': 'ruleNodeChangeOriginator', 'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg', 'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail', diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts index 8ecb4066f1..0cb02d6257 100644 --- a/ui-ngx/src/app/shared/models/settings.models.ts +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -71,8 +71,12 @@ export interface JwtSettings { } export interface UpdateMessage { - message: string; updateAvailable: boolean; + currentVersion: string; + latestVersion: string; + upgradeInstructionsUrl: string; + currentVersionReleaseNotesUrl: string; + latestVersionReleaseNotesUrl: string; } export const phoneNumberPattern = /^\+[1-9]\d{1,14}$/; @@ -418,6 +422,7 @@ export const repositoryAuthMethodTranslationMap = new Map { if (LatestTelemetry[val]) { return LatestTelemetry[val]; } else { return AttributeScope[val]; } -} +}; export const telemetryTypeTranslations = new Map( [ @@ -222,6 +225,11 @@ export class AlarmDataCmd implements WebsocketCmd { } } +export class AlarmCountCmd implements WebsocketCmd { + cmdId: number; + query?: AlarmCountQuery; +} + export class EntityDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } @@ -234,7 +242,11 @@ export class AlarmDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } -export class TelemetryPluginCmdsWrapper { +export class AlarmCountUnsubscribeCmd implements WebsocketCmd { + cmdId: number; +} + +export class TelemetryPluginCmdsWrapper implements CmdWrapper { constructor() { this.attrSubCmds = []; @@ -246,6 +258,8 @@ export class TelemetryPluginCmdsWrapper { this.alarmDataUnsubscribeCmds = []; this.entityCountCmds = []; this.entityCountUnsubscribeCmds = []; + this.alarmCountCmds = []; + this.alarmCountUnsubscribeCmds = []; } attrSubCmds: Array; tsSubCmds: Array; @@ -256,6 +270,8 @@ export class TelemetryPluginCmdsWrapper { alarmDataUnsubscribeCmds: Array; entityCountCmds: Array; entityCountUnsubscribeCmds: Array; + alarmCountCmds: Array; + alarmCountUnsubscribeCmds: Array; private static popCmds(cmds: Array, leftCount: number): Array { const toPublish = Math.min(cmds.length, leftCount); @@ -275,7 +291,9 @@ export class TelemetryPluginCmdsWrapper { this.alarmDataCmds.length > 0 || this.alarmDataUnsubscribeCmds.length > 0 || this.entityCountCmds.length > 0 || - this.entityCountUnsubscribeCmds.length > 0; + this.entityCountUnsubscribeCmds.length > 0 || + this.alarmCountCmds.length > 0 || + this.alarmCountUnsubscribeCmds.length > 0; } public clear() { @@ -288,6 +306,8 @@ export class TelemetryPluginCmdsWrapper { this.alarmDataUnsubscribeCmds.length = 0; this.entityCountCmds.length = 0; this.entityCountUnsubscribeCmds.length = 0; + this.alarmCountCmds.length = 0; + this.alarmCountUnsubscribeCmds.length = 0; } public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { @@ -310,6 +330,10 @@ export class TelemetryPluginCmdsWrapper { preparedWrapper.entityCountCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountCmds, leftCount); leftCount -= preparedWrapper.entityCountCmds.length; preparedWrapper.entityCountUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountUnsubscribeCmds, leftCount); + leftCount -= preparedWrapper.entityCountUnsubscribeCmds.length; + preparedWrapper.alarmCountCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmCountCmds, leftCount); + leftCount -= preparedWrapper.alarmCountCmds.length; + preparedWrapper.alarmCountUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmCountUnsubscribeCmds, leftCount); return preparedWrapper; } } @@ -335,7 +359,10 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder { export enum CmdUpdateType { ENTITY_DATA = 'ENTITY_DATA', ALARM_DATA = 'ALARM_DATA', - COUNT_DATA = 'COUNT_DATA' + ALARM_COUNT_DATA = 'ALARM_COUNT_DATA', + COUNT_DATA = 'COUNT_DATA', + NOTIFICATIONS_COUNT = 'NOTIFICATIONS_COUNT', + NOTIFICATIONS = 'NOTIFICATIONS' } export interface CmdUpdateMsg { @@ -365,22 +392,33 @@ export interface EntityCountUpdateMsg extends CmdUpdateMsg { count: number; } -export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | EntityCountUpdateMsg | SubscriptionUpdateMsg; +export interface AlarmCountUpdateMsg extends CmdUpdateMsg { + cmdUpdateType: CmdUpdateType.ALARM_COUNT_DATA; + count: number; +} + +export type WebsocketDataMsg = AlarmDataUpdateMsg | AlarmCountUpdateMsg | + EntityDataUpdateMsg | EntityCountUpdateMsg | SubscriptionUpdateMsg; -export function isEntityDataUpdateMsg(message: WebsocketDataMsg): message is EntityDataUpdateMsg { +export const isEntityDataUpdateMsg = (message: WebsocketDataMsg): message is EntityDataUpdateMsg => { const updateMsg = (message as CmdUpdateMsg); return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ENTITY_DATA; -} +}; -export function isAlarmDataUpdateMsg(message: WebsocketDataMsg): message is AlarmDataUpdateMsg { +export const isAlarmDataUpdateMsg = (message: WebsocketDataMsg): message is AlarmDataUpdateMsg => { const updateMsg = (message as CmdUpdateMsg); return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ALARM_DATA; -} +}; -export function isEntityCountUpdateMsg(message: WebsocketDataMsg): message is EntityCountUpdateMsg { +export const isEntityCountUpdateMsg = (message: WebsocketDataMsg): message is EntityCountUpdateMsg => { const updateMsg = (message as CmdUpdateMsg); return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.COUNT_DATA; -} +}; + +export const isAlarmCountUpdateMsg = (message: WebsocketDataMsg): message is AlarmCountUpdateMsg => { + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ALARM_COUNT_DATA; +}; export class SubscriptionUpdate implements SubscriptionUpdateMsg { subscriptionId: number; @@ -562,31 +600,31 @@ export class EntityCountUpdate extends CmdUpdate { } } -export interface TelemetryService { - subscribe(subscriber: TelemetrySubscriber); - update(subscriber: TelemetrySubscriber); - unsubscribe(subscriber: TelemetrySubscriber); +export class AlarmCountUpdate extends CmdUpdate { + count: number; + + constructor(msg: AlarmCountUpdateMsg) { + super(msg); + this.count = msg.count; + } } -export class TelemetrySubscriber { +export class TelemetrySubscriber extends WsSubscriber { private dataSubject = new ReplaySubject(1); private entityDataSubject = new ReplaySubject(1); private alarmDataSubject = new ReplaySubject(1); private entityCountSubject = new ReplaySubject(1); - private reconnectSubject = new Subject(); - + private alarmCountSubject = new ReplaySubject(1); private tsOffset = undefined; - public subscriptionCommands: Array; - public data$ = this.dataSubject.asObservable(); public entityData$ = this.entityDataSubject.asObservable(); public alarmData$ = this.alarmDataSubject.asObservable(); public entityCount$ = this.entityCountSubject.asObservable(); - public reconnect$ = this.reconnectSubject.asObservable(); + public alarmCount$ = this.alarmCountSubject.asObservable(); - public static createEntityAttributesSubscription(telemetryService: TelemetryService, + public static createEntityAttributesSubscription(telemetryService: TelemetryWebsocketService, entityId: EntityId, attributeScope: TelemetryType, zone: NgZone, keys: string[] = null): TelemetrySubscriber { let subscriptionCommand: SubscriptionCmd; @@ -606,21 +644,29 @@ export class TelemetrySubscriber { return subscriber; } - constructor(private telemetryService: TelemetryService, private zone?: NgZone) { - this.subscriptionCommands = []; - } - - public subscribe() { - this.telemetryService.subscribe(this); - } - - public update() { - this.telemetryService.update(this); + public static createEntityFilterLatestSubscription(telemetryService: TelemetryWebsocketService, + entityFilter: EntityFilter, zone: NgZone, + latestKeys: EntityKey[] = null): TelemetrySubscriber { + const entityDataQuery: EntityDataQuery = { + entityFilter, + pageLink: { + pageSize: 1, + page: 0 + }, + latestValues: latestKeys || [] + }; + const cmd = new EntityDataCmd(); + cmd.query = entityDataQuery; + cmd.latestCmd = { + keys: latestKeys || [] + }; + const subscriber = new TelemetrySubscriber(telemetryService, zone); + subscriber.subscriptionCommands.push(cmd); + return subscriber; } - public unsubscribe() { - this.telemetryService.unsubscribe(this); - this.complete(); + constructor(private telemetryService: TelemetryWebsocketService, protected zone?: NgZone) { + super(telemetryService, zone); } public complete() { @@ -628,7 +674,8 @@ export class TelemetrySubscriber { this.entityDataSubject.complete(); this.alarmDataSubject.complete(); this.entityCountSubject.complete(); - this.reconnectSubject.complete(); + this.alarmCountSubject.complete(); + super.complete(); } public setTsOffset(tsOffset: number): boolean { @@ -705,8 +752,16 @@ export class TelemetrySubscriber { } } - public onReconnected() { - this.reconnectSubject.next(); + public onAlarmCount(message: AlarmCountUpdate) { + if (this.zone) { + this.zone.run( + () => { + this.alarmCountSubject.next(message); + } + ); + } else { + this.alarmCountSubject.next(message); + } } public attributeData$(): Observable> { diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 27b86583ea..9074d599f5 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -43,6 +43,8 @@ export interface DefaultTenantProfileConfiguration { tenantEntityExportRateLimit?: string; tenantEntityImportRateLimit?: string; + tenantNotificationRequestsRateLimit?: string; + tenantNotificationRequestsPerRuleRateLimit?: string; maxTransportMessages: number; maxTransportDataPoints: number; @@ -52,6 +54,7 @@ export interface DefaultTenantProfileConfiguration { maxRuleNodeExecutionsPerMessage: number; maxEmails: number; maxSms: number; + smsEnabled: boolean; maxCreatedAlarms: number; tenantServerRestLimitsConfiguration: string; @@ -103,6 +106,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan maxRuleNodeExecutionsPerMessage: 0, maxEmails: 0, maxSms: 0, + smsEnabled: true, maxCreatedAlarms: 0, tenantServerRestLimitsConfiguration: '', customerServerRestLimitsConfiguration: '', diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index d657d5cda4..fd2b4f52fa 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -43,7 +43,8 @@ export enum RealtimeWindowType { export enum HistoryWindowType { LAST_INTERVAL, FIXED, - INTERVAL + INTERVAL, + FOR_ALL_TIME } export interface IntervalWindow { @@ -140,6 +141,8 @@ export enum QuickTimeInterval { PREVIOUS_WEEK = 'PREVIOUS_WEEK', PREVIOUS_WEEK_ISO = 'PREVIOUS_WEEK_ISO', PREVIOUS_MONTH = 'PREVIOUS_MONTH', + PREVIOUS_QUARTER = 'PREVIOUS_QUARTER', + PREVIOUS_HALF_YEAR = 'PREVIOUS_HALF_YEAR', PREVIOUS_YEAR = 'PREVIOUS_YEAR', CURRENT_HOUR = 'CURRENT_HOUR', CURRENT_DAY = 'CURRENT_DAY', @@ -150,6 +153,10 @@ export enum QuickTimeInterval { CURRENT_WEEK_ISO_SO_FAR = 'CURRENT_WEEK_ISO_SO_FAR', CURRENT_MONTH = 'CURRENT_MONTH', CURRENT_MONTH_SO_FAR = 'CURRENT_MONTH_SO_FAR', + CURRENT_QUARTER = 'CURRENT_QUARTER', + CURRENT_QUARTER_SO_FAR = 'CURRENT_QUARTER_SO_FAR', + CURRENT_HALF_YEAR = 'CURRENT_HALF_YEAR', + CURRENT_HALF_YEAR_SO_FAR = 'CURRENT_HALF_YEAR_SO_FAR', CURRENT_YEAR = 'CURRENT_YEAR', CURRENT_YEAR_SO_FAR = 'CURRENT_YEAR_SO_FAR' } @@ -161,6 +168,8 @@ export const QuickTimeIntervalTranslationMap = new Map ({ + selectedTab: TimewindowType.HISTORY, + history: { + historyType: HistoryWindowType.FOR_ALL_TIME + } +}); -export function defaultTimewindow(timeService: TimeService): Timewindow { +export const historyInterval = (timewindowMs: number): Timewindow => ({ + selectedTab: TimewindowType.HISTORY, + history: { + historyType: HistoryWindowType.LAST_INTERVAL, + timewindowMs + } +}); + +export const defaultTimewindow = (timeService: TimeService): Timewindow => { const currentTime = moment().valueOf(); return { displayValue: '', @@ -217,17 +235,18 @@ export function defaultTimewindow(timeService: TimeService): Timewindow { limit: Math.floor(timeService.getMaxDatapointsLimit() / 2) } }; -} +}; -function getTimewindowType(timewindow: Timewindow): TimewindowType { +const getTimewindowType = (timewindow: Timewindow): TimewindowType => { if (isUndefined(timewindow.selectedTab)) { return isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY; } else { return timewindow.selectedTab; } -} +}; -export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalOnly: boolean, timeService: TimeService): Timewindow { +export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, + historyOnly: boolean, timeService: TimeService): Timewindow => { const model = defaultTimewindow(timeService); if (value) { model.hideInterval = value.hideInterval; @@ -237,7 +256,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalO model.hideAggInterval = value.hideAggInterval; model.hideTimezone = value.hideTimezone; model.selectedTab = getTimewindowType(value); - if (model.selectedTab === TimewindowType.REALTIME) { + if (isDefined(value.realtime)) { if (isDefined(value.realtime.interval)) { model.realtime.interval = value.realtime.interval; } @@ -250,12 +269,14 @@ export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalO } else { model.realtime.realtimeType = value.realtime.realtimeType; } - if (model.realtime.realtimeType === RealtimeWindowType.INTERVAL) { + if (isDefined(value.realtime.quickInterval)) { model.realtime.quickInterval = value.realtime.quickInterval; - } else { + } + if (isDefined(value.realtime.timewindowMs)) { model.realtime.timewindowMs = value.realtime.timewindowMs; } - } else { + } + if (isDefined(value.history)) { if (isDefined(value.history.interval)) { model.history.interval = value.history.interval; } @@ -270,13 +291,19 @@ export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalO } else { model.history.historyType = value.history.historyType; } - if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) { + if (isDefined(value.history.timewindowMs)) { model.history.timewindowMs = value.history.timewindowMs; - } else if (model.history.historyType === HistoryWindowType.INTERVAL) { + } + if (isDefined(value.history.quickInterval)) { model.history.quickInterval = value.history.quickInterval; - } else { - model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; - model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; + } + if (isDefined(value.history.fixedTimewindow)) { + if (isDefined(value.history.fixedTimewindow.startTimeMs)) { + model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; + } + if (isDefined(value.history.fixedTimewindow.endTimeMs)) { + model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; + } } } if (value.aggregation) { @@ -290,11 +317,14 @@ export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalO if (quickIntervalOnly) { model.realtime.realtimeType = RealtimeWindowType.INTERVAL; } + if (historyOnly) { + model.selectedTab = TimewindowType.HISTORY; + } return model; -} +}; -export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, endTimeMs: number, - interval: number, timeService: TimeService): Timewindow { +export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number, + interval: number, timeService: TimeService): Timewindow => { if (timewindow.history) { interval = isDefined(interval) ? interval : timewindow.history.interval; } else if (timewindow.realtime) { @@ -333,18 +363,20 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, }, timezone: timewindow.timezone }; -} +}; -export function timewindowTypeChanged(newTimewindow: Timewindow, oldTimewindow: Timewindow): boolean { +export const timewindowTypeChanged = (newTimewindow: Timewindow, oldTimewindow: Timewindow): boolean => { if (!newTimewindow || !oldTimewindow) { return false; } const newType = getTimewindowType(newTimewindow); const oldType = getTimewindowType(oldTimewindow); return newType !== oldType; -} +}; + +export const getTimezone = (tz: string): moment_.Moment => moment.tz(tz); -export function calculateTsOffset(timezone?: string): number { +export const calculateTsOffset = (timezone?: string): number => { if (timezone) { const tz = getTimezone(timezone); const localOffset = moment().utcOffset(); @@ -352,14 +384,156 @@ export function calculateTsOffset(timezone?: string): number { } else { return 0; } -} +}; -export function isHistoryTypeTimewindow(timewindow: Timewindow): boolean { - return getTimewindowType(timewindow) === TimewindowType.HISTORY; -} +export const isHistoryTypeTimewindow = (timewindow: Timewindow): boolean => getTimewindowType(timewindow) === TimewindowType.HISTORY; + +export const getCurrentTime = (tz?: string): moment_.Moment => { + if (tz) { + return moment().tz(tz); + } else { + return moment(); + } +}; -export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean, - timeService: TimeService): SubscriptionTimewindow { +const getSubscriptionRealtimeWindowFromTimeInterval = (interval: QuickTimeInterval, tz?: string): number => { + let currentDate; + switch (interval) { + case QuickTimeInterval.CURRENT_HOUR: + return HOUR; + case QuickTimeInterval.CURRENT_DAY: + case QuickTimeInterval.CURRENT_DAY_SO_FAR: + return DAY; + case QuickTimeInterval.CURRENT_WEEK: + case QuickTimeInterval.CURRENT_WEEK_ISO: + case QuickTimeInterval.CURRENT_WEEK_SO_FAR: + case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR: + return WEEK; + case QuickTimeInterval.CURRENT_MONTH: + case QuickTimeInterval.CURRENT_MONTH_SO_FAR: + currentDate = getCurrentTime(tz); + return currentDate.endOf('month').diff(currentDate.clone().startOf('month')); + case QuickTimeInterval.CURRENT_QUARTER: + case QuickTimeInterval.CURRENT_QUARTER_SO_FAR: + currentDate = getCurrentTime(tz); + return currentDate.endOf('quarter').diff(currentDate.clone().startOf('quarter')); + case QuickTimeInterval.CURRENT_HALF_YEAR: + case QuickTimeInterval.CURRENT_HALF_YEAR_SO_FAR: + currentDate = getCurrentTime(tz); + if (currentDate.get('quarter') < 3) { + return currentDate.clone().set('quarter', 2).endOf('quarter').diff(currentDate.startOf('year')); + } else { + return currentDate.endOf('year').diff(currentDate.clone().set('quarter', 3).startOf('quarter')); + } + case QuickTimeInterval.CURRENT_YEAR: + case QuickTimeInterval.CURRENT_YEAR_SO_FAR: + currentDate = getCurrentTime(tz); + return currentDate.endOf('year').diff(currentDate.clone().startOf('year')); + } +}; + +export const calculateIntervalStartTime = (interval: QuickTimeInterval, currentDate: moment_.Moment): moment_.Moment => { + switch (interval) { + case QuickTimeInterval.YESTERDAY: + currentDate.subtract(1, 'days'); + return currentDate.startOf('day'); + case QuickTimeInterval.DAY_BEFORE_YESTERDAY: + currentDate.subtract(2, 'days'); + return currentDate.startOf('day'); + case QuickTimeInterval.THIS_DAY_LAST_WEEK: + currentDate.subtract(1, 'weeks'); + return currentDate.startOf('day'); + case QuickTimeInterval.PREVIOUS_WEEK: + currentDate.subtract(1, 'weeks'); + return currentDate.startOf('week'); + case QuickTimeInterval.PREVIOUS_WEEK_ISO: + currentDate.subtract(1, 'weeks'); + return currentDate.startOf('isoWeek'); + case QuickTimeInterval.PREVIOUS_MONTH: + currentDate.subtract(1, 'months'); + return currentDate.startOf('month'); + case QuickTimeInterval.PREVIOUS_YEAR: + currentDate.subtract(1, 'years'); + return currentDate.startOf('year'); + case QuickTimeInterval.CURRENT_HOUR: + return currentDate.startOf('hour'); + case QuickTimeInterval.CURRENT_DAY: + case QuickTimeInterval.CURRENT_DAY_SO_FAR: + return currentDate.startOf('day'); + case QuickTimeInterval.CURRENT_WEEK: + case QuickTimeInterval.CURRENT_WEEK_SO_FAR: + return currentDate.startOf('week'); + case QuickTimeInterval.CURRENT_WEEK_ISO: + case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR: + return currentDate.startOf('isoWeek'); + case QuickTimeInterval.CURRENT_MONTH: + case QuickTimeInterval.CURRENT_MONTH_SO_FAR: + return currentDate.startOf('month'); + case QuickTimeInterval.CURRENT_QUARTER: + case QuickTimeInterval.CURRENT_QUARTER_SO_FAR: + return currentDate.startOf('quarter'); + case QuickTimeInterval.CURRENT_HALF_YEAR: + case QuickTimeInterval.CURRENT_HALF_YEAR_SO_FAR: + if (currentDate.get('quarter') < 3) { + return currentDate.startOf('year'); + } else { + return currentDate.clone().set('quarter', 3).startOf('quarter'); + } + case QuickTimeInterval.CURRENT_YEAR: + case QuickTimeInterval.CURRENT_YEAR_SO_FAR: + return currentDate.startOf('year'); + } +}; + +export const calculateIntervalEndTime = (interval: QuickTimeInterval, startDate: moment_.Moment, tz?: string): number => { + switch (interval) { + case QuickTimeInterval.YESTERDAY: + case QuickTimeInterval.DAY_BEFORE_YESTERDAY: + case QuickTimeInterval.THIS_DAY_LAST_WEEK: + case QuickTimeInterval.CURRENT_DAY: + return startDate.add(1, 'day').valueOf(); + case QuickTimeInterval.PREVIOUS_WEEK: + case QuickTimeInterval.PREVIOUS_WEEK_ISO: + case QuickTimeInterval.CURRENT_WEEK: + case QuickTimeInterval.CURRENT_WEEK_ISO: + return startDate.add(1, 'week').valueOf(); + case QuickTimeInterval.PREVIOUS_MONTH: + case QuickTimeInterval.CURRENT_MONTH: + return startDate.add(1, 'month').valueOf(); + case QuickTimeInterval.PREVIOUS_QUARTER: + case QuickTimeInterval.CURRENT_QUARTER: + return startDate.add(1, 'quarter').valueOf(); + case QuickTimeInterval.PREVIOUS_HALF_YEAR: + case QuickTimeInterval.CURRENT_HALF_YEAR: + return startDate.add(2, 'quarters').valueOf(); + case QuickTimeInterval.PREVIOUS_YEAR: + case QuickTimeInterval.CURRENT_YEAR: + return startDate.add(1, 'year').valueOf(); + case QuickTimeInterval.CURRENT_HOUR: + return startDate.add(1, 'hour').valueOf(); + case QuickTimeInterval.CURRENT_DAY_SO_FAR: + case QuickTimeInterval.CURRENT_WEEK_SO_FAR: + case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR: + case QuickTimeInterval.CURRENT_MONTH_SO_FAR: + case QuickTimeInterval.CURRENT_QUARTER_SO_FAR: + case QuickTimeInterval.CURRENT_HALF_YEAR_SO_FAR: + case QuickTimeInterval.CURRENT_YEAR_SO_FAR: + return getCurrentTime(tz).valueOf(); + } +}; + +export const calculateIntervalStartEndTime = (interval: QuickTimeInterval, tz?: string): [number, number] => { + const startEndTs: [number, number] = [0, 0]; + const currentDate = getCurrentTime(tz); + const startDate = calculateIntervalStartTime(interval, currentDate); + startEndTs[0] = startDate.valueOf(); + const endDate = calculateIntervalEndTime(interval, startDate, tz); + startEndTs[1] = endDate.valueOf(); + return startEndTs; +}; + +export const createSubscriptionTimewindow = (timewindow: Timewindow, stDiff: number, stateData: boolean, + timeService: TimeService): SubscriptionTimewindow => { const subscriptionTimewindow: SubscriptionTimewindow = { fixedWindow: null, realtimeWindowMs: null, @@ -458,115 +632,9 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval); } return subscriptionTimewindow; -} - -function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, tz?: string): number { - let currentDate; - switch (interval) { - case QuickTimeInterval.CURRENT_HOUR: - return HOUR; - case QuickTimeInterval.CURRENT_DAY: - case QuickTimeInterval.CURRENT_DAY_SO_FAR: - return DAY; - case QuickTimeInterval.CURRENT_WEEK: - case QuickTimeInterval.CURRENT_WEEK_ISO: - case QuickTimeInterval.CURRENT_WEEK_SO_FAR: - case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR: - return WEEK; - case QuickTimeInterval.CURRENT_MONTH: - case QuickTimeInterval.CURRENT_MONTH_SO_FAR: - currentDate = getCurrentTime(tz); - return currentDate.endOf('month').diff(currentDate.clone().startOf('month')); - case QuickTimeInterval.CURRENT_YEAR: - case QuickTimeInterval.CURRENT_YEAR_SO_FAR: - currentDate = getCurrentTime(tz); - return currentDate.endOf('year').diff(currentDate.clone().startOf('year')); - } -} - -export function calculateIntervalStartEndTime(interval: QuickTimeInterval, tz?: string): [number, number] { - const startEndTs: [number, number] = [0, 0]; - const currentDate = getCurrentTime(tz); - const startDate = calculateIntervalStartTime(interval, currentDate); - startEndTs[0] = startDate.valueOf(); - const endDate = calculateIntervalEndTime(interval, startDate, tz); - startEndTs[1] = endDate.valueOf(); - return startEndTs; -} - -export function calculateIntervalStartTime(interval: QuickTimeInterval, currentDate: moment_.Moment): moment_.Moment { - switch (interval) { - case QuickTimeInterval.YESTERDAY: - currentDate.subtract(1, 'days'); - return currentDate.startOf('day'); - case QuickTimeInterval.DAY_BEFORE_YESTERDAY: - currentDate.subtract(2, 'days'); - return currentDate.startOf('day'); - case QuickTimeInterval.THIS_DAY_LAST_WEEK: - currentDate.subtract(1, 'weeks'); - return currentDate.startOf('day'); - case QuickTimeInterval.PREVIOUS_WEEK: - currentDate.subtract(1, 'weeks'); - return currentDate.startOf('week'); - case QuickTimeInterval.PREVIOUS_WEEK_ISO: - currentDate.subtract(1, 'weeks'); - return currentDate.startOf('isoWeek'); - case QuickTimeInterval.PREVIOUS_MONTH: - currentDate.subtract(1, 'months'); - return currentDate.startOf('month'); - case QuickTimeInterval.PREVIOUS_YEAR: - currentDate.subtract(1, 'years'); - return currentDate.startOf('year'); - case QuickTimeInterval.CURRENT_HOUR: - return currentDate.startOf('hour'); - case QuickTimeInterval.CURRENT_DAY: - case QuickTimeInterval.CURRENT_DAY_SO_FAR: - return currentDate.startOf('day'); - case QuickTimeInterval.CURRENT_WEEK: - case QuickTimeInterval.CURRENT_WEEK_SO_FAR: - return currentDate.startOf('week'); - case QuickTimeInterval.CURRENT_WEEK_ISO: - case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR: - return currentDate.startOf('isoWeek'); - case QuickTimeInterval.CURRENT_MONTH: - case QuickTimeInterval.CURRENT_MONTH_SO_FAR: - return currentDate.startOf('month'); - case QuickTimeInterval.CURRENT_YEAR: - case QuickTimeInterval.CURRENT_YEAR_SO_FAR: - return currentDate.startOf('year'); - } -} - -export function calculateIntervalEndTime(interval: QuickTimeInterval, startDate: moment_.Moment, tz?: string): number { - switch (interval) { - case QuickTimeInterval.YESTERDAY: - case QuickTimeInterval.DAY_BEFORE_YESTERDAY: - case QuickTimeInterval.THIS_DAY_LAST_WEEK: - case QuickTimeInterval.CURRENT_DAY: - return startDate.add(1, 'day').valueOf(); - case QuickTimeInterval.PREVIOUS_WEEK: - case QuickTimeInterval.PREVIOUS_WEEK_ISO: - case QuickTimeInterval.CURRENT_WEEK: - case QuickTimeInterval.CURRENT_WEEK_ISO: - return startDate.add(1, 'week').valueOf(); - case QuickTimeInterval.PREVIOUS_MONTH: - case QuickTimeInterval.CURRENT_MONTH: - return startDate.add(1, 'month').valueOf(); - case QuickTimeInterval.PREVIOUS_YEAR: - case QuickTimeInterval.CURRENT_YEAR: - return startDate.add(1, 'year').valueOf(); - case QuickTimeInterval.CURRENT_HOUR: - return startDate.add(1, 'hour').valueOf(); - case QuickTimeInterval.CURRENT_DAY_SO_FAR: - case QuickTimeInterval.CURRENT_WEEK_SO_FAR: - case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR: - case QuickTimeInterval.CURRENT_MONTH_SO_FAR: - case QuickTimeInterval.CURRENT_YEAR_SO_FAR: - return getCurrentTime(tz).valueOf(); - } -} +}; -export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number { +export const quickTimeIntervalPeriod = (interval: QuickTimeInterval): number => { switch (interval) { case QuickTimeInterval.CURRENT_HOUR: return HOUR; @@ -587,15 +655,23 @@ export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number { case QuickTimeInterval.CURRENT_MONTH: case QuickTimeInterval.CURRENT_MONTH_SO_FAR: return DAY * 30; + case QuickTimeInterval.PREVIOUS_QUARTER: + case QuickTimeInterval.CURRENT_QUARTER: + case QuickTimeInterval.CURRENT_QUARTER_SO_FAR: + return DAY * 30 * 3; + case QuickTimeInterval.PREVIOUS_HALF_YEAR: + case QuickTimeInterval.CURRENT_HALF_YEAR: + case QuickTimeInterval.CURRENT_HALF_YEAR_SO_FAR: + return DAY * 30 * 6; case QuickTimeInterval.PREVIOUS_YEAR: case QuickTimeInterval.CURRENT_YEAR: case QuickTimeInterval.CURRENT_YEAR_SO_FAR: return YEAR; } -} +}; -export function calculateIntervalComparisonStartTime(interval: QuickTimeInterval, - startDate: moment_.Moment): moment_.Moment { +export const calculateIntervalComparisonStartTime = (interval: QuickTimeInterval, + startDate: moment_.Moment): moment_.Moment => { switch (interval) { case QuickTimeInterval.YESTERDAY: case QuickTimeInterval.DAY_BEFORE_YESTERDAY: @@ -621,6 +697,20 @@ export function calculateIntervalComparisonStartTime(interval: QuickTimeInterval case QuickTimeInterval.CURRENT_MONTH_SO_FAR: startDate.subtract(1, 'months'); return startDate.startOf('month'); + case QuickTimeInterval.PREVIOUS_QUARTER: + case QuickTimeInterval.CURRENT_QUARTER: + case QuickTimeInterval.CURRENT_QUARTER_SO_FAR: + startDate.subtract(1, 'quarters'); + return startDate.startOf('quarter'); + case QuickTimeInterval.PREVIOUS_HALF_YEAR: + case QuickTimeInterval.CURRENT_HALF_YEAR: + case QuickTimeInterval.CURRENT_HALF_YEAR_SO_FAR: + startDate.subtract(2, 'quarters'); + if (startDate.get('quarter') < 3) { + return startDate.startOf('year'); + } else { + return startDate.clone().set('quarter', 3).startOf('quarter'); + } case QuickTimeInterval.PREVIOUS_YEAR: case QuickTimeInterval.CURRENT_YEAR: case QuickTimeInterval.CURRENT_YEAR_SO_FAR: @@ -630,11 +720,11 @@ export function calculateIntervalComparisonStartTime(interval: QuickTimeInterval startDate.subtract(1, 'hour'); return startDate.startOf('hour'); } -} +}; -export function calculateIntervalComparisonEndTime(interval: QuickTimeInterval, +export const calculateIntervalComparisonEndTime = (interval: QuickTimeInterval, comparisonStartDate: moment_.Moment, - endDate: moment_.Moment): number { + endDate: moment_.Moment): number => { switch (interval) { case QuickTimeInterval.CURRENT_DAY_SO_FAR: return endDate.subtract(1, 'days').valueOf(); @@ -643,15 +733,19 @@ export function calculateIntervalComparisonEndTime(interval: QuickTimeInterval, return endDate.subtract(1, 'week').valueOf(); case QuickTimeInterval.CURRENT_MONTH_SO_FAR: return endDate.subtract(1, 'month').valueOf(); + case QuickTimeInterval.CURRENT_QUARTER_SO_FAR: + return endDate.subtract(1, 'quarter').valueOf(); + case QuickTimeInterval.CURRENT_HALF_YEAR_SO_FAR: + return endDate.subtract(2, 'quarters').valueOf(); case QuickTimeInterval.CURRENT_YEAR_SO_FAR: return endDate.subtract(1, 'year').valueOf(); default: return calculateIntervalEndTime(interval, comparisonStartDate); } -} +}; -export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow, - timeUnit: ComparisonDuration, customIntervalValue: number): SubscriptionTimewindow { +export const createTimewindowForComparison = (subscriptionTimewindow: SubscriptionTimewindow, + timeUnit: ComparisonDuration, customIntervalValue: number): SubscriptionTimewindow => { const timewindowForComparison: SubscriptionTimewindow = { fixedWindow: null, realtimeWindowMs: null, @@ -700,9 +794,9 @@ export function createTimewindowForComparison(subscriptionTimewindow: Subscripti } return timewindowForComparison; -} +}; -export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { +export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => { const cloned: Timewindow = {}; cloned.hideInterval = timewindow.hideInterval || false; cloned.hideLastInterval = timewindow.hideLastInterval || false; @@ -721,7 +815,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { cloned.aggregation = deepClone(timewindow.aggregation); cloned.timezone = timewindow.timezone; return cloned; -} +}; export interface TimeInterval { name: string; @@ -861,7 +955,7 @@ export interface TimezoneInfo { let timezones: TimezoneInfo[] = null; let defaultTimezone: string = null; -export function getTimezones(): TimezoneInfo[] { +export const getTimezones = (): TimezoneInfo[] => { if (!timezones) { timezones = momentTz.tz.names().map((zoneName) => { const tz = momentTz.tz(zoneName); @@ -875,9 +969,15 @@ export function getTimezones(): TimezoneInfo[] { }); } return timezones; -} +}; -export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): TimezoneInfo { +export const getDefaultTimezone = (): string => { + if (!defaultTimezone) { + defaultTimezone = momentTz.tz.guess(); + } + return defaultTimezone; +}; +export const getTimezoneInfo = (timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): TimezoneInfo => { const timezoneList = getTimezones(); let foundTimezone = timezoneId ? timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId) : null; if (!foundTimezone) { @@ -889,40 +989,20 @@ export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, } } return foundTimezone; -} +}; -export function getDefaultTimezoneInfo(): TimezoneInfo { +export const getDefaultTimezoneInfo = (): TimezoneInfo => { const userTimezone = getDefaultTimezone(); return getTimezoneInfo(userTimezone); -} +}; -export function getDefaultTimezone(): string { - if (!defaultTimezone) { - defaultTimezone = momentTz.tz.guess(); - } - return defaultTimezone; -} - -export function getCurrentTime(tz?: string): moment_.Moment { - if (tz) { - return moment().tz(tz); - } else { - return moment(); - } -} - -export function getTime(ts: number, tz?: string): moment_.Moment { +export const getTime = (ts: number, tz?: string): moment_.Moment => { if (tz) { return moment(ts).tz(tz); } else { return moment(ts); } -} - -export function getTimezone(tz: string): moment_.Moment { - return moment.tz(tz); -} +}; -export function getCurrentTimeForComparison(timeForComparison: moment_.unitOfTime.DurationConstructor, tz?: string): moment_.Moment { - return getCurrentTime(tz).subtract(1, timeForComparison); -} +export const getCurrentTimeForComparison = (timeForComparison: moment_.unitOfTime.DurationConstructor, tz?: string): moment_.Moment => + getCurrentTime(tz).subtract(1, timeForComparison); diff --git a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts index d3b2a74fbd..3969845f83 100644 --- a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/usage.models.ts b/ui-ngx/src/app/shared/models/usage.models.ts new file mode 100644 index 0000000000..7455aadb88 --- /dev/null +++ b/ui-ngx/src/app/shared/models/usage.models.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export interface UsageInfo { + devices: number; + maxDevices: number; + assets: number; + maxAssets: number; + customers: number; + maxCustomers: number; + users: number; + maxUsers: number; + dashboards: number; + maxDashboards: number; + + transportMessages: number; + maxTransportMessages: number; + jsExecutions: number; + maxJsExecutions: number; + emails: number; + maxEmails: number; + sms: number; + maxSms: number; + alarms: number; + maxAlarms: number; +} diff --git a/ui-ngx/src/app/shared/models/user-settings.models.ts b/ui-ngx/src/app/shared/models/user-settings.models.ts new file mode 100644 index 0000000000..e39600f950 --- /dev/null +++ b/ui-ngx/src/app/shared/models/user-settings.models.ts @@ -0,0 +1,75 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export interface UserSettings { + openedMenuSections?: string[]; +} + +export const initialUserSettings: UserSettings = { + openedMenuSections: [] +}; + +export enum UserSettingsType { + GENERAL = 'GENERAL', + QUICK_LINKS = 'QUICK_LINKS', + DOC_LINKS = 'DOC_LINKS', + DASHBOARDS = 'DASHBOARDS', + GETTING_STARTED = 'GETTING_STARTED' +} + +export interface DocumentationLink { + icon: string; + name: string; + link: string; +} + +export interface DocumentationLinks { + links?: DocumentationLink[]; +} + +export interface QuickLinks { + links?: string[]; +} + +export interface GettingStarted { + maxSelectedIndex?: number; + lastSelectedIndex?: number; +} + +export interface AbstractUserDashboardInfo { + id: string; + title: string; + starred: boolean; +} + +export interface LastVisitedDashboardInfo extends AbstractUserDashboardInfo { + lastVisited: number; +} + +export interface StarredDashboardInfo extends AbstractUserDashboardInfo { + starredAt: number; +} + +export interface UserDashboardsInfo { + last: Array; + starred: Array; +} + +export enum UserDashboardAction { + VISIT = 'VISIT', + STAR = 'STAR', + UNSTAR = 'UNSTAR' +} diff --git a/ui-ngx/src/app/shared/models/user.model.ts b/ui-ngx/src/app/shared/models/user.model.ts index 42e6407c12..02a8c657f9 100644 --- a/ui-ngx/src/app/shared/models/user.model.ts +++ b/ui-ngx/src/app/shared/models/user.model.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ export interface User extends BaseData { tenantId: TenantId; customerId: CustomerId; email: string; + phone: string; authority: Authority; firstName: string; lastName: string; @@ -54,3 +55,10 @@ export interface AuthUser { isPublic: boolean; authority: Authority; } + +export interface UserEmailInfo { + id: UserId; + email: string; + firstName: string; + lastName: string; +} diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index d92dd26f17..d1886fb1aa 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -30,9 +30,18 @@ export const exportableEntityTypes: Array = [ EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.RULE_CHAIN, - EntityType.WIDGETS_BUNDLE + EntityType.WIDGETS_BUNDLE, + EntityType.NOTIFICATION_TEMPLATE, + EntityType.NOTIFICATION_TARGET, + EntityType.NOTIFICATION_RULE ]; +export const entityTypesWithoutRelatedData: Set = new Set([ + EntityType.NOTIFICATION_TEMPLATE, + EntityType.NOTIFICATION_TARGET, + EntityType.NOTIFICATION_RULE +]); + export interface VersionCreateConfig { saveRelations: boolean; saveAttributes: boolean; diff --git a/ui-ngx/src/app/shared/models/websocket/notification-ws.models.ts b/ui-ngx/src/app/shared/models/websocket/notification-ws.models.ts new file mode 100644 index 0000000000..0acbba1139 --- /dev/null +++ b/ui-ngx/src/app/shared/models/websocket/notification-ws.models.ts @@ -0,0 +1,240 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BehaviorSubject, ReplaySubject } from 'rxjs'; +import { CmdUpdate, CmdUpdateMsg, CmdUpdateType, WebsocketCmd } from '@shared/models/telemetry/telemetry.models'; +import { map } from 'rxjs/operators'; +import { NgZone } from '@angular/core'; +import { isDefinedAndNotNull } from '@core/utils'; +import { Notification } from '@shared/models/notification.models'; +import { CmdWrapper, WsSubscriber } from '@shared/models/websocket/websocket.models'; +import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; + +export class NotificationCountUpdate extends CmdUpdate { + totalUnreadCount: number; + + constructor(msg: NotificationCountUpdateMsg) { + super(msg); + this.totalUnreadCount = msg.totalUnreadCount; + } +} + +export class NotificationsUpdate extends CmdUpdate { + totalUnreadCount: number; + update?: Notification; + notifications?: Notification[]; + + constructor(msg: NotificationsUpdateMsg) { + super(msg); + this.totalUnreadCount = msg.totalUnreadCount; + this.update = msg.update; + this.notifications = msg.notifications; + } +} + +export class NotificationSubscriber extends WsSubscriber { + private notificationCountSubject = new ReplaySubject(1); + private notificationsSubject = new BehaviorSubject({ + cmdId: 0, + cmdUpdateType: undefined, + errorCode: 0, + errorMsg: '', + notifications: [], + totalUnreadCount: 0 + }); + + public messageLimit = 10; + + public notificationCount$ = this.notificationCountSubject.asObservable().pipe(map(msg => msg.totalUnreadCount)); + public notifications$ = this.notificationsSubject.asObservable().pipe(map(msg => msg.notifications )); + + public static createNotificationCountSubscription(notificationWsService: NotificationWebsocketService, + zone: NgZone): NotificationSubscriber { + const subscriptionCommand = new UnreadCountSubCmd(); + const subscriber = new NotificationSubscriber(notificationWsService, zone); + subscriber.subscriptionCommands.push(subscriptionCommand); + return subscriber; + } + + public static createNotificationsSubscription(notificationWsService: NotificationWebsocketService, + zone: NgZone, limit = 10): NotificationSubscriber { + const subscriptionCommand = new UnreadSubCmd(limit); + const subscriber = new NotificationSubscriber(notificationWsService, zone); + subscriber.messageLimit = limit; + subscriber.subscriptionCommands.push(subscriptionCommand); + return subscriber; + } + + public static createMarkAsReadCommand(notificationWsService: NotificationWebsocketService, + ids: string[]): NotificationSubscriber { + const subscriptionCommand = new MarkAsReadCmd(ids); + const subscriber = new NotificationSubscriber(notificationWsService); + subscriber.subscriptionCommands.push(subscriptionCommand); + return subscriber; + } + + public static createMarkAllAsReadCommand(notificationWsService: NotificationWebsocketService): NotificationSubscriber { + const subscriptionCommand = new MarkAllAsReadCmd(); + const subscriber = new NotificationSubscriber(notificationWsService); + subscriber.subscriptionCommands.push(subscriptionCommand); + return subscriber; + } + + constructor(private notificationWsService: NotificationWebsocketService, protected zone?: NgZone) { + super(notificationWsService, zone); + } + + onNotificationCountUpdate(message: NotificationCountUpdate) { + if (this.zone) { + this.zone.run( + () => { + this.notificationCountSubject.next(message); + } + ); + } else { + this.notificationCountSubject.next(message); + } + } + + public complete() { + this.notificationCountSubject.complete(); + this.notificationsSubject.complete(); + super.complete(); + } + + onNotificationsUpdate(message: NotificationsUpdate) { + const currentNotifications = this.notificationsSubject.value; + let processMessage = message; + if (isDefinedAndNotNull(currentNotifications) && message.update) { + currentNotifications.notifications.unshift(message.update); + if (currentNotifications.notifications.length > this.messageLimit) { + currentNotifications.notifications.pop(); + } + processMessage = currentNotifications; + processMessage.totalUnreadCount = message.totalUnreadCount; + } + if (this.zone) { + this.zone.run( + () => { + this.notificationsSubject.next(processMessage); + this.notificationCountSubject.next(processMessage); + } + ); + } else { + this.notificationsSubject.next(processMessage); + this.notificationCountSubject.next(processMessage); + } + } +} + +export class UnreadCountSubCmd implements WebsocketCmd { + cmdId: number; +} + +export class UnreadSubCmd implements WebsocketCmd { + limit: number; + cmdId: number; + + constructor(limit = 10) { + this.limit = limit; + } +} + +export class UnsubscribeCmd implements WebsocketCmd { + cmdId: number; +} + +export class MarkAsReadCmd implements WebsocketCmd { + + cmdId: number; + notifications: string[]; + + constructor(ids: string[]) { + this.notifications = ids; + } +} + +export class MarkAllAsReadCmd implements WebsocketCmd { + cmdId: number; +} + +export interface NotificationCountUpdateMsg extends CmdUpdateMsg { + cmdUpdateType: CmdUpdateType.NOTIFICATIONS_COUNT; + totalUnreadCount: number; +} + +export interface NotificationsUpdateMsg extends CmdUpdateMsg { + cmdUpdateType: CmdUpdateType.NOTIFICATIONS; + totalUnreadCount: number; + update?: Notification; + notifications?: Notification[]; +} + +export type WebsocketNotificationMsg = NotificationCountUpdateMsg | NotificationsUpdateMsg; + +export const isNotificationCountUpdateMsg = (message: WebsocketNotificationMsg): message is NotificationCountUpdateMsg => { + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.NOTIFICATIONS_COUNT; +}; + +export const isNotificationsUpdateMsg = (message: WebsocketNotificationMsg): message is NotificationsUpdateMsg => { + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.NOTIFICATIONS; +}; + +export class NotificationPluginCmdWrapper implements CmdWrapper { + + constructor() { + this.unreadCountSubCmd = null; + this.unreadSubCmd = null; + this.unsubCmd = null; + this.markAsReadCmd = null; + this.markAllAsReadCmd = null; + } + + unreadCountSubCmd: UnreadCountSubCmd; + unreadSubCmd: UnreadSubCmd; + unsubCmd: UnsubscribeCmd; + markAsReadCmd: MarkAsReadCmd; + markAllAsReadCmd: MarkAllAsReadCmd; + + public hasCommands(): boolean { + return isDefinedAndNotNull(this.unreadCountSubCmd) || + isDefinedAndNotNull(this.unreadSubCmd) || + isDefinedAndNotNull(this.unsubCmd) || + isDefinedAndNotNull(this.markAsReadCmd) || + isDefinedAndNotNull(this.markAllAsReadCmd); + } + + public clear() { + this.unreadCountSubCmd = null; + this.unreadSubCmd = null; + this.unsubCmd = null; + this.markAsReadCmd = null; + this.markAllAsReadCmd = null; + } + + public preparePublishCommands(): NotificationPluginCmdWrapper { + const preparedWrapper = new NotificationPluginCmdWrapper(); + preparedWrapper.unreadCountSubCmd = this.unreadCountSubCmd || undefined; + preparedWrapper.unreadSubCmd = this.unreadSubCmd || undefined; + preparedWrapper.unsubCmd = this.unsubCmd || undefined; + preparedWrapper.markAsReadCmd = this.markAsReadCmd || undefined; + preparedWrapper.markAllAsReadCmd = this.markAllAsReadCmd || undefined; + this.clear(); + return preparedWrapper; + } +} diff --git a/ui-ngx/src/app/shared/models/websocket/websocket.models.ts b/ui-ngx/src/app/shared/models/websocket/websocket.models.ts new file mode 100644 index 0000000000..fc7fc3d15c --- /dev/null +++ b/ui-ngx/src/app/shared/models/websocket/websocket.models.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgZone } from '@angular/core'; +import { WebsocketCmd } from '@shared/models/telemetry/telemetry.models'; +import { Subject } from 'rxjs'; + +export interface WsService { + subscribe(subscriber: T); + update(subscriber: T); + unsubscribe(subscriber: T); +} + +export abstract class CmdWrapper { + abstract hasCommands(): boolean; + abstract clear(): void; + abstract preparePublishCommands(maxCommands: number): CmdWrapper; + + [key: string]: WebsocketCmd | any; +} + +export abstract class WsSubscriber { + + protected reconnectSubject = new Subject(); + + subscriptionCommands: Array; + + reconnect$ = this.reconnectSubject.asObservable(); + + protected constructor(protected wsService: WsService, protected zone?: NgZone) { + this.subscriptionCommands = []; + } + + public subscribe() { + this.wsService.subscribe(this); + } + + public update() { + this.wsService.update(this); + } + + public unsubscribe() { + this.wsService.unsubscribe(this); + this.complete(); + } + + public complete() { + this.reconnectSubject.complete(); + } + + public onReconnected() { + this.reconnectSubject.next(); + } +} diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 72d11626cb..bfe9021926 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -23,13 +23,19 @@ import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models'; import { DataKeyType } from './telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; import * as moment_ from 'moment'; -import { EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models'; +import { + AlarmFilter, + AlarmFilterConfig, + EntityDataPageLink, + EntityFilter, + KeyFilter +} from '@shared/models/query/query.models'; import { PopoverPlacement } from '@shared/components/popover.models'; import { PageComponent } from '@shared/components/page.component'; -import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; +import { AfterViewInit, Directive, EventEmitter, Inject, OnInit, Type } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { AbstractControl, FormGroup } from '@angular/forms'; +import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; @@ -156,6 +162,8 @@ export interface WidgetTypeDescriptor { settingsDirective?: string; dataKeySettingsDirective?: string; latestDataKeySettingsDirective?: string; + hasBasicMode?: boolean; + basicModeDirective?: string; defaultConfig: string; sizeX: number; sizeY: number; @@ -246,18 +254,16 @@ export interface LegendConfig { showLatest: boolean; } -export function defaultLegendConfig(wType: widgetType): LegendConfig { - return { - direction: LegendDirection.column, - position: LegendPosition.bottom, - sortDataKeys: false, - showMin: false, - showMax: false, - showAvg: wType === widgetType.timeseries, - showTotal: false, - showLatest: false - }; -} +export const defaultLegendConfig = (wType: widgetType): LegendConfig => ({ + direction: LegendDirection.column, + position: LegendPosition.bottom, + sortDataKeys: false, + showMin: false, + showMax: false, + showAvg: wType === widgetType.timeseries, + showTotal: false, + showLatest: false +}); export enum ComparisonResultType { PREVIOUS_VALUE = 'PREVIOUS_VALUE', @@ -314,15 +320,19 @@ export interface DataKey extends KeyInfo { export enum DatasourceType { function = 'function', + device = 'device', entity = 'entity', - entityCount = 'entityCount' + entityCount = 'entityCount', + alarmCount = 'alarmCount' } export const datasourceTypeTranslationMap = new Map( [ [ DatasourceType.function, 'function.function' ], + [ DatasourceType.device, 'device.device' ], [ DatasourceType.entity, 'entity.entity' ], - [ DatasourceType.entityCount, 'entity.entities-count' ] + [ DatasourceType.entityCount, 'entity.entities-count' ], + [ DatasourceType.alarmCount, 'entity.alarms-count' ] ] ); @@ -335,6 +345,7 @@ export interface Datasource { entityType?: EntityType; entityId?: string; entityName?: string; + deviceId?: string; entityAliasId?: string; filterId?: string; unresolvedStateEntity?: boolean; @@ -348,12 +359,14 @@ export interface Datasource { pageLink?: EntityDataPageLink; keyFilters?: Array; entityFilter?: EntityFilter; + alarmFilterConfig?: AlarmFilterConfig; + alarmFilter?: AlarmFilter; dataKeyStartIndex?: number; latestDataKeyStartIndex?: number; [key: string]: any; } -export function datasourcesHasAggregation(datasources?: Array): boolean { +export const datasourcesHasAggregation = (datasources?: Array): boolean => { if (datasources) { const foundDatasource = datasources.find(datasource => { const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries && @@ -365,9 +378,9 @@ export function datasourcesHasAggregation(datasources?: Array): bool } } return false; -} +}; -export function datasourcesHasOnlyComparisonAggregation(datasources?: Array): boolean { +export const datasourcesHasOnlyComparisonAggregation = (datasources?: Array): boolean => { if (!datasourcesHasAggregation(datasources)) { return false; } @@ -382,7 +395,7 @@ export function datasourcesHasOnlyComparisonAggregation(datasources?: Array; customHtml?: string; customCss?: string; + customModules?: Type[]; } export interface WidgetActionDescriptor extends CustomActionDescriptor { @@ -591,7 +605,13 @@ export interface WidgetSettings { [key: string]: any; } +export enum WidgetConfigMode { + basic = 'basic', + advanced = 'advanced' +} + export interface WidgetConfig { + configMode?: WidgetConfigMode; title?: string; titleIcon?: string; showTitle?: boolean; @@ -603,8 +623,6 @@ export interface WidgetConfig { enableFullscreen?: boolean; useDashboardTimewindow?: boolean; displayTimewindow?: boolean; - showLegend?: boolean; - legendConfig?: LegendConfig; timewindow?: Timewindow; desktopHide?: boolean; mobileHide?: boolean; @@ -624,10 +642,7 @@ export interface WidgetConfig { actions?: {[actionSourceId: string]: Array}; settings?: WidgetSettings; alarmSource?: Datasource; - alarmStatusList?: AlarmSearchStatus[]; - alarmSeverityList?: AlarmSeverity[]; - alarmTypeList?: string[]; - searchPropagatedAlarms?: boolean; + alarmFilterConfig?: AlarmFilterConfig; datasources?: Array; targetDeviceAliasIds?: Array; [key: string]: any; @@ -692,7 +707,7 @@ export interface IWidgetSettingsComponent { [key: string]: any; } -function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings { +const removeEmptyWidgetSettings = (settings: WidgetSettings): WidgetSettings => { if (settings) { const keys = Object.keys(settings); for (const key of keys) { @@ -703,10 +718,10 @@ function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings { } } return settings; -} +}; @Directive() -// tslint:disable-next-line:directive-class-suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix export abstract class WidgetSettingsComponent extends PageComponent implements IWidgetSettingsComponent, OnInit, AfterViewInit { @@ -802,7 +817,7 @@ export abstract class WidgetSettingsComponent extends PageComponent implements } } - protected doUpdateSettings(settingsForm: FormGroup, settings: WidgetSettings) { + protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) { } protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { @@ -819,7 +834,7 @@ export abstract class WidgetSettingsComponent extends PageComponent implements protected onValidate() {} - protected abstract settingsForm(): FormGroup; + protected abstract settingsForm(): UntypedFormGroup; protected abstract onSettingsSet(settings: WidgetSettings); diff --git a/ui-ngx/src/app/shared/models/widgets-bundle.model.ts b/ui-ngx/src/app/shared/models/widgets-bundle.model.ts index 235190ead2..c7eb999221 100644 --- a/ui-ngx/src/app/shared/models/widgets-bundle.model.ts +++ b/ui-ngx/src/app/shared/models/widgets-bundle.model.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/models/window-message.model.ts b/ui-ngx/src/app/shared/models/window-message.model.ts index dff6898958..f720f2692f 100644 --- a/ui-ngx/src/app/shared/models/window-message.model.ts +++ b/ui-ngx/src/app/shared/models/window-message.model.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/pipe/date-ago.pipe.ts b/ui-ngx/src/app/shared/pipe/date-ago.pipe.ts new file mode 100644 index 0000000000..4fb964eae8 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/date-ago.pipe.ts @@ -0,0 +1,63 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Inject, Pipe, PipeTransform } from '@angular/core'; +import { DAY, HOUR, MINUTE, SECOND, WEEK, YEAR } from '@shared/models/time/time.models'; +import { TranslateService } from '@ngx-translate/core'; + +const intervals = { + years: YEAR, + months: DAY * 30, + weeks: WEEK, + days: DAY, + hr: HOUR, + min: MINUTE, + sec: SECOND +}; + +@Pipe({ + name: 'dateAgo' +}) +export class DateAgoPipe implements PipeTransform { + + constructor(@Inject(TranslateService) private translate: TranslateService) { + + } + + transform(value: number, args?: any): string { + if (value) { + const applyAgo = !!args?.applyAgo; + const ms = Math.floor((+new Date() - +new Date(value))); + if (ms < 29 * SECOND) { // less than 30 seconds ago will show as 'Just now' + return this.translate.instant('timewindow.just-now'); + } + let counter; + // eslint-disable-next-line guard-for-in + for (const i in intervals) { + counter = Math.floor(ms / intervals[i]); + if (counter > 0) { + let res = this.translate.instant(`timewindow.${i}`, {[i]: counter}); + if (applyAgo) { + res += ' ' + this.translate.instant('timewindow.ago'); + } + return res; + } + } + } + return ''; + } + +} diff --git a/ui-ngx/src/app/shared/pipe/enum-to-array.pipe.ts b/ui-ngx/src/app/shared/pipe/enum-to-array.pipe.ts index e879727dd5..befd9a7b56 100644 --- a/ui-ngx/src/app/shared/pipe/enum-to-array.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/enum-to-array.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/pipe/file-size.pipe.ts b/ui-ngx/src/app/shared/pipe/file-size.pipe.ts index bfab9ee5c3..dc67cd216d 100644 --- a/ui-ngx/src/app/shared/pipe/file-size.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/file-size.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/pipe/highlight.pipe.ts b/ui-ngx/src/app/shared/pipe/highlight.pipe.ts index 5a676cc96b..0f8595fc3b 100644 --- a/ui-ngx/src/app/shared/pipe/highlight.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/highlight.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts b/ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts index b11c518d40..12104356d0 100644 --- a/ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts index 6722f2f72e..df185fe8d0 100644 --- a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -25,33 +25,51 @@ export class MillisecondsToTimeStringPipe implements PipeTransform { constructor(private translate: TranslateService) { } - transform(millseconds: number, args?: any): string { + transform(millseconds: number, shortFormat = false): string { let seconds = Math.floor(millseconds / 1000); const days = Math.floor(seconds / 86400); let hours = Math.floor((seconds % 86400) / 3600); let minutes = Math.floor(((seconds % 86400) % 3600) / 60); seconds = seconds % 60; let timeString = ''; - if (days > 0) { - timeString += this.translate.instant('timewindow.days', {days}); - } - if (hours > 0) { - if (timeString.length === 0 && hours === 1) { - hours = 0; + if (shortFormat) { + if (days > 0) { + timeString += this.translate.instant('timewindow.short.days', {days}); } - timeString += this.translate.instant('timewindow.hours', {hours}); - } - if (minutes > 0) { - if (timeString.length === 0 && minutes === 1) { - minutes = 0; + if (hours > 0) { + timeString += this.translate.instant('timewindow.short.hours', {hours}); } - timeString += this.translate.instant('timewindow.minutes', {minutes}); - } - if (seconds > 0) { - if (timeString.length === 0 && seconds === 1) { - seconds = 0; + if (minutes > 0) { + timeString += this.translate.instant('timewindow.short.minutes', {minutes}); + } + if (seconds > 0) { + timeString += this.translate.instant('timewindow.short.seconds', {seconds}); + } + if (!timeString.length) { + timeString += this.translate.instant('timewindow.short.seconds', {seconds: 0}); + } + } else { + if (days > 0) { + timeString += this.translate.instant('timewindow.days', {days}); + } + if (hours > 0) { + if (timeString.length === 0 && hours === 1) { + hours = 0; + } + timeString += this.translate.instant('timewindow.hours', {hours}); + } + if (minutes > 0) { + if (timeString.length === 0 && minutes === 1) { + minutes = 0; + } + timeString += this.translate.instant('timewindow.minutes', {minutes}); + } + if (seconds > 0) { + if (timeString.length === 0 && seconds === 1) { + seconds = 0; + } + timeString += this.translate.instant('timewindow.seconds', {seconds}); } - timeString += this.translate.instant('timewindow.seconds', {seconds}); } return timeString; } diff --git a/ui-ngx/src/app/shared/pipe/nospace.pipe.ts b/ui-ngx/src/app/shared/pipe/nospace.pipe.ts index d702464d3b..155f648ae0 100644 --- a/ui-ngx/src/app/shared/pipe/nospace.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/nospace.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/pipe/public-api.ts b/ui-ngx/src/app/shared/pipe/public-api.ts index dc810aaaf1..9ba3be3610 100644 --- a/ui-ngx/src/app/shared/pipe/public-api.ts +++ b/ui-ngx/src/app/shared/pipe/public-api.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ /// limitations under the License. /// +export * from './date-ago.pipe'; export * from './enum-to-array.pipe'; export * from './highlight.pipe'; export * from './keyboard-shortcut.pipe'; diff --git a/ui-ngx/src/app/shared/pipe/safe.pipe.ts b/ui-ngx/src/app/shared/pipe/safe.pipe.ts index 67243802f5..22cf9e4a50 100644 --- a/ui-ngx/src/app/shared/pipe/safe.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/safe.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/pipe/selectable-columns.pipe.ts b/ui-ngx/src/app/shared/pipe/selectable-columns.pipe.ts index 9ee424bd68..e71ba26787 100644 --- a/ui-ngx/src/app/shared/pipe/selectable-columns.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/selectable-columns.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/pipe/short-number.pipe.ts b/ui-ngx/src/app/shared/pipe/short-number.pipe.ts new file mode 100644 index 0000000000..d89bc21b4a --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/short-number.pipe.ts @@ -0,0 +1,53 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'shortNumber' +}) +export class ShortNumberPipe implements PipeTransform { + + transform(number: number, args?: any): any { + if (isNaN(number)) return 0; + if (number === null) return 0; + if (number === 0) return 0; + let abs = Math.abs(number); + const rounder = Math.pow(10, 1); + const isNegative = number < 0; + const isLong = args && args.long; + let key = ''; + + const powers = [ + {key: 'Q', longKey: ' quadrillion', value: Math.pow(10, 15)}, + {key: 'T', longKey: ' trillion', value: Math.pow(10, 12)}, + {key: 'B', longKey: ' billion', value: Math.pow(10, 9)}, + {key: 'M', longKey: ' million', value: Math.pow(10, 6)}, + {key: 'K', longKey: ' thousand', value: 1000} + ]; + + for (let i = 0; i < powers.length; i++) { + let reduced = abs / powers[i].value; + reduced = Math.round(reduced * rounder) / rounder; + if (reduced >= 1) { + abs = reduced; + key = isLong ? powers[i].longKey : powers[i].key; + break; + } + } + return (isNegative ? '-' : '') + abs + key; + } +} diff --git a/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts b/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts index 41385c2f1a..edf55e1f0c 100644 --- a/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/pipe/truncate.pipe.ts b/ui-ngx/src/app/shared/pipe/truncate.pipe.ts index 5e7c8d6f69..db2d0ccb09 100644 --- a/ui-ngx/src/app/shared/pipe/truncate.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/truncate.pipe.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/public-api.ts b/ui-ngx/src/app/shared/public-api.ts index e289cc9339..0539468030 100644 --- a/ui-ngx/src/app/shared/public-api.ts +++ b/ui-ngx/src/app/shared/public-api.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ /// export * from './components/public-api'; +export * from './decorators/public-api'; export * from './models/public-api'; export * from './pipe/public-api'; export * from './shared.module'; diff --git a/ui-ngx/src/app/shared/services/custom-paginator-intl.ts b/ui-ngx/src/app/shared/services/custom-paginator-intl.ts index ef2ab076d5..fa52a99f3b 100644 --- a/ui-ngx/src/app/shared/services/custom-paginator-intl.ts +++ b/ui-ngx/src/app/shared/services/custom-paginator-intl.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 720f74d2a6..3b371c8aae 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2022 The Thingsboard Authors +/// Copyright © 2016-2023 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -24,7 +24,8 @@ import { FlowInjectionToken, NgxFlowModule } from '@flowjs/ngx-flow'; import { NgxFlowchartModule } from 'ngx-flowchart'; import Flow from '@flowjs/flow.js'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatBadgeModule } from '@angular/material/badge'; import { MatButtonModule } from '@angular/material/button'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatCardModule } from '@angular/material/card'; @@ -42,7 +43,7 @@ import { MatPaginatorIntl, MatPaginatorModule } from '@angular/material/paginato import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatRadioModule } from '@angular/material/radio'; -import { MatSelectModule } from '@angular/material/select'; +import { MAT_SELECT_CONFIG, MatSelectModule } from '@angular/material/select'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSliderModule } from '@angular/material/slider'; @@ -52,7 +53,7 @@ import { MatStepperModule } from '@angular/material/stepper'; import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { MAT_TOOLTIP_DEFAULT_OPTIONS, MatTooltipModule } from '@angular/material/tooltip'; import { MatListModule } from '@angular/material/list'; import { DatetimeAdapter, MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material'; @@ -62,8 +63,9 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { ShareModule as ShareButtonsModule } from 'ngx-sharebuttons'; import { HotkeyModule } from 'angular2-hotkeys'; -import { ColorPickerModule } from 'ngx-color-picker'; +import { ColorPickerModule } from '@iplab/ngx-color-picker'; import { NgxHmCarouselModule } from 'ngx-hm-carousel'; +import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular'; import { UserMenuComponent } from '@shared/components/user-menu.component'; import { NospacePipe } from '@shared/pipe/nospace.pipe'; import { TranslateModule } from '@ngx-translate/core'; @@ -84,6 +86,7 @@ import { MarkdownEditorComponent } from '@shared/components/markdown-editor.comp import { FullscreenDirective } from '@shared/components/fullscreen.directive'; import { HighlightPipe } from '@shared/pipe/highlight.pipe'; import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; +import { DashboardStateAutocompleteComponent } from '@shared/components/dashboard-state-autocomplete.component'; import { EntitySubTypeAutocompleteComponent } from '@shared/components/entity/entity-subtype-autocomplete.component'; import { EntitySubTypeSelectComponent } from '@shared/components/entity/entity-subtype-select.component'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; @@ -93,6 +96,7 @@ import { EntitySelectComponent } from '@shared/components/entity/entity-select.c import { DatetimeComponent } from '@shared/components/time/datetime.component'; import { EntityKeysListComponent } from '@shared/components/entity/entity-keys-list.component'; import { SocialSharePanelComponent } from '@shared/components/socialshare-panel.component'; +import { StringItemsListComponent } from '@shared/components/string-items-list.component'; import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component'; import { EntityListSelectComponent } from '@shared/components/entity/entity-list-select.component'; import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; @@ -114,12 +118,12 @@ import { EntitySubTypeListComponent } from '@shared/components/entity/entity-sub import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { TbJsonPipe } from '@shared/pipe/tbJson.pipe'; import { ColorPickerDialogComponent } from '@shared/components/dialog/color-picker-dialog.component'; -import { MatChipDraggableDirective } from '@shared/components/mat-chip-draggable.directive'; import { ColorInputComponent } from '@shared/components/color-input.component'; import { JsFuncComponent } from '@shared/components/js-func.component'; import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; +import { ErrorAlertDialogComponent } from '@shared/components/dialog/error-alert-dialog.component'; import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component'; import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component'; import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component'; @@ -169,6 +173,25 @@ import { PhoneInputComponent } from '@shared/components/phone-input.component'; import { CustomDateAdapter } from '@shared/adapter/custom-datatime-adapter'; import { CustomPaginatorIntl } from '@shared/services/custom-paginator-intl'; import { TbScriptLangComponent } from '@shared/components/script-lang.component'; +import { NotificationComponent } from '@shared/components/notification/notification.component'; +import { TemplateAutocompleteComponent } from '@shared/components/notification/template-autocomplete.component'; +import { SlackConversationAutocompleteComponent } from '@shared/components/slack-conversation-autocomplete.component'; +import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; +import { + TbBreakPointsProvider, + MdLgLayoutDirective, + MdLgLayoutAlignDirective, + MdLgLayoutGapDirective, + MdLgShowHideDirective, + GtMdLgLayoutDirective, + GtMdLgLayoutAlignDirective, + GtMdLgLayoutGapDirective, + GtMdLgShowHideDirective +} from '@shared/layout/layout.directives'; +import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component'; +import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; +import { ToggleHeaderComponent } from '@shared/components/toggle-header.component'; +import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -183,11 +206,17 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TruncatePipe, TbJsonPipe, FileSizePipe, + DateAgoPipe, SafePipe, + ShortNumberPipe, { provide: FlowInjectionToken, useValue: Flow }, + { + provide: TINYMCE_SCRIPT_SRC, + useValue: 'assets/tinymce/tinymce.min.js' + }, { provide: MAT_DATE_LOCALE, useValue: 'en-GB' @@ -196,7 +225,27 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { provide: HELP_MARKDOWN_COMPONENT_TOKEN, useValue: HelpMarkdownComponent }, { provide: SHARED_MODULE_TOKEN, useValue: SharedModule }, { provide: MatPaginatorIntl, useClass: CustomPaginatorIntl }, - TbPopoverService + TbPopoverService, + { + provide: MAT_SELECT_CONFIG, + useValue: { + overlayPanelClass: 'tb-select-overlay', + hideSingleSelectionIndicator: true + } + }, + { + provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, + useValue: { + hideSingleSelectionIndicator: true + } + }, + { + provide: MAT_TOOLTIP_DEFAULT_OPTIONS, + useValue: { + disableTooltipInteractivity: true + } + }, + TbBreakPointsProvider ], declarations: [ FooterComponent, @@ -205,7 +254,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ToastDirective, FullscreenDirective, CircularProgressDirective, - MatChipDraggableDirective, TbHotkeysDirective, TbAnchorComponent, TbPopoverComponent, @@ -233,6 +281,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TimezoneSelectComponent, ValueInputComponent, DashboardAutocompleteComponent, + DashboardStateAutocompleteComponent, EntitySubTypeAutocompleteComponent, EntitySubTypeSelectComponent, EntitySubTypeListComponent, @@ -246,6 +295,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) QueueAutocompleteComponent, RelationTypeAutocompleteComponent, SocialSharePanelComponent, + StringItemsListComponent, JsonObjectEditComponent, JsonObjectViewComponent, JsonContentComponent, @@ -258,6 +308,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSelectComponent, ConfirmDialogComponent, AlertDialogComponent, + ErrorAlertDialogComponent, TodoDialogComponent, ColorPickerDialogComponent, MaterialIconsDialogComponent, @@ -280,7 +331,9 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TruncatePipe, TbJsonPipe, FileSizePipe, + DateAgoPipe, SafePipe, + ShortNumberPipe, SelectableColumnsPipe, KeyboardShortcutPipe, TbJsonToStringDirective, @@ -295,12 +348,27 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ProtobufContentComponent, BranchAutocompleteComponent, PhoneInputComponent, - TbScriptLangComponent + TbScriptLangComponent, + NotificationComponent, + TemplateAutocompleteComponent, + SlackConversationAutocompleteComponent, + MdLgLayoutDirective, + MdLgLayoutAlignDirective, + MdLgLayoutGapDirective, + MdLgShowHideDirective, + GtMdLgLayoutDirective, + GtMdLgLayoutAlignDirective, + GtMdLgLayoutGapDirective, + GtMdLgShowHideDirective, + ColorPickerComponent, + ToggleHeaderComponent, + RuleChainSelectComponent ], imports: [ CommonModule, RouterModule, TranslateModule, + MatBadgeModule, MatButtonModule, MatButtonToggleModule, MatCheckboxModule, @@ -365,7 +433,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ToastDirective, FullscreenDirective, CircularProgressDirective, - MatChipDraggableDirective, TbHotkeysDirective, TbAnchorComponent, TbStringTemplateOutletDirective, @@ -389,6 +456,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) DatetimeComponent, TimezoneSelectComponent, DashboardAutocompleteComponent, + DashboardStateAutocompleteComponent, EntitySubTypeAutocompleteComponent, EntitySubTypeSelectComponent, EntitySubTypeListComponent, @@ -402,6 +470,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) QueueAutocompleteComponent, RelationTypeAutocompleteComponent, SocialSharePanelComponent, + StringItemsListComponent, JsonObjectEditComponent, JsonObjectViewComponent, JsonContentComponent, @@ -410,9 +479,11 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) HtmlComponent, FabTriggerDirective, FabActionsDirective, + TbJsonToStringDirective, FabToolbarComponent, WidgetsBundleSelectComponent, ValueInputComponent, + MatBadgeModule, MatButtonModule, MatButtonToggleModule, MatCheckboxModule, @@ -457,11 +528,13 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) HotkeyModule, ColorPickerModule, NgxHmCarouselModule, + EditorModule, DndModule, NgxFlowchartModule, MarkdownModule, ConfirmDialogComponent, AlertDialogComponent, + ErrorAlertDialogComponent, TodoDialogComponent, ColorPickerDialogComponent, MaterialIconsDialogComponent, @@ -485,7 +558,9 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TbJsonPipe, KeyboardShortcutPipe, FileSizePipe, + DateAgoPipe, SafePipe, + ShortNumberPipe, SelectableColumnsPipe, RouterModule, TranslateModule, @@ -500,7 +575,21 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ProtobufContentComponent, BranchAutocompleteComponent, PhoneInputComponent, - TbScriptLangComponent + TbScriptLangComponent, + NotificationComponent, + TemplateAutocompleteComponent, + SlackConversationAutocompleteComponent, + MdLgLayoutDirective, + MdLgLayoutAlignDirective, + MdLgLayoutGapDirective, + MdLgShowHideDirective, + GtMdLgLayoutDirective, + GtMdLgLayoutAlignDirective, + GtMdLgLayoutGapDirective, + GtMdLgShowHideDirective, + ColorPickerComponent, + ToggleHeaderComponent, + RuleChainSelectComponent ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/fonts/MaterialIconsRound-Regular.otf b/ui-ngx/src/assets/fonts/MaterialIconsRound-Regular.otf new file mode 100644 index 0000000000..dacf094b51 Binary files /dev/null and b/ui-ngx/src/assets/fonts/MaterialIconsRound-Regular.otf differ diff --git a/ui-ngx/src/assets/fonts/material-icons.css b/ui-ngx/src/assets/fonts/material-icons.css index f790ddaf43..0e200c245e 100644 --- a/ui-ngx/src/assets/fonts/material-icons.css +++ b/ui-ngx/src/assets/fonts/material-icons.css @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,24 @@ url(MaterialIcons-Regular.ttf) format('truetype'); } +@font-face { + font-family: 'Material Icons Round'; + font-style: normal; + font-weight: 400; + src: local('Material Icons Round'), + local('MaterialIconsRound-Regular'), + url(MaterialIconsRound-Regular.otf) format('truetype'); +} + .material-icons { font-family: 'Material Icons'; +} + +.material-icons-round { + font-family: 'Material Icons Round'; +} + +.material-icons, .material-icons-round { font-weight: normal; font-style: normal; font-size: 24px; /* Preferred icon size */ diff --git a/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-hint.md b/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-hint.md new file mode 100644 index 0000000000..f1aa10f921 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-hint.md @@ -0,0 +1,18 @@ +##### X509 Certificate Chain info + +X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication. + +This strategy can: +* check for pre-provisioned devices +* update X.509 device credentials +* create new devices + +The user uploads X.509 certificate to the device profile and sets a regular expression to fetch the device name from *Common Name (CN)*. + +Client certificates must be signed by X.509 certificate, pre-uploaded for this device profile to provision devices by the strategy. + +The client must establish a TLS connection using the entire chain of certificates (this chain must include device profile X.509 certificate on the last level). + +If a device already exists with outdated X.509 credentials, this strategy automatically updates it with the device certificate's credentials from the chain. + +Important: Uploaded certificates should be neither root nor intermediate certificates that are provided by a well-known *Certificate Authority (CA)*. diff --git a/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-regex-examples.md b/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-regex-examples.md new file mode 100644 index 0000000000..8baff8ca0b --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-regex-examples.md @@ -0,0 +1,21 @@ +#### Examples of RegEx usage + +The regular expression is required to extract device name from the X509 certificate's common name. +The regular expression syntax is based on Java [Pattern](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/regex/Pattern.html). +You may also use this [resource](https://regex101.com/) to test your expressions but make sure you select Java 8 flavor. + +* **Pattern:**(.*)\.company.com- matches any characters before the ".company.com". +
    **CN sample:**DeviceA.company.com +
    **Result:**DeviceA + +* **Pattern:** (.*)@company.com- matches any characters before the "@company.com". +
    **CN sample:**DeviceA@company.com +
    **Result:**DeviceA + +* **Pattern:** prefix(.*)suffix@company.com- matches characters between "prefix" and "suffix@company.com". +
    **CN sample:**prefixDeviceAsuffix@company.com +
    **Pattern matches:** DeviceA + +* **Pattern:** \\D+\\.(.*)\\.\\d+@company.com- matches characters between not digits prefix followed by period and sequence of digits with "@company.com" ending. +
    **CN sample:**region.DeviceA.220423@company.com +
    **Pattern matches:** DeviceA diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm.md b/ui-ngx/src/assets/help/en_US/notification/alarm.md new file mode 100644 index 0000000000..00ba021e1b --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/alarm.md @@ -0,0 +1,66 @@ +#### Alarm notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `alarmType` - alarm type; +* `action` - one of: 'created', 'severity changed', 'acknowledged', 'cleared', 'deleted'; +* `alarmId` - the alarm id as uuid string; +* `alarmSeverity` - alarm severity (lower case); +* `alarmStatus` - the alarm status; +* `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; +* `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorId` - the alarm originator entity id as uuid string; +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${action}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume the notification about new alarm with type 'High Temperature' for device 'Sensor A'. +The following template: + +```text +Alarm '${alarmType}' - ${action:upperCase} +{:copy-code} +``` + +will be transformed to: + +```text +Alarm 'High Temperature' - CREATED +``` + +
    + +The following template: + +```text +${alarmOriginatorEntityType:capitalize} '${alarmOriginatorName}' +{:copy-code} +``` + +will be transformed to: + +```text +Device - Sensor A +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md b/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md new file mode 100644 index 0000000000..aa80b13b35 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md @@ -0,0 +1,75 @@ +#### Alarm assignment notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `alarmType` - alarm type; +* `alarmId` - the alarm id as uuid string; +* `alarmSeverity` - alarm severity (lower case); +* `alarmStatus` - the alarm status; +* `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; +* `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorId` - the alarm originator entity id as uuid string; +* `assigneeTitle` - title of the assignee; +* `assigneeEmail` - email of the assignee; +* `assigneeFirstName` - first name of the assignee; +* `assigneeLastName` - last name of the assignee; +* `assigneeId` - the id of the assignee as uuid string; +* `userTitle` - title of the user who made the action; +* `userEmail` - email of the user who made the action; +* `userFirstName` - first name of the user who made the action; +* `userLastName` - last name of the user who made the action; +* `action` - one of: 'assigned', 'unassigned'; +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${action}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume the notification about alarm with type 'High Temperature' for device 'Sensor A' was assigned +to user 'John Doe'. The following template: + +```text +Alarm '${alarmType}' - ${action:upperCase} +{:copy-code} +``` + +will be transformed to: + +```text +Alarm 'High Temperature' - ASSIGNED +``` + +
    + +The following template: + +```text +Alarm '${alarmType}' (${alarmSeverity:capitalize}) was assigned to user +{:copy-code} +``` + +will be transformed to: + +```text +Alarm 'High Temperature' (Critical) was assigned to user +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md b/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md new file mode 100644 index 0000000000..150e76babc --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md @@ -0,0 +1,71 @@ +#### Alarm comment notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `alarmType` - alarm type; +* `alarmId` - the alarm id as uuid string; +* `alarmSeverity` - alarm severity (lower case); +* `alarmStatus` - the alarm status; +* `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; +* `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorId` - the alarm originator entity id as uuid string; +* `comment` - text of the comment; +* `action` - one of: 'added', 'updated'; +* `userTitle` - title of the user who made the action; +* `userEmail` - email of the user who made the action; +* `userFirstName` - first name of the user who made the action; +* `userLastName` - last name of the user who made the action; +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${action}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume the notification about alarm with type 'High Temperature' for device 'Sensor A' was assigned +to user 'John Doe'. The following template: + +```text +Alarm '${alarmType}' - comment ${action} +{:copy-code} +``` + +will be transformed to: + +```text +Alarm 'High Temperature' - comment added +``` + +
    + +The following template: + +```text +Alarm '${alarmType}' (${alarmSeverity:capitalize}) was commented +{:copy-code} +``` + +will be transformed to: + +```text +Alarm 'High Temperature' (Critical) was commented +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/api_usage_limit.md b/ui-ngx/src/assets/help/en_US/notification/api_usage_limit.md new file mode 100644 index 0000000000..e6f7462f94 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/api_usage_limit.md @@ -0,0 +1,52 @@ +#### API usage limit notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `feature` - API feature for which the limit is applied; one of: 'Device API', 'Telemetry persistence', + 'Rule Engine execution', 'JavaScript functions execution', 'Email messages', 'SMS messages', 'Alarms'; +* `status` - one of: 'enabled', 'warning', 'disabled'; +* `unitLabel` - name of the limited unit; one of: 'message', 'data point', 'Rule Engine execution', + 'JavaScript execution', 'email message', 'SMS message', 'alarm'; +* `limit` - the limit on used feature units; +* `currentValue` - current number of used units; +* `tenantId` - id of the tenant; +* `tenantName` - name of the tenant; +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume tenant's devices pushed 8K messages with the max allowed number of 10K +and warn threshold in tenant profile set to 0.8 (80%). The following template: + +```text +${feature} feature - ${status:upperCase} (usage: ${currentValue} out of ${limit} ${unitLabel}s) +{:copy-code} +``` + +will be transformed to: + +```text +Device API feature - WARNING (usage: 8000 out of 10000 messages) +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/device_activity.md b/ui-ngx/src/assets/help/en_US/notification/device_activity.md new file mode 100644 index 0000000000..346cae8e8e --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/device_activity.md @@ -0,0 +1,63 @@ +#### Device activity notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `deviceId` - the device id as uuid string; +* `deviceName` - the device name; +* `deviceLabel` - the device label; +* `deviceType` - the device type; +* `eventType` - one of: 'inactive', 'active'; +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume the notification about inactive thermometer device 'Sensor T1'. +The following template: + +```text +Device '${deviceName}' inactive +{:copy-code} +``` + +will be transformed to: + +```text +Device 'Sensor T1' inactive +``` + +
    + +The following template: + +```text +${deviceType:capitalize} '${deviceName}' became ${eventType} +{:copy-code} +``` + +will be transformed to: + +```text +Thermometer 'Sensor T1' became inactive +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/entities_limit.md b/ui-ngx/src/assets/help/en_US/notification/entities_limit.md new file mode 100644 index 0000000000..6b358d1cc3 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/entities_limit.md @@ -0,0 +1,49 @@ +#### Entity count limit notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `entityType` - one of: 'Device', 'Asset', 'User', etc.; +* `currentCount` - the current count of entities; +* `limit` - the limit on number of entities; +* `percents` - the percent from the notification rule configuration; +* `tenantId` - id of the tenant; +* `tenantName` - name of the tenant; +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume the tenant created 400 devices with the max allowed number is 500 and rule threshold 0.8 (80%). +The following template: + +```text +${entityType:capitalize}s usage: ${currentCount}/${limit} (${percents}%) +{:copy-code} +``` + +will be transformed to: + +```text +Devices usage: 400/500 (80%) +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/entity_action.md b/ui-ngx/src/assets/help/en_US/notification/entity_action.md new file mode 100644 index 0000000000..0907a55220 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/entity_action.md @@ -0,0 +1,67 @@ +#### Entity action notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `entityType` - the entity type, e.g. 'Device'; +* `entityId` - the entity id as uuid string; +* `entityName` - the name of the entity; +* `actionType` - one of: 'added', 'updated', 'deleted'; +* `userId` - id of the user who made the action; +* `userTitle` - title of the user who made the action; +* `userEmail` - email of the user who made the action; +* `userFirstName` - first name of the user who made the action; +* `userLastName` - last name of the user who made the action; +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume the notification about device 'T1' was added by user 'john.doe@gmail.com'. +The following template: + +```text +${entityType:capitalize} was ${actionType}! +{:copy-code} +``` + +will be transformed to: + +```text +Device was added! +``` + +
    + +The following template: + +```text +${entityType} '${entityName}' was ${actionType} by user ${userEmail} +{:copy-code} +``` + +will be transformed to: + +```text +Device 'T1' was added by user john.doe@gmail.com +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/general.md b/ui-ngx/src/assets/help/en_US/notification/general.md new file mode 100644 index 0000000000..efbff88b8d --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/general.md @@ -0,0 +1,42 @@ +#### General notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume the notification recipient user `John Doe`. The following template: + +```text +Hi, ${recipientFirstName}! +{:copy-code} +``` + +will be transformed to: + +```text +Hi, John! +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/new_platform_version.md b/ui-ngx/src/assets/help/en_US/notification/new_platform_version.md new file mode 100644 index 0000000000..d2c600d202 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/new_platform_version.md @@ -0,0 +1,47 @@ +#### New platform version notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `latestVersion` - the latest platform version available; +* `latestVersionReleaseNotesUrl` - release notes link for latest version; +* `upgradeInstructionsUrl` - upgrade instructions link for latest version; +* `currentVersion` - the current platform version +* `currentVersionReleaseNotesUrl` - release notes link for current version; +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume that new 3.5.0 version is released but currently deployed version is 3.4.4. The following template: + +```text +New version ${latestVersion} is available. Current version is ${currentVersion} +{:copy-code} +``` + +will be transformed to: + +```text +New version 3.5.0 is available. Current version is 3.4.4 +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/rule_engine_lifecycle_event.md b/ui-ngx/src/assets/help/en_US/notification/rule_engine_lifecycle_event.md new file mode 100644 index 0000000000..0a738d05c1 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/rule_engine_lifecycle_event.md @@ -0,0 +1,50 @@ +#### Rule engine lifecycle notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `componentType` - one of: 'Rule chain', 'Rule node'; +* `componentId` - the component id as uuid string; +* `componentName` - the rule chain or rule node name; +* `ruleChainId` - the rule chain id as uuid string; +* `ruleChainName` - the rule chain name; +* `eventType` - one of: 'started', 'updated', 'stopped'; +* `action` - one of: 'start', 'update', 'stop'; +* `error` - the error text; +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume the notification about misconfigured Kafka rule node. The following template: + +```text +Rule node '${componentName}' - ${action} failure:
    ${error} +{:copy-code} +``` + +will be transformed to: + +```text +Rule node 'Export to Kafka' - start failure:
    Connection refused! +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/notification/rule_node.md b/ui-ngx/src/assets/help/en_US/notification/rule_node.md new file mode 100644 index 0000000000..9eb7bd3b71 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/rule_node.md @@ -0,0 +1,57 @@ +#### Rule engine notification templatization + +
    +
    + +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* values from the incoming message metadata referenced using the metadata key name; +* values from the incoming message data referenced using the data key name; +* `originatorType` - type of the originator, e.g. 'Device'; +* `originatorId` - id of the originator +* `msgType` - type of the message +* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; + +Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${recipientFirstName:upperCase}` +* `lowerCase`, for example - `${recipientFirstName:lowerCase}` +* `capitalize`, for example - `${recipientFirstName:capitalize}` + +
    + +##### Examples + +Let's assume the incoming message to Rule node has the following data: + +```json +{ + "building_1": { + "temperature": 24 + } +} +``` + +The following template: + +```text +Building 1: temperature is ${building_1.temperature} +{:copy-code} +``` + +will be transformed to: + +```text +Building 1: temperature is 24 +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md b/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md index d13af3fc26..35ede5703a 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md @@ -1,4 +1,4 @@ -#### Additional params object +#### Additional params object

    @@ -16,10 +16,10 @@ An optional key/value object holding additional entity parameters depending on w
-
  • Alarms table widget (On row click or Action cell button) - additionalParams: { alarm: AlarmDataInfo }: +
  • Alarms table widget (On row click or Action cell button) - additionalParams: { alarm: AlarmDataInfo }:
      -
    • alarm: AlarmDataInfo - An - AlarmDataInfo object +
    • alarm: AlarmDataInfo - An + AlarmDataInfo object presenting basic alarm properties (ex. type, severity, originator, etc.) and
      provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration.
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md index 567e093c76..e9a2a90ef7 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md @@ -82,7 +82,7 @@
    - + {{ (addEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
  • @@ -98,7 +98,7 @@ Direction - + {{ ("relation.search-direction." + direction.value) | translate}} diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md index 4e3cd0c608..e2f4f531bc 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md @@ -5,7 +5,7 @@
    -

    Edit

    +

    Edit {{entityType.toLowerCase()}} {{entityName}}

    @@ -84,7 +84,7 @@ Direction - + {{ ("relation.search-direction." + direction.value) | translate}} @@ -109,7 +109,7 @@
    @@ -98,7 +98,7 @@ Direction - + {{ ("relation.search-direction." + direction.value) | translate}} diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_js.md index 22738c53c1..130426c6cf 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_js.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_js.md @@ -94,7 +94,7 @@ let activationLinkDialogTemplate = `

    user.activation-link

    -
    @@ -84,7 +84,7 @@ Direction - + {{ ("relation.search-direction." + direction.value) | translate}} @@ -109,7 +109,7 @@