committed by
GitHub
59 changed files with 2192 additions and 478 deletions
@ -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.common.data.widget; |
||||
|
|
||||
|
public enum DeprecatedFilter { |
||||
|
ALL, |
||||
|
ACTUAL, |
||||
|
DEPRECATED |
||||
|
} |
||||
@ -0,0 +1,134 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT 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.dao.sql.widget; |
||||
|
|
||||
|
import org.springframework.data.domain.Page; |
||||
|
import org.springframework.data.domain.Pageable; |
||||
|
import org.springframework.data.jpa.repository.JpaRepository; |
||||
|
import org.springframework.data.jpa.repository.Query; |
||||
|
import org.springframework.data.repository.query.Param; |
||||
|
import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
public interface WidgetTypeInfoRepository extends JpaRepository<WidgetTypeInfoEntity, UUID> { |
||||
|
|
||||
|
@Query(nativeQuery = true, |
||||
|
value = "SELECT * FROM widget_type_info_view wti WHERE wti.tenant_id = :systemTenantId " + |
||||
|
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " + |
||||
|
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " + |
||||
|
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))", |
||||
|
// "OR to_tsvector(lower(array_to_string(wti.tags, ' '))) @@ to_tsquery(lower(:searchText)))))",
|
||||
|
countQuery = "SELECT count(*) FROM widget_type_info_view wti WHERE wti.tenant_id = :systemTenantId " + |
||||
|
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " + |
||||
|
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes) " + |
||||
|
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))" |
||||
|
) |
||||
|
Page<WidgetTypeInfoEntity> findSystemWidgetTypes(@Param("systemTenantId") UUID systemTenantId, |
||||
|
@Param("searchText") String searchText, |
||||
|
@Param("fullSearch") boolean fullSearch, |
||||
|
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled, |
||||
|
@Param("deprecatedFilter") boolean deprecatedFilter, |
||||
|
@Param("widgetTypesEmpty") boolean widgetTypesEmpty, |
||||
|
@Param("widgetTypes") List<String> widgetTypes, |
||||
|
Pageable pageable); |
||||
|
|
||||
|
@Query(nativeQuery = true, |
||||
|
value = "SELECT * FROM widget_type_info_view wti WHERE wti.tenant_id IN (:tenantId, :nullTenantId) " + |
||||
|
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " + |
||||
|
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " + |
||||
|
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))", |
||||
|
countQuery = "SELECT count(*) FROM widget_type_info_view wti WHERE wti.tenant_id IN (:tenantId, :nullTenantId) " + |
||||
|
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " + |
||||
|
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " + |
||||
|
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))" |
||||
|
) |
||||
|
Page<WidgetTypeInfoEntity> findAllTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId, |
||||
|
@Param("nullTenantId") UUID nullTenantId, |
||||
|
@Param("searchText") String searchText, |
||||
|
@Param("fullSearch") boolean fullSearch, |
||||
|
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled, |
||||
|
@Param("deprecatedFilter") boolean deprecatedFilter, |
||||
|
@Param("widgetTypesEmpty") boolean widgetTypesEmpty, |
||||
|
@Param("widgetTypes") List<String> widgetTypes, |
||||
|
Pageable pageable); |
||||
|
|
||||
|
@Query(nativeQuery = true, |
||||
|
value = "SELECT * FROM widget_type_info_view wti WHERE wti.tenant_id = :tenantId " + |
||||
|
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " + |
||||
|
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " + |
||||
|
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))", |
||||
|
countQuery = "SELECT count(*) FROM widget_type_info_view wti WHERE wti.tenant_id = :tenantId " + |
||||
|
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " + |
||||
|
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " + |
||||
|
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))" |
||||
|
) |
||||
|
Page<WidgetTypeInfoEntity> findTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId, |
||||
|
@Param("searchText") String searchText, |
||||
|
@Param("fullSearch") boolean fullSearch, |
||||
|
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled, |
||||
|
@Param("deprecatedFilter") boolean deprecatedFilter, |
||||
|
@Param("widgetTypesEmpty") boolean widgetTypesEmpty, |
||||
|
@Param("widgetTypes") List<String> widgetTypes, |
||||
|
Pageable pageable); |
||||
|
|
||||
|
@Query("SELECT wti FROM WidgetTypeInfoEntity wti, WidgetsBundleWidgetEntity wbw " + |
||||
|
"WHERE wbw.widgetsBundleId = :widgetsBundleId " + |
||||
|
"AND wbw.widgetTypeId = wti.id ORDER BY wbw.widgetTypeOrder") |
||||
|
List<WidgetTypeInfoEntity> findWidgetTypesInfosByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId); |
||||
|
|
||||
|
@Query(nativeQuery = true, |
||||
|
value = "SELECT * FROM widget_type_info_view wti, widgets_bundle_widget wbw " + |
||||
|
"WHERE wbw.widgets_bundle_id = :widgetsBundleId " + |
||||
|
"AND wbw.widget_type_id = wti.id " + |
||||
|
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " + |
||||
|
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " + |
||||
|
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' ')))) " + |
||||
|
"ORDER BY wbw.widget_type_order", |
||||
|
countQuery = "SELECT count(*) FROM widget_type_info_view wti, widgets_bundle_widget wbw " + |
||||
|
"WHERE wbw.widgets_bundle_id = :widgetsBundleId " + |
||||
|
"AND wbw.widget_type_id = wti.id " + |
||||
|
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " + |
||||
|
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " + |
||||
|
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " + |
||||
|
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))" |
||||
|
) |
||||
|
Page<WidgetTypeInfoEntity> findWidgetTypesInfosByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId, |
||||
|
@Param("searchText") String searchText, |
||||
|
@Param("fullSearch") boolean fullSearch, |
||||
|
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled, |
||||
|
@Param("deprecatedFilter") boolean deprecatedFilter, |
||||
|
@Param("widgetTypesEmpty") boolean widgetTypesEmpty, |
||||
|
@Param("widgetTypes") List<String> widgetTypes, |
||||
|
Pageable pageable); |
||||
|
|
||||
|
} |
||||
@ -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.dao.util.mapping; |
||||
|
|
||||
|
import org.hibernate.type.AbstractSingleColumnStandardBasicType; |
||||
|
import org.hibernate.usertype.DynamicParameterizedType; |
||||
|
|
||||
|
import java.util.Properties; |
||||
|
|
||||
|
public abstract class AbstractArrayType<T> |
||||
|
extends AbstractSingleColumnStandardBasicType<T> |
||||
|
implements DynamicParameterizedType { |
||||
|
|
||||
|
public static final String SQL_ARRAY_TYPE = "sql_array_type"; |
||||
|
|
||||
|
public AbstractArrayType(AbstractArrayTypeDescriptor<T> arrayTypeDescriptor) { |
||||
|
super( |
||||
|
ArraySqlTypeDescriptor.INSTANCE, |
||||
|
arrayTypeDescriptor |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected boolean registerUnderJavaType() { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void setParameterValues(Properties parameters) { |
||||
|
((AbstractArrayTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT 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.dao.util.mapping; |
||||
|
|
||||
|
import org.hibernate.HibernateException; |
||||
|
import org.hibernate.type.descriptor.WrapperOptions; |
||||
|
import org.hibernate.type.descriptor.java.AbstractTypeDescriptor; |
||||
|
import org.hibernate.type.descriptor.java.MutabilityPlan; |
||||
|
import org.hibernate.type.descriptor.java.MutableMutabilityPlan; |
||||
|
import org.hibernate.usertype.DynamicParameterizedType; |
||||
|
|
||||
|
import java.sql.Array; |
||||
|
import java.sql.SQLException; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.Properties; |
||||
|
|
||||
|
import static org.thingsboard.server.dao.util.mapping.AbstractArrayType.SQL_ARRAY_TYPE; |
||||
|
|
||||
|
public abstract class AbstractArrayTypeDescriptor<T> |
||||
|
extends AbstractTypeDescriptor<T> implements DynamicParameterizedType { |
||||
|
|
||||
|
private Class<T> arrayObjectClass; |
||||
|
|
||||
|
private String sqlArrayType; |
||||
|
|
||||
|
public AbstractArrayTypeDescriptor(Class<T> arrayObjectClass) { |
||||
|
this(arrayObjectClass, (MutabilityPlan<T>) new MutableMutabilityPlan<Object>() { |
||||
|
@Override |
||||
|
protected T deepCopyNotNull(Object value) { |
||||
|
return ArrayUtil.deepCopy(value); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected AbstractArrayTypeDescriptor(Class<T> arrayObjectClass, MutabilityPlan<T> mutableMutabilityPlan) { |
||||
|
super(arrayObjectClass, mutableMutabilityPlan); |
||||
|
this.arrayObjectClass = arrayObjectClass; |
||||
|
} |
||||
|
|
||||
|
public Class<T> getArrayObjectClass() { |
||||
|
return arrayObjectClass; |
||||
|
} |
||||
|
|
||||
|
public void setArrayObjectClass(Class<T> arrayObjectClass) { |
||||
|
this.arrayObjectClass = arrayObjectClass; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void setParameterValues(Properties parameters) { |
||||
|
if (parameters.containsKey(PARAMETER_TYPE)) { |
||||
|
arrayObjectClass = ((ParameterType) parameters.get(PARAMETER_TYPE)).getReturnedClass(); |
||||
|
} |
||||
|
sqlArrayType = parameters.getProperty(SQL_ARRAY_TYPE); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean areEqual(T one, T another) { |
||||
|
if (one == another) { |
||||
|
return true; |
||||
|
} |
||||
|
if (one == null || another == null) { |
||||
|
return false; |
||||
|
} |
||||
|
return ArrayUtil.isEquals(one, another); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString(T value) { |
||||
|
return Arrays.deepToString(ArrayUtil.wrapArray(value)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public T fromString(String string) { |
||||
|
return ArrayUtil.fromString(string, arrayObjectClass); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String extractLoggableRepresentation(T value) { |
||||
|
return (value == null) ? "null" : toString(value); |
||||
|
} |
||||
|
|
||||
|
@SuppressWarnings({"unchecked"}) |
||||
|
@Override |
||||
|
public <X> X unwrap(T value, Class<X> type, WrapperOptions options) { |
||||
|
return (X) ArrayUtil.wrapArray(value); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <X> T wrap(X value, WrapperOptions options) { |
||||
|
if (value instanceof Array) { |
||||
|
Array array = (Array) value; |
||||
|
try { |
||||
|
return ArrayUtil.unwrapArray((Object[]) array.getArray(), arrayObjectClass); |
||||
|
} catch (SQLException e) { |
||||
|
throw new HibernateException( |
||||
|
new IllegalArgumentException(e) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
return (T) value; |
||||
|
} |
||||
|
|
||||
|
protected String getSqlArrayType() { |
||||
|
return sqlArrayType; |
||||
|
} |
||||
|
} |
||||
@ -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.dao.util.mapping; |
||||
|
|
||||
|
import org.hibernate.type.descriptor.ValueBinder; |
||||
|
import org.hibernate.type.descriptor.ValueExtractor; |
||||
|
import org.hibernate.type.descriptor.WrapperOptions; |
||||
|
import org.hibernate.type.descriptor.java.JavaTypeDescriptor; |
||||
|
import org.hibernate.type.descriptor.sql.BasicBinder; |
||||
|
import org.hibernate.type.descriptor.sql.BasicExtractor; |
||||
|
import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; |
||||
|
|
||||
|
import java.sql.CallableStatement; |
||||
|
import java.sql.PreparedStatement; |
||||
|
import java.sql.ResultSet; |
||||
|
import java.sql.SQLException; |
||||
|
import java.sql.Types; |
||||
|
|
||||
|
public class ArraySqlTypeDescriptor implements SqlTypeDescriptor { |
||||
|
|
||||
|
public static final ArraySqlTypeDescriptor INSTANCE = new ArraySqlTypeDescriptor(); |
||||
|
|
||||
|
@Override |
||||
|
public int getSqlType() { |
||||
|
return Types.ARRAY; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean canBeRemapped() { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) { |
||||
|
return new BasicBinder<X>(javaTypeDescriptor, this) { |
||||
|
@Override |
||||
|
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { |
||||
|
AbstractArrayTypeDescriptor<Object> abstractArrayTypeDescriptor = (AbstractArrayTypeDescriptor<Object>) javaTypeDescriptor; |
||||
|
st.setArray(index, st.getConnection().createArrayOf( |
||||
|
abstractArrayTypeDescriptor.getSqlArrayType(), |
||||
|
abstractArrayTypeDescriptor.unwrap(value, Object[].class, options) |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) |
||||
|
throws SQLException { |
||||
|
throw new UnsupportedOperationException("Binding by name is not supported!"); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) { |
||||
|
return new BasicExtractor<X>(javaTypeDescriptor, this) { |
||||
|
@Override |
||||
|
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { |
||||
|
return javaTypeDescriptor.wrap(rs.getArray(name), options); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { |
||||
|
return javaTypeDescriptor.wrap(statement.getArray(index), options); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { |
||||
|
return javaTypeDescriptor.wrap(statement.getArray(name), options); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,335 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT 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.dao.util.mapping; |
||||
|
|
||||
|
import java.lang.reflect.Array; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.Collection; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class ArrayUtil { |
||||
|
|
||||
|
public static <T> T deepCopy(Object originalArray) { |
||||
|
Class arrayClass = originalArray.getClass(); |
||||
|
|
||||
|
if (boolean[].class.equals(arrayClass)) { |
||||
|
boolean[] array = (boolean[]) originalArray; |
||||
|
return (T) Arrays.copyOf(array, array.length); |
||||
|
} else if (byte[].class.equals(arrayClass)) { |
||||
|
byte[] array = (byte[]) originalArray; |
||||
|
return (T) Arrays.copyOf(array, array.length); |
||||
|
} else if (short[].class.equals(arrayClass)) { |
||||
|
short[] array = (short[]) originalArray; |
||||
|
return (T) Arrays.copyOf(array, array.length); |
||||
|
} else if (int[].class.equals(arrayClass)) { |
||||
|
int[] array = (int[]) originalArray; |
||||
|
return (T) Arrays.copyOf(array, array.length); |
||||
|
} else if (long[].class.equals(arrayClass)) { |
||||
|
long[] array = (long[]) originalArray; |
||||
|
return (T) Arrays.copyOf(array, array.length); |
||||
|
} else if (float[].class.equals(arrayClass)) { |
||||
|
float[] array = (float[]) originalArray; |
||||
|
return (T) Arrays.copyOf(array, array.length); |
||||
|
} else if (double[].class.equals(arrayClass)) { |
||||
|
double[] array = (double[]) originalArray; |
||||
|
return (T) Arrays.copyOf(array, array.length); |
||||
|
} else if (char[].class.equals(arrayClass)) { |
||||
|
char[] array = (char[]) originalArray; |
||||
|
return (T) Arrays.copyOf(array, array.length); |
||||
|
} else { |
||||
|
Object[] array = (Object[]) originalArray; |
||||
|
return (T) Arrays.copyOf(array, array.length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static Object[] wrapArray(Object originalArray) { |
||||
|
Class arrayClass = originalArray.getClass(); |
||||
|
|
||||
|
if (boolean[].class.equals(arrayClass)) { |
||||
|
boolean[] fromArray = (boolean[]) originalArray; |
||||
|
Boolean[] array = new Boolean[fromArray.length]; |
||||
|
for (int i = 0; i < fromArray.length; i++) { |
||||
|
array[i] = fromArray[i]; |
||||
|
} |
||||
|
return array; |
||||
|
} else if (byte[].class.equals(arrayClass)) { |
||||
|
byte[] fromArray = (byte[]) originalArray; |
||||
|
Byte[] array = new Byte[fromArray.length]; |
||||
|
for (int i = 0; i < fromArray.length; i++) { |
||||
|
array[i] = fromArray[i]; |
||||
|
} |
||||
|
return array; |
||||
|
} else if (short[].class.equals(arrayClass)) { |
||||
|
short[] fromArray = (short[]) originalArray; |
||||
|
Short[] array = new Short[fromArray.length]; |
||||
|
for (int i = 0; i < fromArray.length; i++) { |
||||
|
array[i] = fromArray[i]; |
||||
|
} |
||||
|
return array; |
||||
|
} else if (int[].class.equals(arrayClass)) { |
||||
|
int[] fromArray = (int[]) originalArray; |
||||
|
Integer[] array = new Integer[fromArray.length]; |
||||
|
for (int i = 0; i < fromArray.length; i++) { |
||||
|
array[i] = fromArray[i]; |
||||
|
} |
||||
|
return array; |
||||
|
} else if (long[].class.equals(arrayClass)) { |
||||
|
long[] fromArray = (long[]) originalArray; |
||||
|
Long[] array = new Long[fromArray.length]; |
||||
|
for (int i = 0; i < fromArray.length; i++) { |
||||
|
array[i] = fromArray[i]; |
||||
|
} |
||||
|
return array; |
||||
|
} else if (float[].class.equals(arrayClass)) { |
||||
|
float[] fromArray = (float[]) originalArray; |
||||
|
Float[] array = new Float[fromArray.length]; |
||||
|
for (int i = 0; i < fromArray.length; i++) { |
||||
|
array[i] = fromArray[i]; |
||||
|
} |
||||
|
return array; |
||||
|
} else if (double[].class.equals(arrayClass)) { |
||||
|
double[] fromArray = (double[]) originalArray; |
||||
|
Double[] array = new Double[fromArray.length]; |
||||
|
for (int i = 0; i < fromArray.length; i++) { |
||||
|
array[i] = fromArray[i]; |
||||
|
} |
||||
|
return array; |
||||
|
} else if (char[].class.equals(arrayClass)) { |
||||
|
char[] fromArray = (char[]) originalArray; |
||||
|
Character[] array = new Character[fromArray.length]; |
||||
|
for (int i = 0; i < fromArray.length; i++) { |
||||
|
array[i] = fromArray[i]; |
||||
|
} |
||||
|
return array; |
||||
|
} else if (originalArray instanceof Collection) { |
||||
|
return ((Collection) originalArray).toArray(); |
||||
|
} else { |
||||
|
return (Object[]) originalArray; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static <T> T unwrapArray(Object[] originalArray, Class<T> arrayClass) { |
||||
|
|
||||
|
if (boolean[].class.equals(arrayClass)) { |
||||
|
boolean[] array = new boolean[originalArray.length]; |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
array[i] = originalArray[i] != null ? (Boolean) originalArray[i] : Boolean.FALSE; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (byte[].class.equals(arrayClass)) { |
||||
|
byte[] array = new byte[originalArray.length]; |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
array[i] = originalArray[i] != null ? (Byte) originalArray[i] : 0; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (short[].class.equals(arrayClass)) { |
||||
|
short[] array = new short[originalArray.length]; |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
array[i] = originalArray[i] != null ? (Short) originalArray[i] : 0; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (int[].class.equals(arrayClass)) { |
||||
|
int[] array = new int[originalArray.length]; |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
array[i] = originalArray[i] != null ? (Integer) originalArray[i] : 0; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (long[].class.equals(arrayClass)) { |
||||
|
long[] array = new long[originalArray.length]; |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
array[i] = originalArray[i] != null ? (Long) originalArray[i] : 0L; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (float[].class.equals(arrayClass)) { |
||||
|
float[] array = new float[originalArray.length]; |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
array[i] = originalArray[i] != null ? ((Number) originalArray[i]).floatValue() : 0f; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (double[].class.equals(arrayClass)) { |
||||
|
double[] array = new double[originalArray.length]; |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
array[i] = originalArray[i] != null ? (Double) originalArray[i] : 0d; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (char[].class.equals(arrayClass)) { |
||||
|
char[] array = new char[originalArray.length]; |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
array[i] = originalArray[i] != null ? (Character) originalArray[i] : 0; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (Enum[].class.isAssignableFrom(arrayClass)) { |
||||
|
T array = arrayClass.cast(Array.newInstance(arrayClass.getComponentType(), originalArray.length)); |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
Object objectValue = originalArray[i]; |
||||
|
if (objectValue != null) { |
||||
|
String stringValue = (objectValue instanceof String) ? (String) objectValue : String.valueOf(objectValue); |
||||
|
objectValue = Enum.valueOf((Class) arrayClass.getComponentType(), stringValue); |
||||
|
} |
||||
|
Array.set(array, i, objectValue); |
||||
|
} |
||||
|
return array; |
||||
|
} else if (java.time.LocalDate[].class.equals(arrayClass) && java.sql.Date[].class.equals(originalArray.getClass())) { |
||||
|
// special case because conversion is neither with ctor nor valueOf
|
||||
|
Object[] array = (Object[]) Array.newInstance(java.time.LocalDate.class, originalArray.length); |
||||
|
for (int i = 0; i < array.length; ++i) { |
||||
|
array[i] = originalArray[i] != null ? ((java.sql.Date) originalArray[i]).toLocalDate() : null; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (java.time.LocalDateTime[].class.equals(arrayClass) && java.sql.Timestamp[].class.equals(originalArray.getClass())) { |
||||
|
// special case because conversion is neither with ctor nor valueOf
|
||||
|
Object[] array = (Object[]) Array.newInstance(java.time.LocalDateTime.class, originalArray.length); |
||||
|
for (int i = 0; i < array.length; ++i) { |
||||
|
array[i] = originalArray[i] != null ? ((java.sql.Timestamp) originalArray[i]).toLocalDateTime() : null; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if(arrayClass.getComponentType() != null && arrayClass.getComponentType().isArray()) { |
||||
|
int arrayLength = originalArray.length; |
||||
|
Object[] array = (Object[]) Array.newInstance(arrayClass.getComponentType(), arrayLength); |
||||
|
if (arrayLength > 0) { |
||||
|
for (int i = 0; i < originalArray.length; i++) { |
||||
|
array[i] = unwrapArray((Object[]) originalArray[i], arrayClass.getComponentType()); |
||||
|
} |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else { |
||||
|
if (arrayClass.isInstance(originalArray)) { |
||||
|
return (T) originalArray; |
||||
|
} else { |
||||
|
return (T) Arrays.copyOf(originalArray, originalArray.length, (Class) arrayClass); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static <T> T fromString(String string, Class<T> arrayClass) { |
||||
|
String stringArray = string.replaceAll("[\\[\\]]", ""); |
||||
|
String[] tokens = stringArray.split(","); |
||||
|
|
||||
|
int length = tokens.length; |
||||
|
|
||||
|
if (boolean[].class.equals(arrayClass)) { |
||||
|
boolean[] array = new boolean[length]; |
||||
|
for (int i = 0; i < tokens.length; i++) { |
||||
|
array[i] = Boolean.valueOf(tokens[i]); |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (byte[].class.equals(arrayClass)) { |
||||
|
byte[] array = new byte[length]; |
||||
|
for (int i = 0; i < tokens.length; i++) { |
||||
|
array[i] = Byte.valueOf(tokens[i]); |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (short[].class.equals(arrayClass)) { |
||||
|
short[] array = new short[length]; |
||||
|
for (int i = 0; i < tokens.length; i++) { |
||||
|
array[i] = Short.valueOf(tokens[i]); |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (int[].class.equals(arrayClass)) { |
||||
|
int[] array = new int[length]; |
||||
|
for (int i = 0; i < tokens.length; i++) { |
||||
|
array[i] = Integer.valueOf(tokens[i]); |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (long[].class.equals(arrayClass)) { |
||||
|
long[] array = new long[length]; |
||||
|
for (int i = 0; i < tokens.length; i++) { |
||||
|
array[i] = Long.valueOf(tokens[i]); |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (float[].class.equals(arrayClass)) { |
||||
|
float[] array = new float[length]; |
||||
|
for (int i = 0; i < tokens.length; i++) { |
||||
|
array[i] = Float.valueOf(tokens[i]); |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (double[].class.equals(arrayClass)) { |
||||
|
double[] array = new double[length]; |
||||
|
for (int i = 0; i < tokens.length; i++) { |
||||
|
array[i] = Double.valueOf(tokens[i]); |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else if (char[].class.equals(arrayClass)) { |
||||
|
char[] array = new char[length]; |
||||
|
for (int i = 0; i < tokens.length; i++) { |
||||
|
array[i] = tokens[i].length() > 0 ? tokens[i].charAt(0) : Character.MIN_VALUE; |
||||
|
} |
||||
|
return (T) array; |
||||
|
} else { |
||||
|
return (T) tokens; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static boolean isEquals(Object firstArray, Object secondArray) { |
||||
|
if (firstArray.getClass() != secondArray.getClass()) { |
||||
|
return false; |
||||
|
} |
||||
|
Class arrayClass = firstArray.getClass(); |
||||
|
|
||||
|
if (boolean[].class.equals(arrayClass)) { |
||||
|
return Arrays.equals((boolean[]) firstArray, (boolean[]) secondArray); |
||||
|
} else if (byte[].class.equals(arrayClass)) { |
||||
|
return Arrays.equals((byte[]) firstArray, (byte[]) secondArray); |
||||
|
} else if (short[].class.equals(arrayClass)) { |
||||
|
return Arrays.equals((short[]) firstArray, (short[]) secondArray); |
||||
|
} else if (int[].class.equals(arrayClass)) { |
||||
|
return Arrays.equals((int[]) firstArray, (int[]) secondArray); |
||||
|
} else if (long[].class.equals(arrayClass)) { |
||||
|
return Arrays.equals((long[]) firstArray, (long[]) secondArray); |
||||
|
} else if (float[].class.equals(arrayClass)) { |
||||
|
return Arrays.equals((float[]) firstArray, (float[]) secondArray); |
||||
|
} else if (double[].class.equals(arrayClass)) { |
||||
|
return Arrays.equals((double[]) firstArray, (double[]) secondArray); |
||||
|
} else if (char[].class.equals(arrayClass)) { |
||||
|
return Arrays.equals((char[]) firstArray, (char[]) secondArray); |
||||
|
} else { |
||||
|
return Arrays.equals((Object[]) firstArray, (Object[]) secondArray); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static <T> Class<T[]> toArrayClass(Class<T> arrayElementClass) { |
||||
|
|
||||
|
if (boolean.class.equals(arrayElementClass)) { |
||||
|
return (Class) boolean[].class; |
||||
|
} else if (byte.class.equals(arrayElementClass)) { |
||||
|
return (Class) byte[].class; |
||||
|
} else if (short.class.equals(arrayElementClass)) { |
||||
|
return (Class) short[].class; |
||||
|
} else if (int.class.equals(arrayElementClass)) { |
||||
|
return (Class) int[].class; |
||||
|
} else if (long.class.equals(arrayElementClass)) { |
||||
|
return (Class) long[].class; |
||||
|
} else if (float.class.equals(arrayElementClass)) { |
||||
|
return (Class) float[].class; |
||||
|
} else if (double[].class.equals(arrayElementClass)) { |
||||
|
return (Class) double[].class; |
||||
|
} else if (char[].class.equals(arrayElementClass)) { |
||||
|
return (Class) char[].class; |
||||
|
} else { |
||||
|
Object array = Array.newInstance(arrayElementClass, 0); |
||||
|
return (Class<T[]>) array.getClass(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static <T> List<T> asList(T[] array) { |
||||
|
List<T> list = new ArrayList<T>(array.length); |
||||
|
for (int i = 0; i < array.length; i++) { |
||||
|
list.add(i, array[i]); |
||||
|
} |
||||
|
return list; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT 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.dao.util.mapping; |
||||
|
|
||||
|
import org.hibernate.usertype.DynamicParameterizedType; |
||||
|
|
||||
|
import java.lang.annotation.Annotation; |
||||
|
|
||||
|
public class ParameterizedParameterType implements DynamicParameterizedType.ParameterType { |
||||
|
|
||||
|
private final Class<?> clasz; |
||||
|
|
||||
|
public ParameterizedParameterType(Class<?> clasz) { |
||||
|
this.clasz = clasz; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Class getReturnedClass() { |
||||
|
return clasz; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Annotation[] getAnnotationsMethod() { |
||||
|
return new Annotation[0]; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getCatalog() { |
||||
|
throw new UnsupportedOperationException(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getSchema() { |
||||
|
throw new UnsupportedOperationException(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getTable() { |
||||
|
throw new UnsupportedOperationException(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isPrimaryKey() { |
||||
|
throw new UnsupportedOperationException(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String[] getColumns() { |
||||
|
throw new UnsupportedOperationException(); |
||||
|
} |
||||
|
} |
||||
@ -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.dao.util.mapping; |
||||
|
|
||||
|
import org.hibernate.usertype.DynamicParameterizedType; |
||||
|
import java.util.Properties; |
||||
|
|
||||
|
public class StringArrayType extends AbstractArrayType<String[]> { |
||||
|
|
||||
|
public static final StringArrayType INSTANCE = new StringArrayType(); |
||||
|
|
||||
|
public StringArrayType() { |
||||
|
super( |
||||
|
new StringArrayTypeDescriptor() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public StringArrayType(Class arrayClass) { |
||||
|
this(); |
||||
|
Properties parameters = new Properties(); |
||||
|
parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); |
||||
|
setParameterValues(parameters); |
||||
|
} |
||||
|
|
||||
|
public String getName() { |
||||
|
return "string-array"; |
||||
|
} |
||||
|
} |
||||
@ -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.dao.util.mapping; |
||||
|
|
||||
|
public class StringArrayTypeDescriptor |
||||
|
extends AbstractArrayTypeDescriptor<String[]> { |
||||
|
|
||||
|
public StringArrayTypeDescriptor() { |
||||
|
super(String[].class); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected String getSqlArrayType() { |
||||
|
String sqlArrayType = super.getSqlArrayType(); |
||||
|
return sqlArrayType != null ? sqlArrayType : "text"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -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. |
||||
|
|
||||
|
--> |
||||
|
<cdk-virtual-scroll-viewport #viewport class="tb-scroll-grid-viewport" [itemSize]="itemSize" appendOnly> |
||||
|
<ng-container *cdkVirtualFor="let itemsRow of dataSource"> |
||||
|
<div *ngIf="itemsRow" class="tb-scroll-grid-items-row" [style]="{gap: gap+'px'}"> |
||||
|
<div *ngFor="let item of itemsRow" class="tb-scroll-grid-item-container"> |
||||
|
<ng-container *ngIf="item === 'loadingCell'"> |
||||
|
<ng-container *ngTemplateOutlet="loadingCell ? loadingCell : defaultLoadingCell"></ng-container> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="isObject(item)"> |
||||
|
<ng-container *ngTemplateOutlet="itemCard; context:{ item: item }"></ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</cdk-virtual-scroll-viewport> |
||||
|
<ng-container *ngIf="dataSource.isEmpty"> |
||||
|
<ng-container *ngTemplateOutlet="loadingItems"></ng-container> |
||||
|
</ng-container> |
||||
|
<ng-template #loadingItems> |
||||
|
<ng-container *ngIf="dataSource.initialDataLoading; else emptyData"> |
||||
|
<ng-container *ngTemplateOutlet="dataLoading ? dataLoading : defaultDataLoading"></ng-container> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
<ng-template #emptyData> |
||||
|
<ng-container *ngTemplateOutlet="noData"></ng-container> |
||||
|
</ng-template> |
||||
|
<ng-template #defaultLoadingCell> |
||||
|
<div fxLayout="column" fxLayoutAlign="center center" [style]="{minHeight: itemSize + 'px'}"> |
||||
|
<mat-spinner color="accent" strokeWidth="5"></mat-spinner> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template #defaultDataLoading> |
||||
|
<div fxLayout="column" |
||||
|
fxLayoutAlign="center center" class="tb-absolute-fill"> |
||||
|
<mat-spinner color="accent" strokeWidth="5"></mat-spinner> |
||||
|
</div> |
||||
|
</ng-template> |
||||
@ -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. |
||||
|
*/ |
||||
|
.tb-scroll-grid-viewport { |
||||
|
height: 100%; |
||||
|
.cdk-virtual-scroll-content-wrapper { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
.cdk-virtual-scroll-spacer { |
||||
|
height: auto !important; |
||||
|
} |
||||
|
.tb-scroll-grid-items-row { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
} |
||||
|
.tb-scroll-grid-item-container { |
||||
|
flex: 1; |
||||
|
} |
||||
|
} |
||||
@ -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.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
AfterViewInit, |
||||
|
Component, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
SimpleChanges, |
||||
|
TemplateRef, |
||||
|
ViewChild, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
GridEntitiesFetchFunction, |
||||
|
ScrollGridColumns, |
||||
|
ScrollGridDatasource |
||||
|
} from '@home/models/datasource/scroll-grid-datasource'; |
||||
|
import { BreakpointObserver } from '@angular/cdk/layout'; |
||||
|
import { isObject } from '@app/core/utils'; |
||||
|
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-scroll-grid', |
||||
|
templateUrl: './scroll-grid.component.html', |
||||
|
styleUrls: ['./scroll-grid.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class ScrollGridComponent<T, F> implements OnInit, AfterViewInit, OnChanges { |
||||
|
|
||||
|
@ViewChild('viewport') |
||||
|
viewport: CdkVirtualScrollViewport; |
||||
|
|
||||
|
@Input() |
||||
|
columns: ScrollGridColumns = {columns: 1}; |
||||
|
|
||||
|
@Input() |
||||
|
fetchFunction: GridEntitiesFetchFunction<T, F>; |
||||
|
|
||||
|
@Input() |
||||
|
filter: F; |
||||
|
|
||||
|
@Input() |
||||
|
itemSize = 200; |
||||
|
|
||||
|
@Input() |
||||
|
gap = 12; |
||||
|
|
||||
|
@Input() |
||||
|
itemCard: TemplateRef<{item: T}>; |
||||
|
|
||||
|
@Input() |
||||
|
loadingCell: TemplateRef<any>; |
||||
|
|
||||
|
@Input() |
||||
|
dataLoading: TemplateRef<any>; |
||||
|
|
||||
|
@Input() |
||||
|
noData: TemplateRef<any>; |
||||
|
|
||||
|
dataSource: ScrollGridDatasource<T, F>; |
||||
|
|
||||
|
constructor(private breakpointObserver: BreakpointObserver, |
||||
|
private renderer: Renderer2) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.dataSource = new ScrollGridDatasource<T, F>(this.breakpointObserver, this.columns, this.fetchFunction, this.filter); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
this.renderer.setStyle(this.viewport._contentWrapper.nativeElement, 'gap', this.gap + 'px'); |
||||
|
this.renderer.setStyle(this.viewport._contentWrapper.nativeElement, 'padding', this.gap + 'px'); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange && change.currentValue !== change.previousValue && propName === 'filter') { |
||||
|
this.dataSource.updateFilter(this.filter); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
isObject(value: any): boolean { |
||||
|
return isObject(value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,250 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT 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 { DataSource, ListRange } from '@angular/cdk/collections'; |
||||
|
import { CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; |
||||
|
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs'; |
||||
|
import { catchError } from 'rxjs/operators'; |
||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
||||
|
import { BreakpointObserver } from '@angular/cdk/layout'; |
||||
|
|
||||
|
export type GridEntitiesFetchFunction<T, F> = (pageSize: number, page: number, filter: F) => Observable<PageData<T>>; |
||||
|
|
||||
|
export type GridCellType = 'emptyCell' | 'loadingCell'; |
||||
|
|
||||
|
export interface ScrollGridColumns { |
||||
|
columns: number; |
||||
|
breakpoints?: {[breakpoint: string]: number}; |
||||
|
} |
||||
|
|
||||
|
export class ScrollGridDatasource<T, F> extends DataSource<(T | GridCellType)[]> { |
||||
|
|
||||
|
public initialDataLoading = true; |
||||
|
|
||||
|
private _data: T[] = []; |
||||
|
private _rows: (T | GridCellType)[][] = Array.from<T[]>({length: 100000}); |
||||
|
private _hasNext = true; |
||||
|
private _columns: number; |
||||
|
private _viewport: CdkVirtualScrollViewport; |
||||
|
private _pendingRange: ListRange = null; |
||||
|
private _fetchingData = false; |
||||
|
private _fetchSubscription: Subscription; |
||||
|
private _totalElements = 0; |
||||
|
|
||||
|
private _dataStream: BehaviorSubject<(T | GridCellType)[][]>; |
||||
|
private _subscription: Subscription; |
||||
|
|
||||
|
constructor(private breakpointObserver: BreakpointObserver, |
||||
|
private columns: ScrollGridColumns, |
||||
|
private fetchFunction: GridEntitiesFetchFunction<T, F>, |
||||
|
private filter: F) { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
connect(collectionViewer: CdkVirtualForOf<(T | GridCellType)[]>): Observable<(T | GridCellType)[][]> { |
||||
|
this._viewport = (collectionViewer as any)._viewport; |
||||
|
this._init(); |
||||
|
|
||||
|
if (this.columns.breakpoints) { |
||||
|
const breakpoints = Object.keys(this.columns.breakpoints); |
||||
|
this._subscription.add(this.breakpointObserver.observe(breakpoints).subscribe( |
||||
|
() => { |
||||
|
this._columnsChanged(this._detectColumns()); |
||||
|
} |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
this._subscription.add( |
||||
|
collectionViewer.viewChange.subscribe(range => this._fetchDataFromRange(range)) |
||||
|
); |
||||
|
return this._dataStream; |
||||
|
} |
||||
|
|
||||
|
disconnect(): void { |
||||
|
this._reset(); |
||||
|
this._subscription.unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
get isEmpty(): boolean { |
||||
|
return !this._data.length; |
||||
|
} |
||||
|
|
||||
|
get active(): boolean { |
||||
|
return !!this._subscription && !this._subscription.closed; |
||||
|
} |
||||
|
|
||||
|
public updateFilter(filter: F) { |
||||
|
this.filter = filter; |
||||
|
if (this.active) { |
||||
|
const prevLength = this._rows.length; |
||||
|
this._reset(); |
||||
|
const dataLengthChanged = prevLength !== this._rows.length; |
||||
|
|
||||
|
const range = this._viewport.getRenderedRange(); |
||||
|
|
||||
|
if (dataLengthChanged) { |
||||
|
// Force recalculate new range
|
||||
|
if (range.start === 0) { |
||||
|
range.start = 1; |
||||
|
} |
||||
|
this._viewport.appendOnly = false; |
||||
|
} |
||||
|
|
||||
|
const scrollOffset = this._viewport.measureScrollOffset(); |
||||
|
if (scrollOffset > 0) { |
||||
|
this._viewport.scrollToOffset(0); |
||||
|
} |
||||
|
|
||||
|
this._dataUpdated(); |
||||
|
this._viewport.appendOnly = true; |
||||
|
|
||||
|
if (!dataLengthChanged) { |
||||
|
this._fetchDataFromRange(range); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private _detectColumns(): number { |
||||
|
let columns = this.columns.columns; |
||||
|
if (this.columns.breakpoints) { |
||||
|
for (const breakpont of Object.keys(this.columns.breakpoints)) { |
||||
|
if (this.breakpointObserver.isMatched(breakpont)) { |
||||
|
columns = this.columns.breakpoints[breakpont]; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return columns; |
||||
|
} |
||||
|
|
||||
|
private _init() { |
||||
|
this._subscription = new Subscription(); |
||||
|
this._columns = this._detectColumns(); |
||||
|
if (this._dataStream) { |
||||
|
this._dataStream.complete(); |
||||
|
} |
||||
|
this._dataStream = new BehaviorSubject(this._rows); |
||||
|
} |
||||
|
|
||||
|
private _reset() { |
||||
|
this._data = []; |
||||
|
this._totalElements = 0; |
||||
|
this.initialDataLoading = true; |
||||
|
this._rows = Array.from<T[]>({length: 100000}); |
||||
|
this._hasNext = true; |
||||
|
this._pendingRange = null; |
||||
|
this._fetchingData = false; |
||||
|
if (this._fetchSubscription) { |
||||
|
this._fetchSubscription.unsubscribe(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private _columnsChanged(columns: number) { |
||||
|
if (this._columns !== columns) { |
||||
|
const fetchData = columns > this._columns; |
||||
|
this._columns = columns; |
||||
|
const rowsLength = this._totalElements ? Math.ceil(this._totalElements / this._columns) : 100000; |
||||
|
this._rows = Array.from<T[]>({length: rowsLength}); |
||||
|
this._dataUpdated(); |
||||
|
if (fetchData && this._hasNext) { |
||||
|
this._fetchDataFromRange(this._viewport.getRenderedRange()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private _fetchDataFromRange(range: ListRange) { |
||||
|
if (this._hasNext) { |
||||
|
if (this._fetchingData) { |
||||
|
this._pendingRange = range; |
||||
|
} else { |
||||
|
const endIndex = (range.end + 1) * this._columns; |
||||
|
if (endIndex > this._data.length) { |
||||
|
const startIndex = this._data.length; |
||||
|
const minPageSize = endIndex - startIndex; |
||||
|
const maxPageSize = minPageSize * 2; |
||||
|
let pageSize = minPageSize; |
||||
|
let page = Math.floor(startIndex / pageSize); |
||||
|
while (startIndex % pageSize !== 0 && pageSize <= maxPageSize) { |
||||
|
if (((page + 1) * pageSize) > endIndex) { |
||||
|
break; |
||||
|
} |
||||
|
pageSize++; |
||||
|
page = Math.floor(startIndex / pageSize); |
||||
|
} |
||||
|
const offset = startIndex % pageSize; |
||||
|
this._fetchData(offset, pageSize, page); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private _fetchData(offset: number, pageSize: number, page: number) { |
||||
|
this._fetchingData = true; |
||||
|
this._fetchSubscription = this.fetchFunction(pageSize, page, this.filter).pipe( |
||||
|
catchError(() => of(emptyPageData<T>())) |
||||
|
).subscribe( |
||||
|
(data) => { |
||||
|
this._hasNext = data.hasNext; |
||||
|
if (data.data.length > offset) { |
||||
|
for (let i = offset; i < data.data.length; i++) { |
||||
|
this._data.push(data.data[i]); |
||||
|
} |
||||
|
} |
||||
|
this._totalElements = data.totalElements; |
||||
|
const rowsLength = this._totalElements ? Math.ceil(this._totalElements / this._columns) : 100000; |
||||
|
this._rows = Array.from<T[]>({length: rowsLength}); |
||||
|
this._dataUpdated(); |
||||
|
this.initialDataLoading = false; |
||||
|
this._fetchingData = false; |
||||
|
if (this._pendingRange) { |
||||
|
const range = this._pendingRange; |
||||
|
this._pendingRange = null; |
||||
|
this._fetchDataFromRange(range); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private _dataUpdated() { |
||||
|
for (let index = 0; index < this._data.length; index++) { |
||||
|
const row = Math.floor(index / this._columns); |
||||
|
const col = index % this._columns; |
||||
|
if (!this._rows[row]) { |
||||
|
this._rows[row] = []; |
||||
|
} |
||||
|
this._rows[row][col] = this._data[index]; |
||||
|
} |
||||
|
this._fillGridCells(); |
||||
|
this._dataStream.next(this._rows); |
||||
|
} |
||||
|
|
||||
|
private _fillGridCells() { |
||||
|
if (this._totalElements) { |
||||
|
const startIndex = this._data.length; |
||||
|
const endIndex = this._rows.length * this._columns; |
||||
|
for (let index = startIndex; index < endIndex; index++) { |
||||
|
const row = Math.floor(index / this._columns); |
||||
|
const col = index % this._columns; |
||||
|
const cellType: GridCellType = index < this._totalElements ? 'loadingCell' : 'emptyCell'; |
||||
|
if (!this._rows[row]) { |
||||
|
this._rows[row] = []; |
||||
|
} |
||||
|
this._rows[row][col] = cellType; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue