diff --git a/Signum.Engine.Extensions/Chart/ChartLogic.cs b/Signum.Engine.Extensions/Chart/ChartLogic.cs index 57c268dbac..ea955edd60 100644 --- a/Signum.Engine.Extensions/Chart/ChartLogic.cs +++ b/Signum.Engine.Extensions/Chart/ChartLogic.cs @@ -22,57 +22,27 @@ public static void Start(SchemaBuilder sb, bool googleMapsChartScripts, string[] public static Task ExecuteChartAsync(ChartRequestModel request, CancellationToken token) { - IDynamicQueryCore core = QueryLogic.Queries.GetQuery(request.QueryName).Core.Value; - - return miExecuteChartAsync.GetInvoker(core.GetType().GetGenericArguments()[0])(request, core, token); - } - - static GenericInvoker>> miExecuteChartAsync = - new((req, dq, token) => ExecuteChartAsync(req, (DynamicQueryCore)dq, token)); - static async Task ExecuteChartAsync(ChartRequestModel request, DynamicQueryCore dq, CancellationToken token) - { - using (ExecutionMode.UserInterface()) - { - var result = await dq.ExecuteQueryAsync(new QueryRequest - { - GroupResults = request.HasAggregates(), - QueryName = request.QueryName, - Columns = request.GetQueryColumns(), - Filters = request.Filters, - Orders = request.GetQueryOrders(), - Pagination = request.MaxRows.HasValue ? new Pagination.Firsts(request.MaxRows.Value + 1) : new Pagination.All(), - }, token); - - - if (request.MaxRows.HasValue && result.Rows.Length == request.MaxRows.Value) - throw new InvalidOperationException($"The chart request for ${request.QueryName} exceeded the max rows ({request.MaxRows})"); - - return result; - } + return QueryLogic.Queries.ExecuteQueryAsync(request.ToQueryRequest(), token); } + public static ResultTable ExecuteChart(ChartRequestModel request) { - IDynamicQueryCore core = QueryLogic.Queries.GetQuery(request.QueryName).Core.Value; + return QueryLogic.Queries.ExecuteQuery(request.ToQueryRequest()); - return miExecuteChart.GetInvoker(core.GetType().GetGenericArguments()[0])(request, core); } - static GenericInvoker> miExecuteChart = - new((req, dq) => ExecuteChart(req, (DynamicQueryCore)dq)); - static ResultTable ExecuteChart(ChartRequestModel request, DynamicQueryCore dq) + public static QueryRequest ToQueryRequest(this ChartRequestModel request) { - using (ExecutionMode.UserInterface()) + return new QueryRequest { - return dq.ExecuteQuery(new QueryRequest - { - GroupResults = request.HasAggregates(), - QueryName = request.QueryName, - Columns = request.GetQueryColumns(), - Filters = request.Filters, - Orders = request.GetQueryOrders(), - Pagination = new Pagination.All(), - }); - } + QueryName = request.QueryName, + GroupResults = request.HasAggregates(), + Columns = request.GetQueryColumns(), + Filters = request.Filters, + Orders = request.GetQueryOrders(), + Pagination = request.MaxRows.HasValue ? new Pagination.Firsts(request.MaxRows.Value + 1) : new Pagination.All(), + }; } + } diff --git a/Signum.Engine.Extensions/Chart/UserChartLogic.cs b/Signum.Engine.Extensions/Chart/UserChartLogic.cs index 52528acbe4..0af6578e6a 100644 --- a/Signum.Engine.Extensions/Chart/UserChartLogic.cs +++ b/Signum.Engine.Extensions/Chart/UserChartLogic.cs @@ -133,7 +133,7 @@ public static UserChartEntity RetrieveUserChart(this Lite userC } } - internal static ChartRequestModel ToChartRequest(UserChartEntity userChart) + internal static ChartRequestModel ToChartRequest(this UserChartEntity userChart) { var cr = new ChartRequestModel(userChart.Query.ToQueryName()) { diff --git a/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs b/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs index 9380d2b45c..25473cc218 100644 --- a/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs +++ b/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs @@ -1,26 +1,54 @@ using Signum.Engine.Authorization; +using Signum.Engine.Chart; +using Signum.Engine.Files; +using Signum.Engine.Json; using Signum.Engine.Translation; using Signum.Engine.UserAssets; +using Signum.Engine.UserQueries; using Signum.Engine.ViewLog; using Signum.Entities.Authorization; using Signum.Entities.Basics; using Signum.Entities.Chart; using Signum.Entities.Dashboard; +using Signum.Entities.UserAssets; using Signum.Entities.UserQueries; +using Signum.Utilities.Reflection; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Signum.Engine.Dashboard; public static class DashboardLogic { public static ResetLazy, DashboardEntity>> Dashboards = null!; + public static ResetLazy, List>> CachedQueriesCache = null!; public static ResetLazy>>> DashboardsByType = null!; - public static void Start(SchemaBuilder sb) + public static Polymorphic>> OnGetCachedQueryDefinition = new(); + + + [AutoExpressionField] + public static IQueryable CachedQueries(this DashboardEntity db) => + As.Expression(() => Database.Query().Where(a => a.Dashboard.Is(db))); + + [AutoExpressionField] + public static IQueryable CachedQueries(this UserQueryEntity uq) => + As.Expression(() => Database.Query().Where(a => a.UserAssets.Contains(uq.ToLite()))); + + [AutoExpressionField] + public static IQueryable CachedQueries(this UserChartEntity uc) => + As.Expression(() => Database.Query().Where(a => a.UserAssets.Contains(uc.ToLite()))); + + public static void Start(SchemaBuilder sb, IFileTypeAlgorithm cachedQueryAlgorithm) { if (sb.NotDefined(MethodInfo.GetCurrentMethod())) { PermissionAuthLogic.RegisterPermissions(DashboardPermission.ViewDashboard); + FileTypeLogic.Register(CachedQueryFileType.CachedQuery, cachedQueryAlgorithm); + UserAssetsImporter.Register("Dashboard", DashboardOperation.Save); UserAssetsImporter.PartNames.AddRange(new Dictionary @@ -33,6 +61,13 @@ public static void Start(SchemaBuilder sb) {"UserTreePart", typeof(UserTreePartEntity)}, }); + OnGetCachedQueryDefinition.Register((UserChartPartEntity ucp, PanelPartEmbedded pp) => new[] { new CachedQueryDefinition(ucp.UserChart.ToChartRequest().ToQueryRequest(), pp, ucp.UserChart, ucp.IsQueryCached, canWriteFilters: true) }); + OnGetCachedQueryDefinition.Register((CombinedUserChartPartEntity cucp, PanelPartEmbedded pp) => cucp.UserCharts.Select(uc => new CachedQueryDefinition(uc.UserChart.ToChartRequest().ToQueryRequest(), pp, uc.UserChart, uc.IsQueryCached, canWriteFilters: false))); + OnGetCachedQueryDefinition.Register((UserQueryPartEntity uqp, PanelPartEmbedded pp) => new[] { new CachedQueryDefinition(uqp.RenderMode == UserQueryPartRenderMode.BigValue ? uqp.UserQuery.ToQueryRequestValue() : uqp.UserQuery.ToQueryRequest(), pp, uqp.UserQuery, uqp.IsQueryCached, canWriteFilters: false) }); + OnGetCachedQueryDefinition.Register((ValueUserQueryListPartEntity vuql, PanelPartEmbedded pp) => vuql.UserQueries.Select(uqe => new CachedQueryDefinition(uqe.UserQuery.ToQueryRequestValue(), pp, uqe.UserQuery, uqe.IsQueryCached, canWriteFilters: false))); + OnGetCachedQueryDefinition.Register((UserTreePartEntity ute, PanelPartEmbedded pp) => Array.Empty()); + OnGetCachedQueryDefinition.Register((LinkListPartEntity uqp, PanelPartEmbedded pp) => Array.Empty()); + sb.Include() .WithQuery(() => cp => new { @@ -44,6 +79,24 @@ public static void Start(SchemaBuilder sb) cp.DashboardPriority, }); + sb.Include() + .WithExpressionFrom((DashboardEntity d) => d.CachedQueries()) + .WithExpressionFrom((UserChartEntity d) => d.CachedQueries()) + .WithExpressionFrom((UserQueryEntity d) => d.CachedQueries()) + .WithQuery(() => e => new + { + Entity = e, + e.Id, + e.CreationDate, + e.NumColumns, + e.NumRows, + e.QueryDuration, + e.UploadDuration, + e.File, + UserAssetsCount = e.UserAssets.Count, + e.Dashboard, + }); + if (sb.Settings.ImplementedBy((DashboardEntity cp) => cp.Parts.First().Content, typeof(UserQueryPartEntity))) { sb.Schema.EntityEvents().PreUnsafeDelete += query => @@ -74,8 +127,7 @@ public static void Start(SchemaBuilder sb) Database.MListQuery((DashboardEntity cp) => cp.Parts).Where(mle => query.Contains(((UserChartPartEntity)mle.Element.Content).UserChart)).UnsafeDeleteMList(); Database.Query().Where(uqp => query.Contains(uqp.UserChart)).UnsafeDelete(); - Database.MListQuery((DashboardEntity cp) => cp.Parts).Where(mle => ((CombinedUserChartPartEntity)mle.Element.Content).UserCharts.Any(uc => query.Contains(uc))).UnsafeDeleteMList(); - Database.Query().Where(cuqp => cuqp.UserCharts.Any(uc => query.Contains(uc))).UnsafeDelete(); + Database.MListQuery((DashboardEntity cp) => cp.Parts).Where(mle => ((CombinedUserChartPartEntity)mle.Element.Content).UserCharts.Any(uc => query.Contains(uc.UserChart))).UnsafeDeleteMList(); return null; }; @@ -91,21 +143,27 @@ public static void Start(SchemaBuilder sb) .Where(mle => mle.UserChart.Is(uc))); var mlistElems2 = Administrator.UnsafeDeletePreCommandMList((DashboardEntity cp) => cp.Parts, Database.MListQuery((DashboardEntity cp) => cp.Parts) - .Where(mle => ((CombinedUserChartPartEntity)mle.Element.Content).UserCharts.Contains(uc))); + .Where(mle => ((CombinedUserChartPartEntity)mle.Element.Content).UserCharts.Any(ucm => ucm.UserChart.Is(uc)))); - var parts2 = Administrator.UnsafeDeletePreCommand(Database.Query() - .Where(mle => mle.UserCharts.Contains(uc))); - - return SqlPreCommand.Combine(Spacing.Simple, mlistElems, parts, mlistElems2, parts2); + return SqlPreCommand.Combine(Spacing.Simple, mlistElems, parts, mlistElems2); }; } + sb.Schema.EntityEvents().PreUnsafeDelete += query => + { + query.SelectMany(d => d.CachedQueries()).UnsafeDelete(); + return null; + }; + DashboardGraph.Register(); Dashboards = sb.GlobalLazy(() => Database.Query().ToDictionary(a => a.ToLite()), new InvalidateWith(typeof(DashboardEntity))); + CachedQueriesCache = sb.GlobalLazy(() => Database.Query().GroupToDictionary(a => a.Dashboard), + new InvalidateWith(typeof(CachedQueryEntity))); + DashboardsByType = sb.GlobalLazy(() => Dashboards.Value.Values.Where(a => a.EntityType != null) .SelectCatch(d => KeyValuePair.Create(TypeLogic.IdToType.GetOrThrow(d.EntityType!.Id), d.ToLite())) .GroupToDictionary(), @@ -117,7 +175,6 @@ class DashboardGraph : Graph { public static void Register() { - new Execute(DashboardOperation.Save) { CanBeNew = true, @@ -139,6 +196,80 @@ public static void Register() { Construct = (cp, _) => cp.Clone() }.Register(); + + new Execute(DashboardOperation.RegenerateCachedQueries) + { + CanExecute = c => c.CacheQueryConfiguration == null ? ValidationMessage._0IsNotSet.NiceToString(ReflectionTools.GetPropertyInfo(() => c.CacheQueryConfiguration)) : null, + Execute = (db, _) => + { + var cq = db.CacheQueryConfiguration!; + + var oldCachedQueries = db.CachedQueries().ToList(); + oldCachedQueries.ForEach(a => a.File.DeleteFileOnCommit()); + db.CachedQueries().UnsafeDelete(); + + var definitions = DashboardLogic.GetCachedQueryDefinitions(db).ToList(); + + var combined = DashboardLogic.CombineCachedQueryDefinitions(definitions); + + foreach (var c in combined) + { + var qr = c.QueryRequest; + + if (qr.Pagination is Pagination.All) + { + qr = qr.Clone(); + qr.Pagination = new Pagination.Firsts(cq.MaxRows + 1); + } + + var now = Clock.Now; + + Stopwatch sw = Stopwatch.StartNew(); + + var rt = Connector.CommandTimeoutScope(cq.TimeoutForQueries).Using(_ => QueryLogic.Queries.ExecuteQuery(qr)); + + var queryDuration = sw.ElapsedMilliseconds; + + if(c.QueryRequest.Pagination is Pagination.All) + { + if (rt.Rows.Length == cq.MaxRows) + throw new ApplicationException($"The query for {c.UserAssets.CommaAnd(a => a.KeyLong())} has returned more than {cq.MaxRows} rows: " + + JsonSerializer.Serialize(QueryRequestTS.FromQueryRequest(c.QueryRequest), EntityJsonContext.FullJsonSerializerOptions)); + else + rt = new ResultTable(rt.AllColumns(), null, new Pagination.All()); + } + + + sw.Restart(); + + var json = new CachedQueryJS + { + CreationDate = now, + QueryRequest = QueryRequestTS.FromQueryRequest(c.QueryRequest), + ResultTable = rt, + }; + + var bytes = JsonSerializer.SerializeToUtf8Bytes(json, EntityJsonContext.FullJsonSerializerOptions); + + var file = new Entities.Files.FilePathEmbedded(CachedQueryFileType.CachedQuery, "CachedQuery.json", bytes).SaveFile(); + + var uploadDuration = sw.ElapsedMilliseconds; + + new CachedQueryEntity + { + CreationDate = now, + UserAssets = c.UserAssets.ToMList(), + NumColumns = qr.Columns.Count + (qr.GroupResults ? 0 : 1), + NumRows = rt.Rows.Length, + QueryDuration = queryDuration, + UploadDuration = uploadDuration, + File = file, + Dashboard = db.ToLite(), + }.Save(); + } + + } + }.SetMinimumTypeAllowed(TypeAllowedBasic.Read).Register(); } } @@ -239,6 +370,11 @@ public static DashboardEntity RetrieveDashboard(this Lite dashb } } + public static IEnumerable GetCachedQueries(Lite dashboard) + { + return CachedQueriesCache.Value.TryGetC(dashboard).EmptyIfNull(); + } + public static void RegisterUserTypeCondition(SchemaBuilder sb, TypeConditionSymbol typeCondition) { sb.Schema.Settings.AssertImplementedBy((DashboardEntity uq) => uq.Owner, typeof(UserEntity)); @@ -276,4 +412,226 @@ public static void RegisterPartsTypeCondition(TypeConditionSymbol typeCondition) TypeConditionLogic.Register(typeCondition, uqp => Database.Query().WhereCondition(typeCondition).Any(d => d.ContainsContent(uqp))); } + + public static List GetCachedQueryDefinitions(DashboardEntity db) + { + var definitions = db.Parts.SelectMany(p => OnGetCachedQueryDefinition.Invoke(p.Content, p)).ToList(); + + var groups = definitions + .Where(a => a.PanelPart.InteractionGroup != null) + .GroupToDictionary(a => a.PanelPart.InteractionGroup!.Value); + + foreach (var (key, value) in groups) + { + var writers = value.Where(a => a.CanWriteFilters).ToList(); + if (!writers.Any()) + continue; + + foreach (var wr in writers) + { + if (wr.QueryRequest.GroupResults) + { + var keyColumns = wr.QueryRequest.Columns.Where(c => c.Token is not AggregateToken); + + foreach (var item in value.Where(e => e != wr)) + { + var extraColumns = keyColumns.Where(k => !item.QueryRequest.Columns.Any(c => c.Token.Equals(k.Token))).ToList(); + + if (extraColumns.Any()) + { + item.QueryRequest.Columns.AddRange(extraColumns.Select(c => new Column(c.Token, null))); + var avgs = item.QueryRequest.Columns.Extract(a => a.Token is AggregateToken at && at.AggregateFunction == AggregateFunction.Average); + foreach (var av in avgs) + { + item.QueryRequest.Columns.Remove(av); + item.QueryRequest.Columns.Add(new Column(new AggregateToken(AggregateFunction.Sum, av.Token.Parent!), null)); + item.QueryRequest.Columns.Add(new Column(new AggregateToken(AggregateFunction.Count, av.Token.Parent!, FilterOperation.DistinctTo, null), null)); + } + } + + item.QueryRequest.Pagination = new Pagination.All(); + } + } + } + } + + var cached = definitions.Where(a => a.IsQueryCached); + + return cached.ToList(); + } + + public static List CombineCachedQueryDefinitions(List cachedQueryDefinition) + { + var result = new List(); + foreach (var cqd in cachedQueryDefinition) + { + + var combined = false; + foreach (var r in result) + { + if (r.CombineIfPossible(cqd)) + { + combined = true; + break; + } + } + if (!combined) + result.Add(new CombinedCachedQueryDefinition(cqd)); + } + return result; + } + +} + +public class CachedQueryDefinition +{ + public CachedQueryDefinition(QueryRequest queryRequest, PanelPartEmbedded panelPart, IUserAssetEntity userAsset, bool isQueryCached, bool canWriteFilters) + { + QueryRequest = queryRequest; + PanelPart = panelPart; + Guid = userAsset.Guid; + UserAsset = userAsset.ToLite(); + IsQueryCached = isQueryCached; + CanWriteFilters = canWriteFilters; + } + + public QueryRequest QueryRequest { get; set; } + public PanelPartEmbedded PanelPart { get; set; } + public Guid Guid { get; set; } + public Lite UserAsset { get; set; } + public bool IsQueryCached { get; } + public bool CanWriteFilters { get; } +} + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +public class CachedQueryJS +{ + public DateTime CreationDate; + public QueryRequestTS QueryRequest; + public ResultTable ResultTable; +} + + +public class CombinedCachedQueryDefinition +{ + public QueryRequest QueryRequest { get; set; } + public HashSet> UserAssets { get; set; } + + public CombinedCachedQueryDefinition(CachedQueryDefinition definition) + { + this.QueryRequest = definition.QueryRequest; + this.UserAssets = new HashSet> { definition.UserAsset }; + } + + public bool CombineIfPossible(CachedQueryDefinition definition) + { + var me = QueryRequest; + var other = definition.QueryRequest; + + if (!me.QueryName.Equals(other.QueryName)) + return false; + + if (me.GroupResults != other.GroupResults) + return false; + + if (me.GroupResults) + { + var meKeys = me.Columns.Select(a => a.Token).Where(t => t is not AggregateToken).ToHashSet(); + var otherKeys = me.Columns.Select(a => a.Token).Where(t => t is not AggregateToken).ToHashSet(); + if (!meKeys.SetEquals(otherKeys)) + return false; + } + + var meExtraFilters = me.Filters.Distinct(FilterComparer.Instance).Except(other.Filters, FilterComparer.Instance).ToList(); + var otherExtraFilters = other.Filters.Distinct(FilterComparer.Instance).Except(me.Filters, FilterComparer.Instance).ToList(); + + if (meExtraFilters.Count > 0 || otherExtraFilters.Count > 0) + return false; + + if (me.Pagination is Pagination.All) + { + this.QueryRequest = WithExtraColumns(me, other); + + this.UserAssets.Add(definition.UserAsset); + + return true; + + } + + if (other.Pagination is Pagination.All) + { + this.QueryRequest = WithExtraColumns(other, me); + + this.UserAssets.Add(definition.UserAsset); + + return true; + } + + if (me.Pagination.Equals(other.Pagination) && me.Orders.SequenceEqual(other.Orders)) + { + this.QueryRequest = WithExtraColumns(me, other); + + this.UserAssets.Add(definition.UserAsset); + + return true; + } + + //More cases? + + return false; + } + + static QueryRequest WithExtraColumns(QueryRequest me, QueryRequest other) + { + var otherExtraColumns = other.Columns.Where(c => !me.GroupResults || c.Token is AggregateToken).Where(c => !me.Columns.Any(c2 => c.Token.Equals(c2.Token))).ToList(); + + if (otherExtraColumns.Count == 0) + return me; + + var clone = me.Clone(); + clone.Columns = me.Columns.Concat(otherExtraColumns).ToList(); + return clone; + } +} + +public class FilterComparer : IEqualityComparer +{ + public static readonly FilterComparer Instance = new FilterComparer(); + + public bool Equals(Filter? x, Filter? y) + { + if (x == null) + return y == null; + + if (y == null) + return false; + + if (x is FilterCondition xc) + { + if (y is not FilterCondition yc) + return false; + + return xc.Token.Equals(yc.Token) + && xc.Operation == yc.Operation + && object.Equals(xc.Value, yc.Value); + } + else if (x is FilterGroup xg) + { + if (y is not FilterGroup yg) + return false; + + return object.Equals(xg.Token, yg.Token) && + xg.GroupOperation == yg.GroupOperation && + xg.Filters.ToHashSet(this).SetEquals(yg.Filters); + } + else + throw new UnexpectedValueException(x); + } + + public int GetHashCode([DisallowNull] Filter obj) + { + return obj is FilterCondition f ? f.Token.GetHashCode() : + obj is FilterGroup fg ? fg.GroupOperation.GetHashCode() : + throw new UnexpectedValueException(obj); + } } diff --git a/Signum.Engine.Extensions/MachineLearning/PredictorLogic.cs b/Signum.Engine.Extensions/MachineLearning/PredictorLogic.cs index 31297eb665..ff1f6070c7 100644 --- a/Signum.Engine.Extensions/MachineLearning/PredictorLogic.cs +++ b/Signum.Engine.Extensions/MachineLearning/PredictorLogic.cs @@ -74,7 +74,7 @@ public static void RegisterPublication(PredictorPublicationSymbol publication, P return Trainings.TryGetC(lite)?.Context; } - public static void Start(SchemaBuilder sb, Func predictorFileAlgorithm) + public static void Start(SchemaBuilder sb, IFileTypeAlgorithm predictorFileAlgorithm) { if (sb.NotDefined(MethodInfo.GetCurrentMethod())) { @@ -143,7 +143,7 @@ public static void Start(SchemaBuilder sb, Func predictorFil e.AccuracyValidation, }); - FileTypeLogic.Register(PredictorFileType.PredictorFile, predictorFileAlgorithm()); + FileTypeLogic.Register(PredictorFileType.PredictorFile, predictorFileAlgorithm); SymbolLogic.Start(sb, () => Algorithms.Keys); SymbolLogic.Start(sb, () => Algorithms.Values.SelectMany(a => a.GetRegisteredEncodingSymbols()).Distinct()); diff --git a/Signum.Engine.Extensions/UserQueries/UserQueryLogic.cs b/Signum.Engine.Extensions/UserQueries/UserQueryLogic.cs index bb8d924d01..dd141c9fde 100644 --- a/Signum.Engine.Extensions/UserQueries/UserQueryLogic.cs +++ b/Signum.Engine.Extensions/UserQueries/UserQueryLogic.cs @@ -72,21 +72,44 @@ public static QueryRequest ToQueryRequest(this UserQueryEntity userQuery) return qr; } + public static QueryRequest ToQueryRequestValue(this UserQueryEntity userQuery, QueryToken? valueToken = null) + { + var qn = userQuery.Query.ToQueryName(); + + if (valueToken == null) + { + var qd = QueryLogic.Queries.QueryDescription(qn); + valueToken = QueryUtils.Parse("Count", qd, SubTokensOptions.CanAggregate); + } + + var qr = new QueryRequest() + { + QueryName = qn, + GroupResults = userQuery.GroupResults || valueToken is AggregateToken, + }; + + qr.Filters = userQuery.Filters.ToFilterList(); + qr.Columns = new List { new Column(valueToken, null) }; + qr.Orders = valueToken is AggregateToken ? new List() : userQuery.Orders.Select(qo => new Order(qo.Token.Token, qo.OrderType)).ToList(); + + qr.Pagination = userQuery.GetPagination() ?? new Pagination.All(); + + return qr; + } + static List MergeColumns(UserQueryEntity uq) { QueryDescription qd = QueryLogic.Queries.QueryDescription(uq.Query.ToQueryName()); - switch (uq.ColumnsMode) + var result = uq.ColumnsMode switch { - case ColumnOptionsMode.Add: - return qd.Columns.Where(cd => !cd.IsEntity).Select(cd => new Column(cd, qd.QueryName)).Concat(uq.Columns.Select(co => ToColumn(co))).ToList(); - case ColumnOptionsMode.Remove: - return qd.Columns.Where(cd => !cd.IsEntity && !uq.Columns.Any(co => co.Token.TokenString == cd.Name)).Select(cd => new Column(cd, qd.QueryName)).ToList(); - case ColumnOptionsMode.Replace: - return uq.Columns.Select(co => ToColumn(co)).ToList(); - default: - throw new InvalidOperationException("{0} is not a valid ColumnOptionMode".FormatWith(uq.ColumnsMode)); - } + ColumnOptionsMode.Add => qd.Columns.Where(cd => !cd.IsEntity).Select(cd => new Column(cd, qd.QueryName)).Concat(uq.Columns.Select(co => ToColumn(co))).ToList(), + ColumnOptionsMode.Remove => qd.Columns.Where(cd => !cd.IsEntity && !uq.Columns.Any(co => co.Token.TokenString == cd.Name)).Select(cd => new Column(cd, qd.QueryName)).ToList(), + ColumnOptionsMode.Replace => uq.Columns.Select(co => ToColumn(co)).ToList(), + _ => throw new InvalidOperationException("{0} is not a valid ColumnOptionMode".FormatWith(uq.ColumnsMode)) + }; + + return result; } private static Column ToColumn(QueryColumnEmbedded co) diff --git a/Signum.Engine/DynamicQuery/AutoDynamicQuery.cs b/Signum.Engine/DynamicQuery/AutoDynamicQuery.cs index 836195c143..bbce2a0b1e 100644 --- a/Signum.Engine/DynamicQuery/AutoDynamicQuery.cs +++ b/Signum.Engine/DynamicQuery/AutoDynamicQuery.cs @@ -213,21 +213,6 @@ public override IQueryable GetEntitiesFull(QueryEntitiesRequest request) return result.TryTake(request.Count); } - public override DQueryable GetDQueryable(DQueryableRequest request) - { - request.Columns.Insert(0, new _EntityColumn(EntityColumnFactory().BuildColumnDescription(), QueryName)); - - DQueryable query = Query - .ToDQueryable(GetQueryDescription()) - .SelectMany(request.Multiplications) - .OrderBy(request.Orders) - .Where(request.Filters) - .Select(request.Columns) - .TryTake(request.Count); - - return new DQueryable(query.Query, query.Context); - } - public override Expression? Expression { get { return Query.Expression; } diff --git a/Signum.Engine/DynamicQuery/DynamicQuery.cs b/Signum.Engine/DynamicQuery/DynamicQuery.cs index 631dde23ae..c07b53e080 100644 --- a/Signum.Engine/DynamicQuery/DynamicQuery.cs +++ b/Signum.Engine/DynamicQuery/DynamicQuery.cs @@ -69,7 +69,6 @@ public interface IDynamicQueryCore IQueryable> GetEntitiesLite(QueryEntitiesRequest request); IQueryable GetEntitiesFull(QueryEntitiesRequest request); - DQueryable GetDQueryable(DQueryableRequest request); } @@ -130,7 +129,6 @@ public abstract class DynamicQueryCore : IDynamicQueryCore public abstract IQueryable> GetEntitiesLite(QueryEntitiesRequest request); public abstract IQueryable GetEntitiesFull(QueryEntitiesRequest request); - public abstract DQueryable GetDQueryable(DQueryableRequest request); protected virtual ColumnDescriptionFactory[] InitializeColumns() @@ -1122,22 +1120,27 @@ public static ResultTable ToResultTable(this DEnumerableCount collection, { object[] array = collection.Collection as object[] ?? collection.Collection.ToArray(); - var columnAccesors = req.Columns.Select(c => ( - column: c, - lambda: Expression.Lambda(c.Token.BuildExpression(collection.Context), collection.Context.Parameter) - )).ToList(); + var isMultiKeyGrupping = req.GroupResults && req.Columns.Count(col => col.Token is not AggregateToken) >= 2; - return ToResultTable(array, columnAccesors, collection.TotalElements, req.Pagination); - } + var rows = collection.Collection.ToArray(); - public static ResultTable ToResultTable(object[] result, List<(Column column, LambdaExpression lambda)> columnAccesors, int? totalElements, Pagination pagination) - { - var columnValues = columnAccesors.Select(c => new ResultColumn( - c.column, - miGetValues.GetInvoker(c.column.Type)(result, c.lambda.Compile())) - ).ToArray(); + var columnAccesors = req.Columns.Select(c => + { + var expression = Expression.Lambda(c.Token.BuildExpression(collection.Context), collection.Context.Parameter); + + var lambda = expression.Compile(); + + var values = miGetValues.GetInvoker(c.Token.Type)(rows, lambda); + + var rc = new ResultColumn(c, values); + + if (c.Token.Type.IsLite() || isMultiKeyGrupping && c.Token is not AggregateToken) + rc.CompressUniqueValues = true; + + return rc; + }).ToArray(); - return new ResultTable(columnValues, totalElements, pagination); + return new ResultTable(columnAccesors, collection.TotalElements, req.Pagination); } static readonly GenericInvoker> miGetValues = new((objs, del) => GetValues(objs, (Func)del)); diff --git a/Signum.Engine/DynamicQuery/DynamicQueryContainer.cs b/Signum.Engine/DynamicQuery/DynamicQueryContainer.cs index 962f4f9db4..730d8f079f 100644 --- a/Signum.Engine/DynamicQuery/DynamicQueryContainer.cs +++ b/Signum.Engine/DynamicQuery/DynamicQueryContainer.cs @@ -162,11 +162,6 @@ public IQueryable GetEntitiesFull(QueryEntitiesRequest request) return Execute(ExecuteType.GetEntities, request.QueryName, null, dqb => dqb.Core.Value.GetEntitiesFull(request)); } - public DQueryable GetDQueryable(DQueryableRequest request) - { - return Execute(ExecuteType.GetDQueryable, request.QueryName, null, dqb => dqb.Core.Value.GetDQueryable(request)); - } - public event Func? AllowQuery; public bool QueryAllowed(object queryName, bool fullScreen) diff --git a/Signum.Engine/DynamicQuery/ManualDynamicQuery.cs b/Signum.Engine/DynamicQuery/ManualDynamicQuery.cs index 9e9281fa76..45d9423c9f 100644 --- a/Signum.Engine/DynamicQuery/ManualDynamicQuery.cs +++ b/Signum.Engine/DynamicQuery/ManualDynamicQuery.cs @@ -95,7 +95,7 @@ public override async Task ExecuteQueryGroupAsync(QueryRequest requ { req.Columns.Add(new Column(request.ValueToken, request.ValueToken.NiceName())); var result = await Execute(req, GetQueryDescription(), cancellationToken); - return result.SelectOne(request.ValueToken).Unique(UniqueType.Single); + return result.SelectOne(request.ValueToken).Unique(UniqueType.SingleOrDefault); } } @@ -129,11 +129,6 @@ public override IQueryable> GetEntitiesLite(QueryEntitiesRequest re throw new NotImplementedException(); } - public override DQueryable GetDQueryable(DQueryableRequest request) - { - throw new NotImplementedException(); - } - public override IQueryable GetEntitiesFull(QueryEntitiesRequest request) { throw new NotImplementedException(); diff --git a/Signum.Engine/Engine/SchemaSynchronizer.cs b/Signum.Engine/Engine/SchemaSynchronizer.cs index 2633e7232b..0a4f83e665 100644 --- a/Signum.Engine/Engine/SchemaSynchronizer.cs +++ b/Signum.Engine/Engine/SchemaSynchronizer.cs @@ -625,6 +625,7 @@ public static string GetDefaultValue(ITable table, IColumn column, Replacements column.DbType.IsString() ? "''" : column.DbType.IsDate() ? "GetDate()" : column.DbType.IsGuid() ? "NEWID()" : + column.DbType.IsTime() ? "'00:00'" : "?"); string defaultValue = rep.Interactive ? SafeConsole.AskString($"Default value for '{table.Name.Name}.{column.Name}'? ([Enter] for {typeDefault} or 'force' if there are no {(forNewColumn ? "rows" : "nulls")}) ", stringValidator: str => null) : ""; diff --git a/Signum.Engine/Json/JsonExtensions.cs b/Signum.Engine/Json/EntityJsonContext.cs similarity index 92% rename from Signum.Engine/Json/JsonExtensions.cs rename to Signum.Engine/Json/EntityJsonContext.cs index d94e8d52cb..d8dfc08e87 100644 --- a/Signum.Engine/Json/JsonExtensions.cs +++ b/Signum.Engine/Json/EntityJsonContext.cs @@ -1,73 +1,76 @@ -using System.Collections.Immutable; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Signum.Engine.Json; - -public static class EntityJsonContext -{ - public static JsonSerializerOptions FullJsonSerializerOptions; - static EntityJsonContext() - { - var ejcf = new EntityJsonConverterFactory(); - - FullJsonSerializerOptions = new JsonSerializerOptions - { - IncludeFields = true, - Converters = - { - ejcf, - new MListJsonConverterFactory(ejcf.AssertCanWrite), - new LiteJsonConverterFactory(), - new JsonStringEnumConverter(), - new TimeSpanConverter(), - new DateOnlyConverter() - } - }; - } - - static readonly ThreadVariable?> currentPropertyRoute = Statics.ThreadVariable?>("jsonPropertyRoute"); - - public static (PropertyRoute pr, ModifiableEntity? mod, PrimaryKey? rowId)? CurrentPropertyRouteAndEntity - { - get { return currentPropertyRoute.Value?.Peek(); } - } - - public static IRootEntity? FindCurrentRootEntity() - { - return currentPropertyRoute.Value?.FirstOrDefault(a => a.mod is IRootEntity).mod as IRootEntity; - } - - public static PrimaryKey? FindCurrentRowId() - { - return currentPropertyRoute.Value?.Where(a => a.rowId != null).FirstOrDefault().rowId; - } - - public static IDisposable SetCurrentPropertyRouteAndEntity((PropertyRoute, ModifiableEntity?, PrimaryKey? rowId) pair) - { - var old = currentPropertyRoute.Value; - - currentPropertyRoute.Value = (old ?? ImmutableStack<(PropertyRoute pr, ModifiableEntity? mod, PrimaryKey? rowId)>.Empty).Push(pair); - - return new Disposable(() => { currentPropertyRoute.Value = old; }); - } - - static readonly ThreadVariable allowDirectMListChangesVariable = Statics.ThreadVariable("allowDirectMListChanges"); - - public static bool AllowDirectMListChanges - { - get { return allowDirectMListChangesVariable.Value; } - } - - public static IDisposable SetAllowDirectMListChanges(bool allowMListDirectChanges) - { - var old = allowDirectMListChangesVariable.Value; - - allowDirectMListChangesVariable.Value = allowMListDirectChanges; - - return new Disposable(() => { allowDirectMListChangesVariable.Value = old; }); - } - - - -} +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Signum.Engine.Json; + +public static class EntityJsonContext +{ + public static JsonSerializerOptions FullJsonSerializerOptions; + static EntityJsonContext() + { + var ejcf = new EntityJsonConverterFactory(); + + FullJsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + IncludeFields = true, + Converters = + { + ejcf, + new LiteJsonConverterFactory(), + new MListJsonConverterFactory(ejcf.AssertCanWrite), + new JsonStringEnumConverter(), + new ResultTableConverter(), + new TimeSpanConverter(), + new DateOnlyConverter(), + new TimeOnlyConverter() + } + }; + } + + static readonly ThreadVariable?> currentPropertyRoute = Statics.ThreadVariable?>("jsonPropertyRoute"); + + public static (PropertyRoute pr, ModifiableEntity? mod, PrimaryKey? rowId)? CurrentPropertyRouteAndEntity + { + get { return currentPropertyRoute.Value?.Peek(); } + } + + public static IRootEntity? FindCurrentRootEntity() + { + return currentPropertyRoute.Value?.FirstOrDefault(a => a.mod is IRootEntity).mod as IRootEntity; + } + + public static PrimaryKey? FindCurrentRowId() + { + return currentPropertyRoute.Value?.Where(a => a.rowId != null).FirstOrDefault().rowId; + } + + public static IDisposable SetCurrentPropertyRouteAndEntity((PropertyRoute, ModifiableEntity?, PrimaryKey? rowId) pair) + { + var old = currentPropertyRoute.Value; + + currentPropertyRoute.Value = (old ?? ImmutableStack<(PropertyRoute pr, ModifiableEntity? mod, PrimaryKey? rowId)>.Empty).Push(pair); + + return new Disposable(() => { currentPropertyRoute.Value = old; }); + } + + static readonly ThreadVariable allowDirectMListChangesVariable = Statics.ThreadVariable("allowDirectMListChanges"); + + public static bool AllowDirectMListChanges + { + get { return allowDirectMListChangesVariable.Value; } + } + + public static IDisposable SetAllowDirectMListChanges(bool allowMListDirectChanges) + { + var old = allowDirectMListChangesVariable.Value; + + allowDirectMListChangesVariable.Value = allowMListDirectChanges; + + return new Disposable(() => { allowDirectMListChangesVariable.Value = old; }); + } + + + +} diff --git a/Signum.Engine/Json/FilterJsonConverter.cs b/Signum.Engine/Json/FilterJsonConverter.cs new file mode 100644 index 0000000000..fd97d290d7 --- /dev/null +++ b/Signum.Engine/Json/FilterJsonConverter.cs @@ -0,0 +1,362 @@ +using Signum.Engine.Basics; +using Signum.Entities.DynamicQuery; +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Signum.Engine.Json; +#pragma warning disable CS8618 // Non-nullable field is uninitialized. +public class FilterJsonConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return typeof(FilterTS).IsAssignableFrom(objectType); + } + + public override void Write(Utf8JsonWriter writer, FilterTS value, JsonSerializerOptions options) + { + if (value is FilterConditionTS fc) + JsonSerializer.Serialize(writer, fc, options); + else if (value is FilterGroupTS fg) + JsonSerializer.Serialize(writer, fg, options); + else + throw new UnexpectedValueException(value); + } + + public override FilterTS? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using (var doc = JsonDocument.ParseValue(ref reader)) + { + var elem = doc.RootElement; + + if (elem.TryGetProperty("operation", out var oper)) + { + return new FilterConditionTS + { + token = elem.GetProperty("token").GetString()!, + operation = oper.GetString()!.ToEnum(), + value = elem.TryGetProperty("value", out var val) ? val.ToObject(options) : null, + }; + } + + if (elem.TryGetProperty("groupOperation", out var groupOper)) + return new FilterGroupTS + { + groupOperation = groupOper.GetString()!.ToEnum(), + token = elem.TryGetProperty("token", out var token)? token.GetString() : null, + filters = elem.GetProperty("filters").EnumerateArray().Select(a => a.ToObject()!).ToList() + }; + + throw new InvalidOperationException("Impossible to determine type of filter"); + } + } +} + +[JsonConverter(typeof(FilterJsonConverter))] +public abstract class FilterTS +{ + public abstract Filter ToFilter(QueryDescription qd, bool canAggregate, JsonSerializerOptions jsonSerializerOptions); + + public static FilterTS FromFilter(Filter filter) + { + if (filter is FilterCondition fc) + return new FilterConditionTS + { + token = fc.Token.FullKey(), + operation = fc.Operation, + value = fc.Value + }; + + if (filter is FilterGroup fg) + return new FilterGroupTS + { + token = fg.Token?.FullKey(), + groupOperation = fg.GroupOperation, + filters = fg.Filters.Select(f => FromFilter(f)).ToList(), + }; + + throw new UnexpectedValueException(filter); + } +} + +public class FilterConditionTS : FilterTS +{ + public string token; + public FilterOperation operation; + public object? value; + + public override Filter ToFilter(QueryDescription qd, bool canAggregate, JsonSerializerOptions jsonSerializerOptions) + { + var options = SubTokensOptions.CanElement | SubTokensOptions.CanAnyAll | (canAggregate ? SubTokensOptions.CanAggregate : 0); + var parsedToken = QueryUtils.Parse(token, qd, options); + var expectedValueType = operation.IsList() ? typeof(List<>).MakeGenericType(parsedToken.Type.Nullify()) : parsedToken.Type; + + var val = value is JsonElement jtok ? + jtok.ToObject(expectedValueType, jsonSerializerOptions) : + value; + + if (val is DateTime dt) + val = dt.FromUserInterface(); + else if (val is ObservableCollection col) + val = col.Select(dt => dt?.FromUserInterface()).ToObservableCollection(); + + return new FilterCondition(parsedToken, operation, val); + } + + public override string ToString() => $"{token} {operation} {value}"; +} + +public class FilterGroupTS : FilterTS +{ + public FilterGroupOperation groupOperation; + public string? token; + public List filters; + + public override Filter ToFilter(QueryDescription qd, bool canAggregate, JsonSerializerOptions jsonSerializerOptions) + { + var options = SubTokensOptions.CanElement | SubTokensOptions.CanAnyAll | (canAggregate ? SubTokensOptions.CanAggregate : 0); + var parsedToken = token == null ? null : QueryUtils.Parse(token, qd, options); + + var parsedFilters = filters.Select(f => f.ToFilter(qd, canAggregate, jsonSerializerOptions)).ToList(); + + return new FilterGroup(groupOperation, parsedToken, parsedFilters); + } +} + +public class ColumnTS +{ + public string token; + public string? displayName; + + public Column ToColumn(QueryDescription qd, bool canAggregate) + { + var queryToken = QueryUtils.Parse(token, qd, SubTokensOptions.CanElement | (canAggregate ? SubTokensOptions.CanAggregate : 0)); + + return new Column(queryToken, displayName ?? queryToken.NiceName()); + } + + public override string ToString() => $"{token} '{displayName}'"; + +} + +public class PaginationTS +{ + public PaginationMode mode; + public int? elementsPerPage; + public int? currentPage; + + public PaginationTS() { } + + public PaginationTS(Pagination pagination) + { + this.mode = pagination.GetMode(); + this.elementsPerPage = pagination.GetElementsPerPage(); + this.currentPage = (pagination as Pagination.Paginate)?.CurrentPage; + } + + public override string ToString() => $"{mode} {elementsPerPage} {currentPage}"; + + + public Pagination ToPagination() + { + return mode switch + { + PaginationMode.All => new Pagination.All(), + PaginationMode.Firsts => new Pagination.Firsts(this.elementsPerPage!.Value), + PaginationMode.Paginate => new Pagination.Paginate(this.elementsPerPage!.Value, this.currentPage!.Value), + _ => throw new InvalidOperationException($"Unexpected {mode}"), + }; + } +} + +public class SystemTimeTS +{ + public SystemTimeMode mode; + public SystemTimeJoinMode? joinMode; + public DateTimeOffset? startDate; + public DateTimeOffset? endDate; + + public SystemTimeTS() { } + + public SystemTimeTS(SystemTime systemTime) + { + if (systemTime is SystemTime.AsOf asOf) + { + mode = SystemTimeMode.AsOf; + startDate = asOf.DateTime; + } + else if (systemTime is SystemTime.Between between) + { + mode = SystemTimeMode.Between; + joinMode = ToSystemTimeJoinMode(between.JoinBehaviour); + startDate = between.StartDateTime; + endDate = between.EndtDateTime; + } + else if (systemTime is SystemTime.ContainedIn containedIn) + { + mode = SystemTimeMode.ContainedIn; + joinMode = ToSystemTimeJoinMode(containedIn.JoinBehaviour); + startDate = containedIn.StartDateTime; + endDate = containedIn.EndtDateTime; + } + else if (systemTime is SystemTime.All all) + { + mode = SystemTimeMode.All; + joinMode = ToSystemTimeJoinMode(all.JoinBehaviour); + startDate = null; + endDate = null; + } + else + throw new InvalidOperationException("Unexpected System Time"); + } + + public override string ToString() => $"{mode} {startDate} {endDate}"; + + + public SystemTime ToSystemTime() + { + return mode switch + { + SystemTimeMode.AsOf => new SystemTime.AsOf(startDate!.Value), + SystemTimeMode.Between => new SystemTime.Between(startDate!.Value, endDate!.Value, ToJoinBehaviour(joinMode!.Value)), + SystemTimeMode.ContainedIn => new SystemTime.ContainedIn(startDate!.Value, endDate!.Value, ToJoinBehaviour(joinMode!.Value)), + SystemTimeMode.All => new SystemTime.All(ToJoinBehaviour(joinMode!.Value)), + _ => throw new InvalidOperationException($"Unexpected {mode}"), + }; + } + + public static JoinBehaviour ToJoinBehaviour(SystemTimeJoinMode joinMode) + { + return joinMode switch + { + SystemTimeJoinMode.Current => JoinBehaviour.Current, + SystemTimeJoinMode.FirstCompatible => JoinBehaviour.FirstCompatible, + SystemTimeJoinMode.AllCompatible => JoinBehaviour.AllCompatible, + _ => throw new UnexpectedValueException(joinMode), + }; + } + + public static SystemTimeJoinMode ToSystemTimeJoinMode(JoinBehaviour joinBehaviour) + { + return joinBehaviour switch + { + JoinBehaviour.Current => SystemTimeJoinMode.Current, + JoinBehaviour.FirstCompatible => SystemTimeJoinMode.FirstCompatible, + JoinBehaviour.AllCompatible => SystemTimeJoinMode.AllCompatible, + _ => throw new UnexpectedValueException(joinBehaviour), + }; + } +} + +public class QueryValueRequestTS +{ + public string querykey; + public List filters; + public string valueToken; + public bool? multipleValues; + public SystemTimeTS/*?*/ systemTime; + + public QueryValueRequest ToQueryValueRequest(JsonSerializerOptions jsonSerializerOptions) + { + var qn = QueryLogic.ToQueryName(this.querykey); + var qd = QueryLogic.Queries.QueryDescription(qn); + + var value = valueToken.HasText() ? QueryUtils.Parse(valueToken, qd, SubTokensOptions.CanAggregate | SubTokensOptions.CanElement) : null; + + return new QueryValueRequest + { + QueryName = qn, + MultipleValues = multipleValues ?? false, + Filters = this.filters.EmptyIfNull().Select(f => f.ToFilter(qd, canAggregate: false, jsonSerializerOptions)).ToList(), + ValueToken = value, + SystemTime = this.systemTime?.ToSystemTime(), + }; + } + + public override string ToString() => querykey; +} + +public class QueryRequestTS +{ + public string? queryUrl; + public string queryKey; + public bool groupResults; + public List filters; + public List orders; + public List columns; + public PaginationTS pagination; + public SystemTimeTS? systemTime; + + public static QueryRequestTS FromQueryRequest(QueryRequest qr) + { + return new QueryRequestTS + { + queryKey = QueryUtils.GetKey(qr.QueryName), + queryUrl = qr.QueryUrl, + groupResults = qr.GroupResults, + columns = qr.Columns.Select(c => new ColumnTS { token = c.Token.FullKey(), displayName = c.DisplayName }).ToList(), + filters = qr.Filters.Select(f => FilterTS.FromFilter(f)).ToList(), + orders = qr.Orders.Select(o => new OrderTS { orderType = o.OrderType, token = o.Token.FullKey() }).ToList(), + pagination = new PaginationTS(qr.Pagination), + systemTime = qr.SystemTime == null ? null : new SystemTimeTS(qr.SystemTime), + }; + } + + public QueryRequest ToQueryRequest(JsonSerializerOptions jsonSerializerOptions) + { + var qn = QueryLogic.ToQueryName(this.queryKey); + var qd = QueryLogic.Queries.QueryDescription(qn); + + return new QueryRequest + { + QueryUrl = queryUrl, + QueryName = qn, + GroupResults = groupResults, + Filters = this.filters.EmptyIfNull().Select(f => f.ToFilter(qd, canAggregate: groupResults, jsonSerializerOptions)).ToList(), + Orders = this.orders.EmptyIfNull().Select(f => f.ToOrder(qd, canAggregate: groupResults)).ToList(), + Columns = this.columns.EmptyIfNull().Select(f => f.ToColumn(qd, canAggregate: groupResults)).ToList(), + Pagination = this.pagination.ToPagination(), + SystemTime = this.systemTime?.ToSystemTime(), + }; + } + + + public override string ToString() => queryKey; +} + +public class QueryEntitiesRequestTS +{ + public string queryKey; + public List filters; + public List orders; + public int? count; + + public override string ToString() => queryKey; + + public QueryEntitiesRequest ToQueryEntitiesRequest(JsonSerializerOptions jsonSerializerOptions) + { + var qn = QueryLogic.ToQueryName(queryKey); + var qd = QueryLogic.Queries.QueryDescription(qn); + return new QueryEntitiesRequest + { + QueryName = qn, + Count = count, + Filters = filters.EmptyIfNull().Select(f => f.ToFilter(qd, canAggregate: false, jsonSerializerOptions)).ToList(), + Orders = orders.EmptyIfNull().Select(f => f.ToOrder(qd, canAggregate: false)).ToList(), + }; + } +} + +public class OrderTS +{ + public string token; + public OrderType orderType; + + public Order ToOrder(QueryDescription qd, bool canAggregate) + { + return new Order(QueryUtils.Parse(this.token, qd, SubTokensOptions.CanElement | (canAggregate ? SubTokensOptions.CanAggregate : 0)), orderType); + } + + public override string ToString() => $"{token} {orderType}"; +} + diff --git a/Signum.Engine/Json/ResultTableConverter.cs b/Signum.Engine/Json/ResultTableConverter.cs new file mode 100644 index 0000000000..8802c9c21f --- /dev/null +++ b/Signum.Engine/Json/ResultTableConverter.cs @@ -0,0 +1,154 @@ +using Signum.Engine.Json; +using Signum.Entities.DynamicQuery; +using Signum.Utilities.Reflection; +using System.Collections; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Signum.Engine.Json; + +public class ResultTableConverter : JsonConverter +{ + public override void Write(Utf8JsonWriter writer, ResultTable value, JsonSerializerOptions options) + { + using (HeavyProfiler.LogNoStackTrace("ReadJson", () => typeof(ResultTable).Name)) + { + var rt = (ResultTable)value!; + + writer.WriteStartObject(); + + writer.WritePropertyName("columns"); + writer.WriteStartArray(); + foreach (var rc in rt.Columns) + { + writer.WriteStringValue(rc.Column.Token.FullKey()); + } + writer.WriteEndArray(); + + Dictionary> uniqueValueIndexes = new Dictionary>(); + + writer.WritePropertyName("uniqueValues"); + writer.WriteStartObject(); + foreach (var rc in rt.Columns) + { + if (rc.CompressUniqueValues) + { + writer.WritePropertyName(rc.Column.Token.FullKey()); + { + var pair = giUniqueValues.GetInvoker(rc.Column.Token.Type)(rc.Values); + + using (EntityJsonContext.SetCurrentPropertyRouteAndEntity((rc.Column.Token.GetPropertyRoute()!, null, null))) + { + JsonSerializer.Serialize(writer, pair.UniqueValues, pair.UniqueValues.GetType(), options); + } + + uniqueValueIndexes.Add(rc, pair.Indexes); + } + } + + } + writer.WriteEndObject(); + + writer.WritePropertyName("pagination"); + JsonSerializer.Serialize(writer, new PaginationTS(rt.Pagination), typeof(PaginationTS), options); + + writer.WritePropertyName("totalElements"); + if (rt.TotalElements == null) + writer.WriteNullValue(); + else + writer.WriteNumberValue(rt.TotalElements!.Value); + + + writer.WritePropertyName("rows"); + writer.WriteStartArray(); + foreach (var row in rt.Rows) + { + writer.WriteStartObject(); + if (rt.EntityColumn != null) + { + writer.WritePropertyName("entity"); + JsonSerializer.Serialize(writer, row.Entity, options); + } + + writer.WritePropertyName("columns"); + writer.WriteStartArray(); + foreach (var column in rt.Columns) + { + if (uniqueValueIndexes.TryGetValue(column, out var indexes)) + { + var ix = indexes[row.Index]; + if (ix != null) + writer.WriteNumberValue(ix.Value); + else + writer.WriteNullValue(); + } + else + { + using (EntityJsonContext.SetCurrentPropertyRouteAndEntity((column.Column.Token.GetPropertyRoute()!, null, null))) + { + JsonSerializer.Serialize(writer, row[column], options); + } + } + } + writer.WriteEndArray(); + + + writer.WriteEndObject(); + + } + writer.WriteEndArray(); + + + writer.WriteEndObject(); + } + } + + + interface IUniqueValuesPair + { + Array UniqueValues { get; } + List Indexes { get; } + } + + class UniqueValuesPair : IUniqueValuesPair + { + public UniqueValuesPair(T[] uniqueValues, List indexes) + { + this.UniqueValues = uniqueValues; + this.Indexes = indexes; + } + + public T[] UniqueValues { get; } + public List Indexes { get; } + + Array IUniqueValuesPair.UniqueValues => UniqueValues; + } + + static GenericInvoker> giUniqueValues = + new(list => UniqueValues((string[])list)); + + static UniqueValuesPair UniqueValues(T[] list) where T : notnull + { + List indexes = new List(list.Length); + Dictionary uniqueDic = new Dictionary(); + foreach (var item in list) + { + int? idx = item == null ? null : uniqueDic.GetOrCreate(item, uniqueDic.Count); + + indexes.Add(idx); + } + + var uniqueValues = new T[uniqueDic.Count]; + foreach (var kvp in uniqueDic) + { + uniqueValues[kvp.Value] = kvp.Key; + } + + return new UniqueValuesPair(uniqueValues, indexes); + } + + public override ResultTable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } +} diff --git a/Signum.Entities.Extensions/Dashboard/CachedQuery.cs b/Signum.Entities.Extensions/Dashboard/CachedQuery.cs new file mode 100644 index 0000000000..1ae3f16b27 --- /dev/null +++ b/Signum.Entities.Extensions/Dashboard/CachedQuery.cs @@ -0,0 +1,38 @@ +using Signum.Entities.Chart; +using Signum.Entities.Files; +using Signum.Entities.UserAssets; +using Signum.Entities.UserQueries; + +namespace Signum.Entities.Dashboard; + +[EntityKind(EntityKind.System, EntityData.Master)] +public class CachedQueryEntity : Entity +{ + public Lite Dashboard { get; set; } + + [PreserveOrder, NoRepeatValidator] + [ImplementedBy(typeof(UserQueryEntity), typeof(UserChartEntity))] + public MList> UserAssets { get; set; } = new MList>(); + + [DefaultFileType(nameof(CachedQueryFileType.CachedQuery), nameof(CachedQueryFileType))] + public FilePathEmbedded File { get; set; } + + public int NumRows { get; set; } + + public int NumColumns { get; set; } + + public DateTime CreationDate { get; internal set; } + + [Unit("ms")] + public long QueryDuration { get; set; } + + [Unit("ms")] + public long UploadDuration { get; set; } +} + +[AutoInit] +public static class CachedQueryFileType +{ + public static FileTypeSymbol CachedQuery; +} + diff --git a/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs b/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs index 647475d816..15baf02471 100644 --- a/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs +++ b/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs @@ -5,6 +5,8 @@ using Signum.Entities.UserAssets; using System.Xml.Linq; using Signum.Entities.Authorization; +using Signum.Entities.DynamicQuery; +using Signum.Entities.UserQueries; namespace Signum.Entities.Dashboard; @@ -41,6 +43,8 @@ public DashboardEntity() public bool CombineSimilarRows { get; set; } = true; + public CacheQueryConfigurationEmbedded? CacheQueryConfiguration { get; set; } + [NotifyCollectionChanged, NotifyChildProperty] [NoRepeatValidator] public MList Parts { get; set; } = new MList(); @@ -119,13 +123,15 @@ public DashboardEntity Clone() { return new DashboardEntity { - DisplayName = "Clone {0}".FormatWith(this.DisplayName), - DashboardPriority = DashboardPriority, - Parts = Parts.Select(p => p.Clone()).ToMList(), - Owner = Owner, EntityType = this.EntityType, EmbeddedInEntity = this.EmbeddedInEntity, + Owner = Owner, + DashboardPriority = DashboardPriority, AutoRefreshPeriod = this.AutoRefreshPeriod, + DisplayName = "Clone {0}".FormatWith(this.DisplayName), + CombineSimilarRows = this.CombineSimilarRows, + CacheQueryConfiguration = this.CacheQueryConfiguration?.Clone(), + Parts = Parts.Select(p => p.Clone()).ToMList(), Key = this.Key }; } @@ -140,6 +146,7 @@ public XElement ToXml(IToXmlContext ctx) DashboardPriority == null ? null! : new XAttribute("DashboardPriority", DashboardPriority.Value.ToString()), EmbeddedInEntity == null ? null! : new XAttribute("EmbeddedInEntity", EmbeddedInEntity.Value.ToString()), new XAttribute("CombineSimilarRows", CombineSimilarRows), + CacheQueryConfiguration?.ToXml(ctx), new XElement("Parts", Parts.Select(p => p.ToXml(ctx)))); } @@ -152,6 +159,7 @@ public void FromXml(XElement element, IFromXmlContext ctx) DashboardPriority = element.Attribute("DashboardPriority")?.Let(a => int.Parse(a.Value)); EmbeddedInEntity = element.Attribute("EmbeddedInEntity")?.Let(a => a.Value.ToEnum()); CombineSimilarRows = element.Attribute("CombineSimilarRows")?.Let(a => bool.Parse(a.Value)) ?? false; + CacheQueryConfiguration = CacheQueryConfiguration.CreateOrAssignEmbedded(element.Element(nameof(CacheQueryConfiguration)), (cqc, elem) => cqc.FromXml(elem)); Parts.Synchronize(element.Element("Parts")!.Elements().ToList(), (pp, x) => pp.FromXml(x, ctx)); } @@ -166,10 +174,40 @@ public void FromXml(XElement element, IFromXmlContext ctx) return ValidationMessage._0IsNotAllowed.NiceToString(pi.NiceName()); } + if(pi.Name == nameof(CacheQueryConfiguration) && CacheQueryConfiguration != null && EntityType != null) + { + return ValidationMessage._0ShouldBeNullWhen1IsSet.NiceToString(pi.NiceName(), NicePropertyName(() => EntityType)); + } + return base.PropertyValidation(pi); } } +public class CacheQueryConfigurationEmbedded : EmbeddedEntity +{ + [Unit("s")] + public int TimeoutForQueries { get; set; } = 5 * 60; + + public int MaxRows { get; set; } = 1000 * 1000; + + internal CacheQueryConfigurationEmbedded Clone() => new CacheQueryConfigurationEmbedded + { + TimeoutForQueries = TimeoutForQueries, + MaxRows = MaxRows, + }; + + internal XElement ToXml(IToXmlContext ctx) => new XElement("CacheQueryConfiguration", + new XAttribute(nameof(TimeoutForQueries), TimeoutForQueries), + new XAttribute(nameof(MaxRows), MaxRows) + ); + + internal void FromXml(XElement elem) + { + TimeoutForQueries = elem.Attribute(nameof(TimeoutForQueries))?.Value.ToInt() ?? 5 * 60; + MaxRows = elem.Attribute(nameof(MaxRows))?.Value.ToInt() ?? 1000 * 1000; + } +} + [AutoInit] public static class DashboardPermission { @@ -180,6 +218,7 @@ public static class DashboardPermission public static class DashboardOperation { public static ExecuteSymbol Save; + public static ExecuteSymbol RegenerateCachedQueries; public static ConstructSymbol.From Clone; public static DeleteSymbol Delete; } @@ -212,3 +251,5 @@ public enum DashboardEmbedededInEntity Bottom, Tab } + + diff --git a/Signum.Entities.Extensions/Dashboard/PanelPart.cs b/Signum.Entities.Extensions/Dashboard/PanelPart.cs index 1d8937cc71..92b870daab 100644 --- a/Signum.Entities.Extensions/Dashboard/PanelPart.cs +++ b/Signum.Entities.Extensions/Dashboard/PanelPart.cs @@ -68,7 +68,9 @@ public PanelPartEmbedded Clone() Title = Title, Row = Row, Style = Style, - + InteractionGroup = InteractionGroup, + IconColor = IconColor, + IconName = IconName, }; } @@ -127,11 +129,14 @@ public interface IPartEntity : IEntity void FromXml(XElement element, IFromXmlContext ctx); } + [EntityKind(EntityKind.Part, EntityData.Master)] public class UserQueryPartEntity : Entity, IPartEntity { public UserQueryEntity UserQuery { get; set; } + public bool IsQueryCached { get; set; } + public UserQueryPartRenderMode RenderMode { get; set; } public bool AllowSelection { get; set; } @@ -157,27 +162,31 @@ public IPartEntity Clone() AllowSelection = this.AllowSelection, ShowFooter = this.ShowFooter, CreateNew = this.CreateNew, + IsQueryCached = this.IsQueryCached, + }; } public XElement ToXml(IToXmlContext ctx) { return new XElement("UserQueryPart", - new XAttribute("UserQuery", ctx.Include(UserQuery)), - new XAttribute("RenderMode", RenderMode.ToString()), - new XAttribute("AllowSelection", AllowSelection.ToString()), - new XAttribute("ShowFooter", ShowFooter.ToString()), - new XAttribute("CreateNew", CreateNew.ToString()) + new XAttribute(nameof(UserQuery), ctx.Include(UserQuery)), + new XAttribute(nameof(RenderMode), RenderMode), + new XAttribute(nameof(AllowSelection), AllowSelection), + ShowFooter ? new XAttribute(nameof(ShowFooter), ShowFooter) : null, + CreateNew ? new XAttribute(nameof(CreateNew), CreateNew) : null, + IsQueryCached ? new XAttribute(nameof(IsQueryCached), IsQueryCached) : null ); } public void FromXml(XElement element, IFromXmlContext ctx) { UserQuery = (UserQueryEntity)ctx.GetEntity(Guid.Parse(element.Attribute("UserQuery")!.Value)); - RenderMode = element.Attribute("RenderMode")?.Value.ToEnum() ?? UserQueryPartRenderMode.SearchControl; - AllowSelection = element.Attribute("AllowSelection")?.Value.ToBool() ?? true; - ShowFooter = element.Attribute("ShowFooter")?.Value.ToBool() ?? false; - CreateNew = element.Attribute("CreateNew")?.Value.ToBool() ?? false; + RenderMode = element.Attribute(nameof(RenderMode))?.Value.ToEnum() ?? UserQueryPartRenderMode.SearchControl; + AllowSelection = element.Attribute(nameof(AllowSelection))?.Value.ToBool() ?? true; + ShowFooter = element.Attribute(nameof(ShowFooter))?.Value.ToBool() ?? false; + CreateNew = element.Attribute(nameof(CreateNew))?.Value.ToBool() ?? false; + IsQueryCached = element.Attribute(nameof(IsQueryCached))?.Value.ToBool() ?? false; } } @@ -213,18 +222,15 @@ public bool RequiresTitle get { return false; } } - public IPartEntity Clone() + public IPartEntity Clone() => new UserTreePartEntity { - return new UserTreePartEntity - { - UserQuery = this.UserQuery, - }; - } + UserQuery = this.UserQuery, + }; public XElement ToXml(IToXmlContext ctx) { return new XElement("UserTreePart", - new XAttribute("UserQuery", ctx.Include(UserQuery)) + new XAttribute(nameof(UserQuery), ctx.Include(UserQuery)) ); } @@ -234,11 +240,15 @@ public void FromXml(XElement element, IFromXmlContext ctx) } } + + [EntityKind(EntityKind.Part, EntityData.Master)] public class UserChartPartEntity : Entity, IPartEntity { public UserChartEntity UserChart { get; set; } + public bool IsQueryCached { get; set; } + public bool ShowData { get; set; } = false; public bool AllowChangeShowData { get; set; } = false; @@ -255,24 +265,26 @@ public bool RequiresTitle get { return false; } } - public IPartEntity Clone() + public IPartEntity Clone() => new UserChartPartEntity { - return new UserChartPartEntity - { - UserChart = this.UserChart, - ShowData = this.ShowData, - AllowChangeShowData = this.AllowChangeShowData, - }; - } + UserChart = this.UserChart, + IsQueryCached = this.IsQueryCached, + ShowData = this.ShowData, + AllowChangeShowData = this.AllowChangeShowData, + CreateNew = this.CreateNew, + AutoRefresh = this.AutoRefresh, + }; public XElement ToXml(IToXmlContext ctx) { return new XElement("UserChartPart", - new XAttribute("ShowData", ShowData), - new XAttribute("AllowChangeShowData", AllowChangeShowData), - CreateNew ? new XAttribute("CreateNew", CreateNew) : null!, - AutoRefresh ? new XAttribute("AutoRefresh", AutoRefresh) : null!, - new XAttribute("UserChart", ctx.Include(UserChart))); + new XAttribute(nameof(UserChart), ctx.Include(UserChart)), + ShowData ? new XAttribute(nameof(ShowData), ShowData) : null, + AllowChangeShowData ? new XAttribute(nameof(AllowChangeShowData), AllowChangeShowData) : null, + IsQueryCached ? new XAttribute(nameof(IsQueryCached), IsQueryCached) : null, + CreateNew ? new XAttribute(nameof(CreateNew), CreateNew) : null!, + AutoRefresh ? new XAttribute(nameof(AutoRefresh), AutoRefresh) : null! + ); } public void FromXml(XElement element, IFromXmlContext ctx) @@ -289,7 +301,7 @@ public void FromXml(XElement element, IFromXmlContext ctx) public class CombinedUserChartPartEntity : Entity, IPartEntity { [PreserveOrder, NoRepeatValidator] - public MList UserCharts { get; set; } = new MList(); + public MList UserCharts { get; set; } = new MList(); public bool ShowData { get; set; } = false; @@ -309,35 +321,55 @@ public bool RequiresTitle get { return true; } } - public IPartEntity Clone() + public IPartEntity Clone() => new CombinedUserChartPartEntity { - return new CombinedUserChartPartEntity - { - UserCharts = this.UserCharts.ToMList(), - }; - } + UserCharts = this.UserCharts.Select(a=>a.Clone()).ToMList(), + ShowData = ShowData, + AllowChangeShowData = AllowChangeShowData, + CombinePinnedFiltersWithSameLabel = CombinePinnedFiltersWithSameLabel, + UseSameScale = UseSameScale + }; public XElement ToXml(IToXmlContext ctx) { return new XElement("CombinedUserChartPart", - new XAttribute("ShowData", ShowData), - new XAttribute("AllowChangeShowData", AllowChangeShowData), - new XAttribute("CombinePinnedFiltersWithSameLabel", CombinePinnedFiltersWithSameLabel), - new XAttribute("UseSameScale", UseSameScale), - UserCharts.Select(uc => new XElement("UserChart", new XAttribute("Guid", ctx.Include(uc))))); + ShowData ? new XAttribute(nameof(ShowData), ShowData) : null, + AllowChangeShowData ? new XAttribute(nameof(AllowChangeShowData), AllowChangeShowData) : null, + CombinePinnedFiltersWithSameLabel ? new XAttribute(nameof(CombinePinnedFiltersWithSameLabel), CombinePinnedFiltersWithSameLabel) : null, + UseSameScale ? new XAttribute(nameof(UseSameScale), UseSameScale) : null, + UserCharts.Select(uc => new XElement(nameof(UserCharts), + new XAttribute("Guid", ctx.Include(uc.UserChart)), + uc.IsQueryCached ? new XAttribute(nameof(uc.IsQueryCached), uc.IsQueryCached) : null)) + ); } public void FromXml(XElement element, IFromXmlContext ctx) { - var newUserCharts = element.Elements("UserChart").Select(uc => (UserChartEntity)ctx.GetEntity(Guid.Parse(uc.Attribute("Guid")!.Value))).ToList(); - ShowData = element.Attribute("ShowData")?.Value.ToBool() ?? false; - AllowChangeShowData = element.Attribute("AllowChangeShowData")?.Value.ToBool() ?? false; - CombinePinnedFiltersWithSameLabel = element.Attribute("CombinePinnedFiltersWithSameLabel")?.Value.ToBool() ?? false; - UseSameScale = element.Attribute("UseSameScale")?.Value.ToBool() ?? false; - UserCharts.Synchronize(newUserCharts); + ShowData = element.Attribute(nameof(ShowData))?.Value.ToBool() ?? false; + AllowChangeShowData = element.Attribute(nameof(AllowChangeShowData))?.Value.ToBool() ?? false; + CombinePinnedFiltersWithSameLabel = element.Attribute(nameof(CombinePinnedFiltersWithSameLabel))?.Value.ToBool() ?? false; + UseSameScale = element.Attribute(nameof(UseSameScale))?.Value.ToBool() ?? false; + UserCharts.Synchronize(element.Elements(nameof(UserCharts)).ToList(), (cuce, elem) => + { + cuce.UserChart = (UserChartEntity)ctx.GetEntity(Guid.Parse(elem.Attribute("Guid")!.Value)); + cuce.IsQueryCached = elem.Attribute(nameof(cuce.IsQueryCached))?.Value.ToBool() ?? true; + }); } } +public class CombinedUserChartElementEmbedded : EmbeddedEntity +{ + public UserChartEntity UserChart { get; set; } + + public bool IsQueryCached { get; set; } + + internal CombinedUserChartElementEmbedded Clone() => new CombinedUserChartElementEmbedded + { + UserChart = UserChart, + IsQueryCached = IsQueryCached, + }; +} + [EntityKind(EntityKind.Part, EntityData.Master)] public class ValueUserQueryListPartEntity : Entity, IPartEntity @@ -354,13 +386,10 @@ public bool RequiresTitle get { return true; } } - public IPartEntity Clone() + public IPartEntity Clone() => new ValueUserQueryListPartEntity { - return new ValueUserQueryListPartEntity - { - UserQueries = this.UserQueries.Select(e => e.Clone()).ToMList(), - }; - } + UserQueries = this.UserQueries.Select(e => e.Clone()).ToMList(), + }; public XElement ToXml(IToXmlContext ctx) { @@ -381,6 +410,8 @@ public class ValueUserQueryElementEmbedded : EmbeddedEntity public UserQueryEntity UserQuery { get; set; } + public bool IsQueryCached { get; set; } + [StringLengthValidator(Max = 200)] public string? Href { get; set; } @@ -391,22 +422,25 @@ public ValueUserQueryElementEmbedded Clone() Href = this.Href, Label = this.Label, UserQuery = UserQuery, + IsQueryCached = this.IsQueryCached, }; } internal XElement ToXml(IToXmlContext ctx) { return new XElement("ValueUserQueryElement", - Label == null ? null! : new XAttribute("Label", Label), - Href == null ? null! : new XAttribute("Href", Href), + Label == null ? null! : new XAttribute(nameof(Label), Label), + Href == null ? null! : new XAttribute(nameof(Href), Href), + IsQueryCached == false? null! : new XAttribute(nameof(IsQueryCached), IsQueryCached), new XAttribute("UserQuery", ctx.Include(UserQuery))); } internal void FromXml(XElement element, IFromXmlContext ctx) { - Label = element.Attribute("Label")?.Value; - Href = element.Attribute("Href")?.Value; - UserQuery = (UserQueryEntity)ctx.GetEntity(Guid.Parse(element.Attribute("UserQuery")!.Value)); + Label = element.Attribute(nameof(Label))?.Value; + Href = element.Attribute(nameof(Href))?.Value; + IsQueryCached = element.Attribute(nameof(IsQueryCached))?.Value.ToBool() ?? false; + UserQuery = (UserQueryEntity)ctx.GetEntity(Guid.Parse(element.Attribute(nameof(UserQuery))!.Value)); } } diff --git a/Signum.Entities/DynamicQuery/Order.cs b/Signum.Entities/DynamicQuery/Order.cs index f01ab42eaa..a81ed5e25d 100644 --- a/Signum.Entities/DynamicQuery/Order.cs +++ b/Signum.Entities/DynamicQuery/Order.cs @@ -1,24 +1,25 @@ - + namespace Signum.Entities.DynamicQuery; -public class Order +public class Order: IEquatable { - QueryToken token; - public QueryToken Token { get { return token; } } - - OrderType orderType; - public OrderType OrderType { get { return orderType; } } + public QueryToken Token { get; } + public OrderType OrderType { get; } public Order(QueryToken token, OrderType orderType) { - this.token = token; - this.orderType = orderType; + this.Token = token; + this.OrderType = orderType; } public override string ToString() { - return "{0} {1}".FormatWith(token.FullKey(), orderType); + return "{0} {1}".FormatWith(Token.FullKey(), OrderType); } + + public override int GetHashCode() => Token.GetHashCode(); + public override bool Equals(object? obj) => obj is Order order && Equals(order); + public bool Equals(Order? other) => other is Order o && o.Token.Equals(Token) && o.OrderType.Equals(OrderType); } [InTypeScript(true), DescriptionOptions(DescriptionOptions.Members | DescriptionOptions.Description)] diff --git a/Signum.Entities/DynamicQuery/Requests.cs b/Signum.Entities/DynamicQuery/Requests.cs index da8b973fbf..63e5904d29 100644 --- a/Signum.Entities/DynamicQuery/Requests.cs +++ b/Signum.Entities/DynamicQuery/Requests.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.ComponentModel; #pragma warning disable CS8618 // Non-nullable field is uninitialized. @@ -9,7 +10,7 @@ public abstract class BaseQueryRequest public List Filters { get; set; } - public string QueryUrl { get; set; } + public string? QueryUrl { get; set; } public override string ToString() { @@ -48,6 +49,17 @@ public List AllTokens() return allTokens; } + + public QueryRequest Clone() => new QueryRequest + { + QueryName = QueryName, + GroupResults = GroupResults, + Columns = Columns, + Filters = Filters, + Orders = Orders, + Pagination = Pagination, + SystemTime = SystemTime, + }; } [DescriptionOptions(DescriptionOptions.Members | DescriptionOptions.Description), InTypeScript(true)] @@ -91,28 +103,19 @@ public enum SystemTimeProperty SystemValidTo, } -public abstract class Pagination +public abstract class Pagination : IEquatable { public abstract PaginationMode GetMode(); public abstract int? GetElementsPerPage(); public abstract int? MaxElementIndex { get; } + public abstract bool Equals(Pagination? other); public class All : Pagination { - public override int? MaxElementIndex - { - get { return null; } - } - - public override PaginationMode GetMode() - { - return PaginationMode.All; - } - - public override int? GetElementsPerPage() - { - return null; - } + public override PaginationMode GetMode() => PaginationMode.All; + public override int? GetElementsPerPage() => null; + public override int? MaxElementIndex => null; + public override bool Equals(Pagination? other) => other is All; } public class Firsts : Pagination @@ -124,28 +127,16 @@ public Firsts(int topElements) this.TopElements = topElements; } - public int TopElements { get; private set; } - - public override int? MaxElementIndex - { - get { return TopElements; } - } - - public override PaginationMode GetMode() - { - return PaginationMode.Firsts; - } + public int TopElements { get; } - public override int? GetElementsPerPage() - { - return TopElements; - } + public override PaginationMode GetMode() => PaginationMode.Firsts; + public override int? GetElementsPerPage() => TopElements; + public override int? MaxElementIndex => TopElements; + public override bool Equals(Pagination? other) => other is Firsts f && f.TopElements == this.TopElements; } public class Paginate : Pagination { - public static int DefaultElementsPerPage = 20; - public Paginate(int elementsPerPage, int currentPage = 1) { if (elementsPerPage <= 0) @@ -159,43 +150,17 @@ public Paginate(int elementsPerPage, int currentPage = 1) } public int ElementsPerPage { get; private set; } - public int CurrentPage { get; private set; } - public int StartElementIndex() - { - return (ElementsPerPage * (CurrentPage - 1)) + 1; - } - - public int EndElementIndex(int rows) - { - return StartElementIndex() + rows - 1; - } - - public int TotalPages(int totalElements) - { - return (totalElements + ElementsPerPage - 1) / ElementsPerPage; //Round up - } - - public override int? MaxElementIndex - { - get { return (ElementsPerPage * (CurrentPage + 1)) - 1; } - } + public int StartElementIndex() => (ElementsPerPage * (CurrentPage - 1)) + 1; + public int EndElementIndex(int rows) => StartElementIndex() + rows - 1; + public int TotalPages(int totalElements) => (totalElements + ElementsPerPage - 1) / ElementsPerPage; //Round up + public Paginate WithCurrentPage(int newPage) => new Paginate(this.ElementsPerPage, newPage); - public override PaginationMode GetMode() - { - return PaginationMode.Paginate; - } - - public override int? GetElementsPerPage() - { - return ElementsPerPage; - } - - public Paginate WithCurrentPage(int newPage) - { - return new Paginate(this.ElementsPerPage, newPage); - } + public override PaginationMode GetMode() => PaginationMode.Paginate; + public override int? GetElementsPerPage() => ElementsPerPage; + public override int? MaxElementIndex => (ElementsPerPage * (CurrentPage + 1)) - 1; + public override bool Equals(Pagination? other) => other is Paginate p && p.ElementsPerPage == ElementsPerPage && p.CurrentPage == CurrentPage; } } @@ -277,37 +242,3 @@ public List Multiplications public override string ToString() => QueryName.ToString()!; } - -public class DQueryableRequest : BaseQueryRequest -{ - List orders = new List(); - public List Orders - { - get { return orders; } - set { orders = value; } - } - - List columns = new List(); - public List Columns - { - get { return columns; } - set { columns = value; } - } - - public List Multiplications - { - get - { - HashSet allTokens = - Filters.SelectMany(a => a.GetFilterConditions()).Select(a => a.Token) - .Concat(Columns.Select(a => a.Token)) - .Concat(Orders.Select(a => a.Token)).ToHashSet(); - - return CollectionElementToken.GetElements(allTokens); - } - } - - public int? Count { get; set; } - - public override string ToString() => QueryName.ToString()!; -} diff --git a/Signum.Entities/DynamicQuery/ResultTable.cs b/Signum.Entities/DynamicQuery/ResultTable.cs index 7eb873cc48..f7f0adf411 100644 --- a/Signum.Entities/DynamicQuery/ResultTable.cs +++ b/Signum.Entities/DynamicQuery/ResultTable.cs @@ -23,6 +23,8 @@ public int Index IList values; public IList Values => values; + public bool CompressUniqueValues { get; set; } + public ResultColumn(Column column, IList values) { this.column = column; @@ -229,6 +231,8 @@ public bool HasEntities ResultRow[] rows; public ResultRow[] Rows { get { return rows; } } + public ResultColumn[] AllColumns() => entityColumn == null ? Columns : Columns.PreAnd(entityColumn).ToArray(); + public ResultTable(ResultColumn[] columns, int? totalElements, Pagination pagination) { this.entityColumn = columns.Where(c => c.Column is _EntityColumn).SingleOrDefaultEx(); @@ -242,13 +246,6 @@ public ResultTable(ResultColumn[] columns, int? totalElements, Pagination pagina this.totalElements = totalElements; this.pagination = pagination; } - - //[OnDeserialized] - //private void OnDeserialized(StreamingContext context) - //{ - // CreateIndices(columns); - //} - public DataTable ToDataTable(DataTableValueConverter? converter = null) { diff --git a/Signum.React.Extensions/Chart/ChartClient.tsx b/Signum.React.Extensions/Chart/ChartClient.tsx index bd6a25d2bd..eff234a245 100644 --- a/Signum.React.Extensions/Chart/ChartClient.tsx +++ b/Signum.React.Extensions/Chart/ChartClient.tsx @@ -792,24 +792,28 @@ export module API { export function executeChart(request: ChartRequestModel, chartScript: ChartScript, abortSignal?: AbortSignal): Promise { - return Navigator.API.validateEntity(cleanedChartRequest(request)).then(cr => { - const queryRequest = getRequest(request); - - var allTypes = request.columns - .map(c => c.element.token) - .notNull() - .map(a => a.token && a.token.type.name) - .notNull() - .flatMap(a => tryGetTypeInfos(a)) - .notNull() - .distinctBy(a => a.name); - - var palettesPromise = Promise.all(allTypes.map(ti => ChartPaletteClient.getColorPalette(ti).then(cp => ({ type: ti.name, palette: cp })))) - .then(list => list.toObject(a => a.type, a => a.palette)); - - return Finder.API.executeQuery(queryRequest, abortSignal) - .then(rt => palettesPromise.then(palettes => toChartResult(request, rt, chartScript, palettes))); - }); + + var palettesPromise = getPalletes(request); + + const queryRequest = getRequest(request); + return Finder.API.executeQuery(queryRequest, abortSignal) + .then(rt => palettesPromise.then(palettes => toChartResult(request, rt, chartScript, palettes))); + } + + export function getPalletes(request: ChartRequestModel): Promise<{ [type: string]: ChartPaletteClient.ColorPalette | null }> { + var allTypes = request.columns + .map(c => c.element.token) + .notNull() + .map(a => a.token && a.token.type.name) + .notNull() + .flatMap(a => tryGetTypeInfos(a)) + .notNull() + .distinctBy(a => a.name); + + var palettesPromise = Promise.all(allTypes.map(ti => ChartPaletteClient.getColorPalette(ti).then(cp => ({ type: ti.name, palette: cp })))) + .then(list => list.toObject(a => a.type, a => a.palette)); + + return palettesPromise; } export interface ExecuteChartResult { diff --git a/Signum.React.Extensions/Chart/ChartPalette/ChartPaletteClient.tsx b/Signum.React.Extensions/Chart/ChartPalette/ChartPaletteClient.tsx index ee7f00d217..911b5534b7 100644 --- a/Signum.React.Extensions/Chart/ChartPalette/ChartPaletteClient.tsx +++ b/Signum.React.Extensions/Chart/ChartPalette/ChartPaletteClient.tsx @@ -50,10 +50,11 @@ export function getColorPalette(type: PseudoType): Promise const typeName = getTypeName(type); - if (colorPalette[typeName]) + if (colorPalette[typeName] !== undefined) return Promise.resolve(colorPalette[typeName]); - return API.fetchColorPalette(typeName, false).then(cs => colorPalette[typeName] = cs && toColorPalete(cs)); + return API.fetchColorPalette(typeName, false) + .then(cs => colorPalette[typeName] = (cs && toColorPalete(cs)) ?? null); } export function toColorPalete(model: ChartPaletteModel): ColorPalette { diff --git a/Signum.React.Extensions/Chart/ChartServer.cs b/Signum.React.Extensions/Chart/ChartServer.cs index 209382593e..278ef8377f 100644 --- a/Signum.React.Extensions/Chart/ChartServer.cs +++ b/Signum.React.Extensions/Chart/ChartServer.cs @@ -95,7 +95,7 @@ private static void CustomizeChartRequest() var qd = QueryLogic.Queries.QueryDescription(cr.QueryName); - cr.Filters = list.Select(l => l.ToFilter(qd, canAggregate: true)).ToList(); + cr.Filters = list.Select(l => l.ToFilter(qd, canAggregate: true, SignumServer.JsonSerializerOptions)).ToList(); }, CustomWriteJsonProperty = (Utf8JsonWriter writer, WriteJsonPropertyContext ctx) => { diff --git a/Signum.React.Extensions/Chart/Templates/ChartRequestView.tsx b/Signum.React.Extensions/Chart/Templates/ChartRequestView.tsx index 2adcb5eb7e..0d4e3c6447 100644 --- a/Signum.React.Extensions/Chart/Templates/ChartRequestView.tsx +++ b/Signum.React.Extensions/Chart/Templates/ChartRequestView.tsx @@ -20,7 +20,7 @@ import ChartTableComponent from './ChartTable' import ChartRenderer from './ChartRenderer' import "@framework/SearchControl/Search.css" import "../Chart.css" -import { ChartScript } from '../ChartClient'; +import { ChartScript, cleanedChartRequest } from '../ChartClient'; import { useForceUpdate, useAPI } from '@framework/Hooks' import { AutoFocus } from '@framework/Components/AutoFocus'; import PinnedFilterBuilder from '@framework/SearchControl/PinnedFilterBuilder'; @@ -61,7 +61,9 @@ export default function ChartRequestView(p: ChartRequestViewProps) { const queryDescription = useAPI(signal => p.chartRequest ? Finder.getQueryDescription(p.chartRequest.queryKey) : Promise.resolve(undefined), [p.chartRequest.queryKey]); - const abortableQuery = React.useRef(new AbortableRequest<{ cr: ChartRequestModel; cs: ChartScript }, ChartClient.API.ExecuteChartResult>((signal, request) => ChartClient.API.executeChart(request.cr, request.cs, signal))); + const abortableQuery = React.useRef(new AbortableRequest<{ cr: ChartRequestModel; cs: ChartScript }, ChartClient.API.ExecuteChartResult>( + (signal, request) => Navigator.API.validateEntity(cleanedChartRequest(request.cr)).then(() => ChartClient.API.executeChart(request.cr, request.cs, signal)))); + React.useEffect(() => { if (p.searchOnLoad) handleOnDrawClick(); diff --git a/Signum.React.Extensions/Chart/UserChart/UserChartClient.tsx b/Signum.React.Extensions/Chart/UserChart/UserChartClient.tsx index 30e65b1e19..a03b96bdb3 100644 --- a/Signum.React.Extensions/Chart/UserChart/UserChartClient.tsx +++ b/Signum.React.Extensions/Chart/UserChart/UserChartClient.tsx @@ -60,7 +60,7 @@ export function start(options: { routes: JSX.Element[] }) { }) .done(); }).done(); - }, { isVisible: AuthClient.isPermissionAuthorized(ChartPermission.ViewCharting) })); + }, { isVisible: AuthClient.isPermissionAuthorized(ChartPermission.ViewCharting), group: null, icon: "eye", iconColor: "blue", color: "info" })); Navigator.addSettings(new EntitySettings(UserChartEntity, e => import('./UserChart'), { isCreable: "Never" })); diff --git a/Signum.React.Extensions/Dashboard/Admin/CachedQuery.tsx b/Signum.React.Extensions/Dashboard/Admin/CachedQuery.tsx new file mode 100644 index 0000000000..ea964ab8f5 --- /dev/null +++ b/Signum.React.Extensions/Dashboard/Admin/CachedQuery.tsx @@ -0,0 +1,38 @@ + +import * as React from 'react' +import { ValueLine, EntityLine, EntityRepeater, EntityTable, EntityStrip } from '@framework/Lines' +import { TypeContext } from '@framework/TypeContext' +import { ValueUserQueryListPartEntity, ValueUserQueryElementEmbedded, DashboardEntity, CachedQueryEntity } from '../Signum.Entities.Dashboard' +import { IsQueryCachedLine } from './Dashboard'; +import * as FilesClient from '../../Files/FilesClient'; +import { downloadFile } from '../../Files/FileDownloader'; +import * as Services from '../../../Signum.React/Scripts/Services'; +import { useAPI } from '../../../Signum.React/Scripts/Hooks'; +import { FormatJson } from '../../../Signum.React/Scripts/Exceptions/Exception'; +import { FileLine } from '../../Files/FileLine'; +import { JavascriptMessage } from '@framework/Signum.Entities'; + +export default function CachedQueryView(p: { ctx: TypeContext }) { + + const ctx = p.ctx; + + const text = useAPI(() => downloadFile(p.ctx.value.file).then(res => res.text()), [p.ctx.value.file]); + + return ( +
+ a.dashboard)} /> + a.userAssets)} /> + a.creationDate)} /> +
+
+ a.queryDuration)} labelColumns={4}/> +
+
+ a.queryDuration)} labelColumns={4}/> +
+
+ a.file)} /> + {text == null ? JavascriptMessage.loading.niceToString() : } +
+ ); +} diff --git a/Signum.React.Extensions/Dashboard/Admin/CombinedUserChartPart.tsx b/Signum.React.Extensions/Dashboard/Admin/CombinedUserChartPart.tsx index c2da5c9349..95bb7cf76d 100644 --- a/Signum.React.Extensions/Dashboard/Admin/CombinedUserChartPart.tsx +++ b/Signum.React.Extensions/Dashboard/Admin/CombinedUserChartPart.tsx @@ -1,22 +1,34 @@ import * as React from 'react' -import { ValueLine, EntityLine, EntityStrip } from '@framework/Lines' +import { ValueLine, EntityLine, EntityStrip, EntityTable } from '@framework/Lines' import { TypeContext } from '@framework/TypeContext' -import { UserChartPartEntity, DashboardEntity, CombinedUserChartPartEntity } from '../Signum.Entities.Dashboard' +import { UserChartPartEntity, DashboardEntity, CombinedUserChartPartEntity, CombinedUserChartElementEmbedded } from '../Signum.Entities.Dashboard' import { D3ChartScript, UserChartEntity } from '../../Chart/Signum.Entities.Chart'; +import { IsQueryCachedLine } from './Dashboard'; export default function CombinedUserChartPart(p: { ctx: TypeContext }) { const ctx = p.ctx; return (
- p.userCharts)} findOptions={{ - queryName: UserChartEntity, filterOptions: [{ - token: UserChartEntity.token(a => a.entity.chartScript.key), - operation: "IsIn", - value: [D3ChartScript.Columns.key, D3ChartScript.Line.key] - }] - }} /> - + p.userCharts)} columns={EntityTable.typedColumns([ + { + property: p => p.userChart, + template: (ectx) => p.userChart)} findOptions={{ + queryName: UserChartEntity, filterOptions: [{ + token: UserChartEntity.token(a => a.entity.chartScript.key), + operation: "IsIn", + value: [D3ChartScript.Columns.key, D3ChartScript.Line.key] + }] + }}/>, + headerHtmlAttributes: { style: { width: "70%" } }, + }, + ctx.findParentCtx(DashboardEntity).value.cacheQueryConfiguration && { + property: p => p.isQueryCached, + headerHtmlAttributes: { style: { width: "30%" } }, + template: ectx => p.isQueryCached)} />, + }, + ])} + /> p.showData)} inlineCheckbox="block" /> p.allowChangeShowData)} inlineCheckbox="block" /> p.combinePinnedFiltersWithSameLabel)} inlineCheckbox="block" /> diff --git a/Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx b/Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx index de5ddc7610..c5a4a60097 100644 --- a/Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx +++ b/Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx @@ -1,11 +1,11 @@ import * as React from 'react' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { ValueLine, EntityLine, RenderEntity, OptionItem } from '@framework/Lines' +import { ValueLine, EntityLine, RenderEntity, OptionItem, EntityDetail } from '@framework/Lines' import { tryGetTypeInfos, New, getTypeInfos } from '@framework/Reflection' import SelectorModal from '@framework/SelectorModal' import { TypeContext } from '@framework/TypeContext' -import { DashboardEntity, PanelPartEmbedded, IPartEntity, InteractionGroup } from '../Signum.Entities.Dashboard' +import { DashboardEntity, PanelPartEmbedded, IPartEntity, InteractionGroup, CacheQueryConfigurationEmbedded, CachedQueryEntity, DashboardOperation } from '../Signum.Entities.Dashboard' import { EntityGridRepeater, EntityGridItem } from './EntityGridRepeater' import * as DashboardClient from "../DashboardClient"; import { iconToString, IconTypeaheadLine, parseIcon } from "../../Basics/Templates/IconTypeahead"; @@ -13,8 +13,13 @@ import { ColorTypeaheadLine } from "../../Basics/Templates/ColorTypeahead"; import "../Dashboard.css" import { getToString } from '@framework/Signum.Entities'; import { useForceUpdate } from '@framework/Hooks' +import { ValueSearchControlLine } from '../../../Signum.React/Scripts/Search'; +import { withClassName } from '../../Dynamic/View/HtmlAttributesExpression'; +import { classes } from '../../../Signum.React/Scripts/Globals'; +import { OperationButton } from '../../../Signum.React/Scripts/Operations/EntityOperations'; +import { EntityOperationContext } from '../../../Signum.React/Scripts/Operations'; -export default function Dashboard(p : { ctx: TypeContext }){ +export default function Dashboard(p: { ctx: TypeContext }) { const forceUpdate = useForceUpdate(); function handleEntityTypeChange() { if (!p.ctx.value.entityType) @@ -91,44 +96,67 @@ export default function Dashboard(p : { ctx: TypeContext }){ return ( - a.content)} /> + a.content)} extraProps={{ dashboard: ctx.value }} /> ); } const ctx = p.ctx; - const sc = ctx.subCtx({ formGroupStyle: "Basic" }); + const ctxBasic = ctx.subCtx({ formGroupStyle: "Basic" }); return (
- cp.displayName)} /> + cp.displayName)} />
- cp.dashboardPriority)} /> + cp.dashboardPriority)} />
- cp.autoRefreshPeriod)} /> + cp.autoRefreshPeriod)} />
- cp.owner)} create={false} /> + cp.owner)} create={false} />
- cp.entityType)} onChange={handleEntityTypeChange} /> + cp.entityType)} onChange={handleEntityTypeChange} />
- {sc.value.entityType &&
- f.embeddedInEntity)} /> + {ctxBasic.value.entityType &&
+ f.embeddedInEntity)} />
}
- cp.combineSimilarRows)} inlineCheckbox={true} /> + + cp.cacheQueryConfiguration)} + onChange={forceUpdate} + onCreate={() => Promise.resolve(CacheQueryConfigurationEmbedded.New({ timeoutForQueries: 5 * 60, maxRows: 1000 * 1000 }))} + getComponent={(ectx: TypeContext) =>
+
+ cp.timeoutForQueries)} /> +
+
+ cp.maxRows)} /> +
+
+ {!ctx.value.isNew && a.dashboard), value: ctxBasic.value }] }} />} +
+
+ {!ctx.value.isNew && } +
+
} /> + + cp.combineSimilarRows)} inlineCheckbox={true} />
- cp.parts)} getComponent={renderPart} onCreate={handleOnCreate} /> -
+ cp.parts)} getComponent={renderPart} onCreate={handleOnCreate} /> +
); } +export function IsQueryCachedLine(p: { ctx: TypeContext }) { + const forceUpate = useForceUpdate(); + return {p.ctx.niceName()}} inlineCheckbox="block" onChange={forceUpate} /> +} diff --git a/Signum.React.Extensions/Dashboard/Admin/LinkListPart.tsx b/Signum.React.Extensions/Dashboard/Admin/LinkListPart.tsx index 2c3494027b..a7755752ae 100644 --- a/Signum.React.Extensions/Dashboard/Admin/LinkListPart.tsx +++ b/Signum.React.Extensions/Dashboard/Admin/LinkListPart.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { ValueLine, EntityRepeater } from '@framework/Lines' +import { ValueLine, EntityRepeater, EntityTable } from '@framework/Lines' import { TypeContext } from '@framework/TypeContext' import { LinkListPartEntity, LinkElementEmbedded } from '../Signum.Entities.Dashboard' @@ -9,15 +9,7 @@ export default function ValueSearchControlPart(p : { ctx: TypeContext - p.links)} getComponent={(tc: TypeContext) => { - return ( -
- cuq.label)} /> -   - cuq.link)} /> -
- ); - }} /> + p.links)} />
); } diff --git a/Signum.React.Extensions/Dashboard/Admin/UserChartPart.tsx b/Signum.React.Extensions/Dashboard/Admin/UserChartPart.tsx index 14cc5760e9..834c92c8b7 100644 --- a/Signum.React.Extensions/Dashboard/Admin/UserChartPart.tsx +++ b/Signum.React.Extensions/Dashboard/Admin/UserChartPart.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { ValueLine, EntityLine } from '@framework/Lines' import { TypeContext } from '@framework/TypeContext' import { UserChartPartEntity, DashboardEntity } from '../Signum.Entities.Dashboard' +import { IsQueryCachedLine } from './Dashboard'; export default function UserChartPart(p: { ctx: TypeContext }) { const ctx = p.ctx; @@ -13,6 +14,9 @@ export default function UserChartPart(p: { ctx: TypeContext p.allowChangeShowData)} inlineCheckbox="block" /> p.createNew)} inlineCheckbox="block" /> p.autoRefresh)} inlineCheckbox="block" /> + {ctx.findParentCtx(DashboardEntity).value.cacheQueryConfiguration && p.isQueryCached)} />} ); } + + diff --git a/Signum.React.Extensions/Dashboard/Admin/UserQueryPart.tsx b/Signum.React.Extensions/Dashboard/Admin/UserQueryPart.tsx index 31808411d8..77163863bb 100644 --- a/Signum.React.Extensions/Dashboard/Admin/UserQueryPart.tsx +++ b/Signum.React.Extensions/Dashboard/Admin/UserQueryPart.tsx @@ -3,6 +3,7 @@ import { ValueLine, EntityLine } from '@framework/Lines' import { TypeContext } from '@framework/TypeContext' import { UserQueryPartEntity, DashboardEntity } from '../Signum.Entities.Dashboard' import { useForceUpdate } from '@framework/Hooks'; +import { IsQueryCachedLine } from './Dashboard'; export default function UserQueryPart(p: { ctx: TypeContext }) { const ctx = p.ctx.subCtx({ formGroupStyle: p.ctx.value.renderMode == "BigValue" ? "Basic" : undefined }); @@ -18,6 +19,7 @@ export default function UserQueryPart(p: { ctx: TypeContext p.createNew)} inlineCheckbox="block" /> } + {ctx.findParentCtx(DashboardEntity).value.cacheQueryConfiguration && p.isQueryCached)} />} ); } diff --git a/Signum.React.Extensions/Dashboard/Admin/ValueUserQueryListPart.tsx b/Signum.React.Extensions/Dashboard/Admin/ValueUserQueryListPart.tsx index 0b3f23b2e6..78a028a0c3 100644 --- a/Signum.React.Extensions/Dashboard/Admin/ValueUserQueryListPart.tsx +++ b/Signum.React.Extensions/Dashboard/Admin/ValueUserQueryListPart.tsx @@ -2,13 +2,33 @@ import * as React from 'react' import { ValueLine, EntityLine, EntityRepeater, EntityTable } from '@framework/Lines' import { TypeContext } from '@framework/TypeContext' -import { ValueUserQueryListPartEntity, ValueUserQueryElementEmbedded } from '../Signum.Entities.Dashboard' +import { ValueUserQueryListPartEntity, ValueUserQueryElementEmbedded, DashboardEntity } from '../Signum.Entities.Dashboard' +import { IsQueryCachedLine } from './Dashboard'; export default function ValueUserQueryListPart(p : { ctx: TypeContext }){ const ctx = p.ctx; + const db = ctx.findParentCtx(DashboardEntity).value.cacheQueryConfiguration; return ( - p.userQueries)} /> +
+ p.userQueries)} columns={EntityTable.typedColumns([ + { + property: p => p.userQuery, + headerHtmlAttributes: { style: { width: "35%" } }, + }, + { + property: p => p.label, + }, + { + property: p => p.href, + }, + db && { + property: p => p.isQueryCached, + template: rctx => db && p.isQueryCached)} /> + } + ])} + /> +
); } diff --git a/Signum.React.Extensions/Dashboard/CachedQueryExecutor.ts b/Signum.React.Extensions/Dashboard/CachedQueryExecutor.ts new file mode 100644 index 0000000000..cf587720be --- /dev/null +++ b/Signum.React.Extensions/Dashboard/CachedQueryExecutor.ts @@ -0,0 +1,661 @@ +import * as Finder from '@framework/Finder' +import { ColumnRequest, FilterOperation, FilterOptionParsed, FilterRequest, FindOptionsParsed, isFilterGroupOptionParsed, isFilterGroupRequest, OrderRequest, Pagination, QueryRequest, QueryToken, QueryValueRequest, ResultRow, ResultTable } from '@framework/FindOptions' +import { Entity, is, Lite } from '@framework/Signum.Entities'; +import { useFetchAll } from '../../Signum.React/Scripts/Navigator'; +import { ignoreErrors } from '../../Signum.React/Scripts/QuickLinks'; +import * as ChartClient from '../Chart/ChartClient' +import { ChartRequestModel } from '../Chart/Signum.Entities.Chart'; + + +export interface CachedQueryJS { + creationDate: string; + queryRequest: QueryRequest; + resultTable: ResultTable; +} + + + +export function executeQueryCached(request: QueryRequest, fop: FindOptionsParsed, cachedQuery: CachedQueryJS): ResultTable { + + const tokens = [ + ...fop.columnOptions.map(a => a.token).notNull(), + ...fop.columnOptions.map(a => a.summaryToken).notNull(), + ...fop.orderOptions.map(a => a.token), + ...getAllFilterTokens(fop.filterOptions), + ].notNull().toObjectDistinct(a => a.fullKey); + + const resultTable = getCachedResultTable(cachedQuery, request, tokens); + + return resultTable; +} + +export function executeQueryValueCached(request: QueryValueRequest, fop: FindOptionsParsed, token: QueryToken | undefined, cachedQuery: CachedQueryJS): unknown { + + if (token == null) + token = { + fullKey: "Count", + type: { name: "number" }, + queryTokenType: "Aggregate", + niceName: "Count", + key: "Count", + toStr: "Count", + niceTypeName: "Number", + typeColor: "", + isGroupable: false, + filterType: "Integer", + }; + + var queryRequest: QueryRequest = { + queryKey: request.queryKey, + columns: [{ token: token.fullKey, displayName: token.niceName }], + filters: request.filters, + groupResults: token.queryTokenType == "Aggregate", + orders: [], + pagination: request.multipleValues ? { mode: "All" } : { mode: "Firsts", elementsPerPage: 2 }, + systemTime: undefined, + }; + + const tokens = [ + token, + ...getAllFilterTokens(fop.filterOptions), + ].notNull().toObjectDistinct(a => a.fullKey); + + const resultTable = getCachedResultTable(cachedQuery, queryRequest, tokens); + + if (request.multipleValues) + return resultTable.rows.map(r => r.columns[0]); + + return resultTable.rows.map(r => r.columns[0]).singleOrNull(); +} + +export function executeChartCached(request: ChartRequestModel, chartScript: ChartClient.ChartScript, cachedQuery: CachedQueryJS): Promise { + const palettesPromise = ChartClient.API.getPalletes(request); + + const tokens = [ + ...request.columns.map(a => a.element.token?.token).notNull(), + ...getAllFilterTokens(request.filterOptions), + ].toObjectDistinct(a => a.fullKey); + + const queryRequest = ChartClient.API.getRequest(request); + const resultTable = getCachedResultTable(cachedQuery, queryRequest, tokens); + + return palettesPromise.then(palettes => ChartClient.API.toChartResult(request, resultTable, chartScript, palettes)); +} + +function getAllFilterTokens(fos: FilterOptionParsed[]): QueryToken[]{ + return fos.flatMap(f => isFilterGroupOptionParsed(f) ? + [f.token, ...getAllFilterTokens(f.filters)] : + [f.token]) + .notNull(); +} + + +class CachedQueryError { + message: string; + constructor(error: string) { + this.message = error; + } + + toString() { + return this.message; + } +} + +export function getCachedResultTable(cachedQuery: CachedQueryJS, request: QueryRequest, parsedTokens: { [token: string]: QueryToken }): ResultTable { + + if (request.queryKey != cachedQuery.queryRequest.queryKey) + throw new CachedQueryError("Invalid queryKey"); + + var pagProblems = pagionationRestriction(request.pagination, cachedQuery.queryRequest.pagination); + + const exactFiltersAndOrders = pagProblems == "ExactFiltersAndOrders"; + + const sameOrders = ordersEquals(cachedQuery.queryRequest.orders, request.orders) + if (!sameOrders && exactFiltersAndOrders) + throw new CachedQueryError("Incompatible pagination if the orders are not identical"); + + const extraFilters = extractRequestedFilters(cachedQuery.queryRequest.filters, request.filters); + if (extraFilters.length && exactFiltersAndOrders) + throw new CachedQueryError("Incompatible pagination if the filters are not identical"); + + if (request.groupResults) { + + if (exactFiltersAndOrders) { + + if (!cachedQuery.queryRequest.groupResults) + throw new CachedQueryError("Incompatible pagination if the request is grouping but the cached query is not"); + else { + + const requestKeyColumns = request.columns.map(a => parsedTokens[a.token].queryTokenType != "Aggregate"); + + const cachedKeyColumns = cachedQuery.queryRequest.columns.map(a => parsedTokens[a.token].queryTokenType != "Aggregate"); + + var extraColumns = cachedKeyColumns.filter(c => !requestKeyColumns.contains(c)); + if (extraColumns.length && exactFiltersAndOrders) + throw new CachedQueryError("Incompatible pagination if the key columns are not identical"); + } + } + + const aggregateFilters = extraFilters.extract(f => !isFilterGroupRequest(f) && parsedTokens[f.token].queryTokenType == "Aggregate"); + + const filtered = filterRows(cachedQuery.resultTable, extraFilters); + + const allColumns = [...request.columns.map(a => a.token), ...aggregateFilters.map(a => a.token!), ...sameOrders ? [] : request.orders.map(a => a.token)].distinctBy(a => a); + + const grouped = groupByRows(filtered, true, allColumns, parsedTokens); + + const reFiltered = filterRows(grouped, aggregateFilters); + + const ordered = sameOrders ? reFiltered : orderRows(reFiltered, request.orders, parsedTokens); + + const select = selectRows(ordered, request.columns); + + const paginate = paginateRows(select, request.pagination); + + return paginate; + + } else { + if (cachedQuery.queryRequest.groupResults) + throw new CachedQueryError("Cached query is grouping but request is not"); + else { + const filtered = filterRows(cachedQuery.resultTable, extraFilters); + + const ordered = sameOrders ? filtered : orderRows(filtered, request.orders, parsedTokens); + + const select = selectRows(ordered, request.columns); + + const paginate = paginateRows(select, request.pagination); + return paginate; + } + } +} + + +function groupByRows(rt: ResultTable, alreadyGrouped: boolean, tokens: string[], parsedTokens: { [token: string]: QueryToken }): ResultTable { + + const groups = new Map(); + const keyColumns = tokens.filter(a => parsedTokens[a].queryTokenType != "Aggregate"); + const rowKey = getRowKey(rt, keyColumns, parsedTokens); + + for (var i = 0; i < rt.rows.length; i++) { + + const row = rt.rows[i]; + const key = rowKey(row); + let array = groups.get(key); + if (!array) { + array = []; + groups.set(key, array); + } + array.push(row); + } + + var result: ResultRow[]; + + + function getGetter(token: string): ((gr: ResultRow[]) => any) { + + function getColumnIndex(t: string) { + + var idx = rt.columns.indexOf(t); + if (idx == -1) + throw new CachedQueryError(`Column ${t} not found` + (t != token ? ` (required for ${token})` : "")); + + return idx; + } + + function tryColumnIndex(t: string) { + + var idx = rt.columns.indexOf(t); + if (idx == -1) + return null; + + return idx; + } + + const qt = parsedTokens[token]; + if (qt.queryTokenType != "Aggregate") { + const index = rt.columns.indexOf(token); + return gr => gr[0].columns[index]; + } + else { + + if (!alreadyGrouped) { + if (qt.key == "Count") + return gr => gr.length; + + const index = getColumnIndex(qt.parent!.fullKey); + + switch (qt.key) { + case "Min": return rows => rows.map(a => a.columns[index]).min(); + case "Max": return rows => rows.map(a => a.columns[index]).max(); + case "Sum": return rows => rows.map(a => a.columns[index]).sum(); + case "Avg": return rows => { + var vals = rows.map(a => a.columns[index]).notNull(); + return vals.sum() / vals.length; + }; + } + } else { + + if (qt.key == "Count") { + const indexCount = getColumnIndex(qt.fullKey); + + return rows => rows.sum(a => a.columns[indexCount]); + + } else if (qt.key == "Average") { + var avg = tryColumnIndex(qt.fullKey); + if (avg != null) { //No interaction group + return rows => rows.single().columns[avg!]; + } + + const sumToken = qt.parent!.fullKey + ".Sum"; + const indexSum = getColumnIndex(sumToken); + + const countToken = qt.parent!.fullKey + ".CountNotNull"; + const indexCount2 = getColumnIndex(countToken); + return rows => rows.sum(a => a.columns[indexSum]) / rows.sum(a => a.columns[indexCount2]); + } else { + const index = tryColumnIndex(qt.fullKey) ?? getColumnIndex(qt.parent!.fullKey); + + switch (qt.key) { + case "Min": return rows => rows.map(a => a.columns[index]).min(); + case "Max": return rows => rows.map(a => a.columns[index]).max(); + case "Sum": return rows => rows.map(a => a.columns[index]).sum(); + } + } + } + } + + throw new Error("Unexpected " + token); + } + + var getters = tokens.map(t => { + var g = getGetter(t); + + + return g; + }); + + const newRows: ResultRow[] = []; + groups.forEach(rows => { + + var columns: any[] = []; + for (var i = 0; i < getters.length; i++) { + columns.push(getters[i](rows)) + } + + newRows.push({ + entity: undefined, + columns: columns + }); + }) + + return ({ + columns: tokens, + pagination: { mode: "All" }, + rows: newRows, + uniqueValues: rt.uniqueValues, + totalElements: newRows.length + }); +} + +function getRowKey(rt: ResultTable, keyTokens: string[], parsedTokens: { [token: string]: QueryToken }): (row: ResultRow) => string { + + const rr = "rr"; + + function columnKey(token: string) { + const index = rt.columns.indexOf(token); + + if (index == -1) + throw new CachedQueryError("Token " + token + " not found for filtering"); + + const qt = parsedTokens[token]; + + if (qt.filterType == "Lite") + return `(rr.columns[${index}] && (rr.columns[${index}].EntityType + ";" + rr.columns[${index}].id))` + + return `rr.columns[${index}]`; + } + + + const parts = keyTokens.map(token => columnKey(token)).join("+ \"|\" + "); + + return new Function(rr, "return " + parts + ";") as (row: ResultRow) => string; +} + +function orderRows(rt: ResultTable, orders: OrderRequest[], parseTokens: { [token: string]: QueryToken }): ResultTable { + + var newRows = Array.from(rt.rows); + + + for (var i = orders.length - 1; i >= 0; i--) { + var o = orders[i]; + + const pt = parseTokens[o.token]; + + var index = rt.columns.indexOf(o.token); + + if (index == -1) + throw new CachedQueryError("Unable to order by token " + o.token); + + if (o.orderType == "Ascending") { + if (pt.filterType == "Lite") + newRows.sort((ra, rb) => { const a = ra.columns[index]; const b = rb.columns[index]; return a == b ? 0 : a == null ? -1 : b == null ? 1 : a.toStr > b.toStr ? 1 : -1 }); + else + newRows.sort((ra, rb) => { const a = ra.columns[index]; const b = rb.columns[index]; return a == b ? 0 : a == null ? -1 : b == null ? 1 : a > b ? 1 : -1 }); + } else { + if (pt.filterType == "Lite") + newRows.sort((ra, rb) => { const a = ra.columns[index]; const b = rb.columns[index]; return a == b ? 0 : a == null ? 1 : b == null ? -1 : a.toStr > b.toStr ? -1 : 1 }); + else + newRows.sort((ra, rb) => { const a = ra.columns[index]; const b = rb.columns[index]; return a == b ? 0 : a == null ? 1 : b == null ? -1 : a > b ? -1 : 1 }); + } + } + + + return ({ + columns: rt.columns, + pagination: rt.pagination, + rows: newRows, + uniqueValues: rt.uniqueValues, + totalElements: rt.totalElements + }); + +} + +function selectRows(rt: ResultTable, columns: ColumnRequest[]): ResultTable { + + const indexes: number[] = []; + for (var i = 0; i < columns.length; i++) { + var idx = rt.columns.indexOf(columns[i].token); + if (idx == -1) + throw new CachedQueryError("Unable to select by token " + columns[i].token); + + indexes.push(idx); + } + + var oldRows = rt.rows; + var newRows: ResultRow[] = []; + for (var i = 0; i < oldRows.length; i++) { + const or = oldRows[i]; + const nr = { entity: or.entity, columns: [] } as ResultRow; + for (var j = 0; j < indexes.length; j++) { + nr.columns.push(or.columns[indexes[j]]); + } + newRows.push(nr); + } + + return ({ + columns: columns.map(a => a.token), + pagination: rt.pagination, + rows: newRows, + uniqueValues: rt.uniqueValues, + totalElements: rt.totalElements + }); +} + +function filterRows(rt: ResultTable, filters: FilterRequest[]): ResultTable{ + + if (filters.length == 0) + return rt; + + if (rt.pagination.mode != "All") + throw new CachedQueryError("Unable to filter " + rt.pagination.mode); + + var filterer = createFilterer(rt, filters); + + var newRows = filterer(rt.rows); + + return { + columns: rt.columns, + rows: newRows, + uniqueValues: rt.uniqueValues, + pagination: { mode: "All" }, + totalElements: newRows.length + }; +} + +function createFilterer(result: ResultTable, filters: FilterRequest[]): ((rows: ResultRow[]) => ResultRow[]){ + + const cls = "cls"; + + var allValues: unknown[] = []; + + function getUniqueValue(v: unknown, token: string) { + var uvs = result.uniqueValues[token]; + + if (uvs) { + for (var i = 0; i < uvs.length; i++) { + if (uvs[i] == v || is(uvs[i], v as Lite, false, false)) + return uvs[i]; + } + } + + return v; + } + + function getVarName(v: unknown) { + allValues.push(v); + return "v" + (allValues.length - 1); + } + + function getExpression(f: FilterRequest): string { + if (isFilterGroupRequest(f)) { + + const parts = f.filters.map(ff => getExpression(ff)); + + if (f.groupOperation == "Or") + return "( " + parts.join(" || ") + ")"; + return parts.join(" && "); + + } else { + + var index = result.columns.indexOf(f.token); + + if (index == -1) + throw new CachedQueryError("Unable to filter " + f.token + ", column not found"); + + var op = "cls[" + index + "]"; + + if (f.operation == "IsIn" || f.operation == "IsNotIn") { + var values = f.value as unknown[]; + + var exps = allValues.map(v => op + "===" + getVarName(getUniqueValue(v, f.token))).join(" || "); + + return f.operation == "IsIn" ? exps : ("!(" + exps + ")"); + } + else { + + var vn = getVarName(getUniqueValue(f.value, f.token)); + switch (f.operation) { + case "EqualTo": return `${op} === ${vn}`; + case "DistinctTo": return `${op} !== ${vn}`; + case "GreaterThan": return `${op} > ${vn}`; + case "GreaterThanOrEqual": return `${op} >= ${vn}`; + case "LessThan": return `${op} < ${vn}`; + case "LessThanOrEqual": return `${op} <= ${vn}`; + case "Contains": return `${op} != null && ${op}.includes(${vn})`; + case "NotContains": return `!(${op} != null && ${op}.includes(${vn}))`; + case "EndsWith": return `${op} !== null && ${op}.endsWith(${vn})`; + case "NotEndsWith": return `!(${op} !== null && ${op}.endsWith(${vn}))`; + case "StartsWith": return `${op} !== null && ${op}.startsWith(${vn})`; + case "NotStartsWith": return `!(${op} !== null && ${op}.startsWith(${vn}))`; + + case "Like": throw new CachedQueryError("Like not supported"); + case "NotLike": throw new CachedQueryError("NotLike not supported"); + + default: throw new Error("Unexpected " + f.operation); + } + } + } + + } + + + + var expression = filters.map(f => getExpression(f)).join(" &&\n"); + + var factory = new Function(...allValues.map((v, i) => "v" + i), `return rows => { + const result = []; + for(let i = 0; i < rows.length; i++) { + var cls = rows[i].columns; + if (${expression}) { + result.push(rows[i]); + } + } + return result; +};`); + + return factory(...allValues); +} + +function ordersEquals(cached: OrderRequest[], requested: OrderRequest[]) { + if (cached.length != requested.length) + return false; + + for (var i = 0; i < cached.length; i++) { + if (cached[i].token != requested[i].token) + return false; + + if (cached[i].orderType != requested[i].orderType) + return false; + } + + return true; +} + +function extractRequestedFilters(cached: FilterRequest[], request: FilterRequest[]): FilterRequest[] { + + var cloned = JSON.parse(JSON.stringify(request)) as FilterRequest[]; + + for (var i = 0; i < cached.length; i++) { + + const c = cached[i]; + + const removed = cloned.extract(rf => equalFilter(c, rf)); + + if (removed.length == 0) + throw new CachedQueryError("Cached filter not found in requet"); + } + + return cloned; +} + +function equalFilter(c: FilterRequest, r: FilterRequest): boolean { + if (isFilterGroupRequest(c)) { + if (!isFilterGroupRequest(r)) + return false; + + if (c.groupOperation != r.groupOperation) + return false; + + if (c.token != r.token) + return false; + + if (c.filters.length != r.filters.length) + return false; + + return c.filters.every((cf, i) => equalFilter(cf, r.filters[i])); + } else { + if (isFilterGroupRequest(r)) + return false; + + if (c.token != r.token) + return false; + + if (c.operation != r.operation) + return false; + + if (!is(c.value, r.value, false, false) && c.value != r.value) + return false; + + return true; + } +} + +function paginateRows(rt: ResultTable, reqPag: Pagination): ResultTable{ + switch (rt.pagination.mode) { + case "All": + { + switch (reqPag.mode) { + case "All": return rt; + case "Firsts": return { ...rt, rows: rt.rows.slice(0, rt.pagination.elementsPerPage), pagination: reqPag }; + case "Paginate": + var startIndex = reqPag.elementsPerPage! * (reqPag.currentPage! - 1); + return { ...rt, rows: rt.rows.slice(startIndex, startIndex + reqPag.elementsPerPage!), pagination: reqPag }; + } + } + case "Paginate": { + switch (reqPag.mode) { + case "All": throw new Error(`Requesting ${reqPag.mode} but cached is ${rt.pagination.mode}`) + case "Firsts": + if (reqPag.currentPage == 1 && reqPag.elementsPerPage! <= rt.pagination.elementsPerPage!) + return { ...rt, rows: rt.rows.slice(0, reqPag.elementsPerPage), pagination: reqPag }; + + throw new CachedQueryError(`Invalid first`); + + case "Paginate": + if (((reqPag.elementsPerPage! == rt.pagination.elementsPerPage! && reqPag.currentPage == rt.pagination.currentPage) || + (reqPag.elementsPerPage! <= rt.pagination.elementsPerPage! && reqPag.currentPage == 1 && rt.pagination.currentPage == 1))) { + + var startIndex = reqPag.elementsPerPage! * (reqPag.currentPage! - 1); + return { ...rt, rows: rt.rows.slice(startIndex, startIndex + reqPag.elementsPerPage!), pagination: reqPag }; + } + + throw new CachedQueryError("Invalid paginate"); + } + } + case "Firsts": { + switch (reqPag.mode) { + case "Firsts": + if (reqPag.elementsPerPage! <= rt.pagination.elementsPerPage!) + return { ...rt, rows: rt.rows.slice(0, reqPag.elementsPerPage), pagination: reqPag }; + + throw new Error(`Invalid first`); + case "Paginate": + case "All": throw new CachedQueryError(`Requesting ${reqPag.mode} but cached is ${rt.pagination.mode}`); + } + } + } +} + +function pagionationRestriction(req: Pagination, cached: Pagination): null | "ExactFiltersAndOrders" { + + switch (cached.mode) { + case "All": return null; + case "Paginate": { + + switch (req.mode) { + + case "Firsts": { + if (cached.currentPage == 1 && req.elementsPerPage! <= cached.elementsPerPage!) + return "ExactFiltersAndOrders"; + + throw new CachedQueryError("Invalid First"); + } + + case "Paginate": { + if (((req.elementsPerPage! == cached.elementsPerPage! && req.currentPage == cached.currentPage) || + (req.elementsPerPage! <= cached.elementsPerPage! && req.currentPage == 1 && cached.currentPage == 1))) + return "ExactFiltersAndOrders"; + + throw new CachedQueryError("Invalid Paginate"); + } + + case "All": throw new CachedQueryError(`Requesting ${req.mode} but cached is ${cached.mode}`); + } + } + + case "Firsts": + { + switch (req.mode) { + case "Firsts": { + if (req.elementsPerPage! <= cached.elementsPerPage!) + return "ExactFiltersAndOrders"; + + throw new CachedQueryError("Invalid First"); + } + case "Paginate": + case "All": throw new CachedQueryError(`Requesting ${req.mode} but cached is ${cached.mode}`); + } + } + } +} diff --git a/Signum.React.Extensions/Dashboard/DashboardClient.tsx b/Signum.React.Extensions/Dashboard/DashboardClient.tsx index 3e53d9b04f..9417f30e99 100644 --- a/Signum.React.Extensions/Dashboard/DashboardClient.tsx +++ b/Signum.React.Extensions/Dashboard/DashboardClient.tsx @@ -1,9 +1,10 @@ import * as React from 'react' import { IconProp } from '@fortawesome/fontawesome-svg-core' -import { ajaxGet } from '@framework/Services'; +import { ajaxGet, ajaxPost } from '@framework/Services'; import * as Constructor from '@framework/Constructor'; import { EntitySettings } from '@framework/Navigator' import * as Navigator from '@framework/Navigator' +import * as Operations from '@framework/Operations' import * as AppContext from '@framework/AppContext' import * as Finder from '@framework/Finder' import { Entity, Lite, liteKey, toLite, EntityPack, getToString, SelectorMessage } from '@framework/Signum.Entities' @@ -14,7 +15,7 @@ import * as AuthClient from '../Authorization/AuthClient' import * as ChartClient from '../Chart/ChartClient' import * as UserChartClient from '../Chart/UserChart/UserChartClient' import * as UserQueryClient from '../UserQueries/UserQueryClient' -import { DashboardPermission, DashboardEntity, ValueUserQueryListPartEntity, LinkListPartEntity, UserChartPartEntity, UserQueryPartEntity, IPartEntity, DashboardMessage, PanelPartEmbedded, UserTreePartEntity, CombinedUserChartPartEntity } from './Signum.Entities.Dashboard' +import { DashboardPermission, DashboardEntity, ValueUserQueryListPartEntity, LinkListPartEntity, UserChartPartEntity, UserQueryPartEntity, IPartEntity, DashboardMessage, PanelPartEmbedded, UserTreePartEntity, CombinedUserChartPartEntity, CachedQueryEntity, DashboardOperation } from './Signum.Entities.Dashboard' import * as UserAssetClient from '../UserAssets/UserAssetClient' import { ImportRoute } from "@framework/AsyncImport"; import { useAPI } from '@framework/Hooks'; @@ -23,6 +24,7 @@ import SelectorModal from '@framework/SelectorModal'; import { translated } from '../Translation/TranslatedInstanceTools'; import { DashboardFilterController } from "./View/DashboardFilterController"; import { EntityFrame } from '../../Signum.React/Scripts/TypeContext'; +import { CachedQueryJS } from './CachedQueryExecutor'; export interface PanelPartContentProps { @@ -31,6 +33,7 @@ export interface PanelPartContentProps { entity?: Lite; deps?: React.DependencyList; filterController: DashboardFilterController; + cachedQueries: { [userAssetKey: string]: Promise } } interface IconColor { @@ -58,6 +61,7 @@ export function start(options: { routes: JSX.Element[] }) { Constructor.registerConstructor(DashboardEntity, () => DashboardEntity.New({ owner: AppContext.currentUser && toLite(AppContext.currentUser) })); Navigator.addSettings(new EntitySettings(DashboardEntity, e => import('./Admin/Dashboard'))); + Navigator.addSettings(new EntitySettings(CachedQueryEntity, e => import('./Admin/CachedQuery'))); Navigator.addSettings(new EntitySettings(ValueUserQueryListPartEntity, e => import('./Admin/ValueUserQueryListPart'))); Navigator.addSettings(new EntitySettings(LinkListPartEntity, e => import('./Admin/LinkListPart'))); @@ -65,6 +69,14 @@ export function start(options: { routes: JSX.Element[] }) { Navigator.addSettings(new EntitySettings(CombinedUserChartPartEntity, e => import('./Admin/CombinedUserChartPart'))); Navigator.addSettings(new EntitySettings(UserQueryPartEntity, e => import('./Admin/UserQueryPart'))); + Operations.addSettings(new Operations.EntityOperationSettings(DashboardOperation.RegenerateCachedQueries, { + isVisible: () => false, + color: "warning", + icon: "cogs", + contextual: { isVisible: () => true }, + contextualFromMany: { isVisible: () => true }, + })); + Finder.addSettings({ queryName: DashboardEntity, defaultOrders: [{ token: DashboardEntity.token(d => d.dashboardPriority), orderType: "Descending" }] @@ -106,8 +118,8 @@ export function start(options: { routes: JSX.Element[] }) { (p, e, ev) => { ev.preventDefault(); return SelectorModal.chooseElement(p.userCharts.map(a => a.element), { - buttonDisplay: a => a.displayName ?? "", - buttonName: a => a.id!.toString(), + buttonDisplay: a => a.userChart.displayName ?? "", + buttonName: a => a.userChart.id!.toString(), title: SelectorMessage.SelectAnElement.niceToString(), message: SelectorMessage.PleaseSelectAnElement.niceToString() }) @@ -119,14 +131,14 @@ export function start(options: { routes: JSX.Element[] }) { ev.preventDefault(); ev.persist(); SelectorModal.chooseElement(p.userCharts.map(a => a.element), { - buttonDisplay: a => a.displayName ?? "", - buttonName: a => a.id!.toString(), + buttonDisplay: a => a.userChart.displayName ?? "", + buttonName: a => a.userChart.id!.toString(), title: SelectorMessage.SelectAnElement.niceToString(), message: SelectorMessage.PleaseSelectAnElement.niceToString() }).then(uc => { if (uc) { - UserChartClient.Converter.toChartRequest(uc, e) - .then(cr => ChartClient.Encoder.chartPathPromise(cr, toLite(uc!))) + UserChartClient.Converter.toChartRequest(uc.userChart, e) + .then(cr => ChartClient.Encoder.chartPathPromise(cr, toLite(uc.userChart))) .then(path => AppContext.pushOrOpenInTab(path, ev)) .done(); } @@ -216,7 +228,7 @@ export function start(options: { routes: JSX.Element[] }) { AppContext.pushOrOpenInTab(dashboardUrl(ctx.lite, entity), e); }).done(); - }).done())); + }).done(), { group: null, icon: "eye", iconColor: "blue", color: "info" })); } export function home(): Promise | null> { @@ -246,6 +258,15 @@ export module API { export function home(): Promise | null> { return ajaxGet({ url: "~/api/dashboard/home" }); } + + export function get(dashboard: Lite): Promise { + return ajaxPost({ url: "~/api/dashboard/get" }, dashboard); + } +} + +export interface DashboardWithCachedQueries { + dashboard: DashboardEntity + cachedQueries: Array; } declare module '@framework/Signum.Entities' { @@ -273,6 +294,7 @@ export function DashboardWidget(p: DashboardWidgetProps) { dashboard: p.dashboard, entity: p.pack.entity, reload: () => p.frame.onReload(), + cachedQueries: {} /*for now*/ }); } diff --git a/Signum.React.Extensions/Dashboard/DashboardController.cs b/Signum.React.Extensions/Dashboard/DashboardController.cs index 0ada32d1b2..07c000c605 100644 --- a/Signum.React.Extensions/Dashboard/DashboardController.cs +++ b/Signum.React.Extensions/Dashboard/DashboardController.cs @@ -11,10 +11,27 @@ public IEnumerable> FromEntityType(string typeName) { return DashboardLogic.GetDashboardsEntity(TypeLogic.GetType(typeName)); } + [HttpGet("api/dashboard/home")] public Lite? Home() { var result = DashboardLogic.GetHomePageDashboard(); return result?.ToLite(); } + + [HttpPost("api/dashboard/get")] + public DashboardWithCachedQueries GetDashboard([FromBody]Lite dashboard) + { + return new DashboardWithCachedQueries + { + Dashboard = DashboardLogic.RetrieveDashboard(dashboard), + CachedQueries = DashboardLogic.GetCachedQueries(dashboard).ToList(), + }; + } +} + +public class DashboardWithCachedQueries +{ + public DashboardEntity Dashboard; + public List CachedQueries; } diff --git a/Signum.React.Extensions/Dashboard/Signum.Entities.Dashboard.ts b/Signum.React.Extensions/Dashboard/Signum.Entities.Dashboard.ts index 6f44b922cb..4e1071671e 100644 --- a/Signum.React.Extensions/Dashboard/Signum.Entities.Dashboard.ts +++ b/Signum.React.Extensions/Dashboard/Signum.Entities.Dashboard.ts @@ -6,16 +6,48 @@ import { MessageKey, QueryKey, Type, EnumType, registerSymbol } from '../../Sign import * as Entities from '../../Signum.React/Scripts/Signum.Entities' import * as Basics from '../../Signum.React/Scripts/Signum.Entities.Basics' import * as UserAssets from '../UserAssets/Signum.Entities.UserAssets' +import * as Files from '../Files/Signum.Entities.Files' import * as Signum from '../Basics/Signum.Entities.Basics' import * as UserQueries from '../UserQueries/Signum.Entities.UserQueries' import * as Chart from '../Chart/Signum.Entities.Chart' import * as Authorization from '../Authorization/Signum.Entities.Authorization' +export const CachedQueryEntity = new Type("CachedQuery"); +export interface CachedQueryEntity extends Entities.Entity { + Type: "CachedQuery"; + dashboard: Entities.Lite; + userAssets: Entities.MList>; + file: Files.FilePathEmbedded; + numRows: number; + numColumns: number; + creationDate: string /*DateTime*/; + queryDuration: number; + uploadDuration: number; +} + +export module CachedQueryFileType { + export const CachedQuery : Files.FileTypeSymbol = registerSymbol("FileType", "CachedQueryFileType.CachedQuery"); +} + +export const CacheQueryConfigurationEmbedded = new Type("CacheQueryConfigurationEmbedded"); +export interface CacheQueryConfigurationEmbedded extends Entities.EmbeddedEntity { + Type: "CacheQueryConfigurationEmbedded"; + timeoutForQueries: number; + maxRows: number; +} + +export const CombinedUserChartElementEmbedded = new Type("CombinedUserChartElementEmbedded"); +export interface CombinedUserChartElementEmbedded extends Entities.EmbeddedEntity { + Type: "CombinedUserChartElementEmbedded"; + userChart: Chart.UserChartEntity; + isQueryCached: boolean; +} + export const CombinedUserChartPartEntity = new Type("CombinedUserChartPart"); export interface CombinedUserChartPartEntity extends Entities.Entity, IPartEntity { Type: "CombinedUserChartPart"; - userCharts: Entities.MList; + userCharts: Entities.MList; showData: boolean; allowChangeShowData: boolean; combinePinnedFiltersWithSameLabel: boolean; @@ -40,6 +72,7 @@ export interface DashboardEntity extends Entities.Entity, UserAssets.IUserAssetE autoRefreshPeriod: number | null; displayName: string; combineSimilarRows: boolean; + cacheQueryConfiguration: CacheQueryConfigurationEmbedded | null; parts: Entities.MList; guid: string /*Guid*/; key: string | null; @@ -57,6 +90,7 @@ export module DashboardMessage { export module DashboardOperation { export const Save : Entities.ExecuteSymbol = registerSymbol("Operation", "DashboardOperation.Save"); + export const RegenerateCachedQueries : Entities.ExecuteSymbol = registerSymbol("Operation", "DashboardOperation.RegenerateCachedQueries"); export const Clone : Entities.ConstructSymbol_From = registerSymbol("Operation", "DashboardOperation.Clone"); export const Delete : Entities.DeleteSymbol = registerSymbol("Operation", "DashboardOperation.Delete"); } @@ -112,6 +146,7 @@ export const UserChartPartEntity = new Type("UserChartPart" export interface UserChartPartEntity extends Entities.Entity, IPartEntity { Type: "UserChartPart"; userChart: Chart.UserChartEntity; + isQueryCached: boolean; showData: boolean; allowChangeShowData: boolean; createNew: boolean; @@ -123,6 +158,7 @@ export const UserQueryPartEntity = new Type("UserQueryPart" export interface UserQueryPartEntity extends Entities.Entity, IPartEntity { Type: "UserQueryPart"; userQuery: UserQueries.UserQueryEntity; + isQueryCached: boolean; renderMode: UserQueryPartRenderMode; allowSelection: boolean; showFooter: boolean; @@ -147,6 +183,7 @@ export interface ValueUserQueryElementEmbedded extends Entities.EmbeddedEntity { Type: "ValueUserQueryElementEmbedded"; label: string | null; userQuery: UserQueries.UserQueryEntity; + isQueryCached: boolean; href: string | null; } diff --git a/Signum.React.Extensions/Dashboard/View/CombinedUserChartPart.tsx b/Signum.React.Extensions/Dashboard/View/CombinedUserChartPart.tsx index 27c09dc0f2..c94cea863d 100644 --- a/Signum.React.Extensions/Dashboard/View/CombinedUserChartPart.tsx +++ b/Signum.React.Extensions/Dashboard/View/CombinedUserChartPart.tsx @@ -3,7 +3,7 @@ import { ServiceError } from '@framework/Services' import * as Finder from '@framework/Finder' import * as Navigator from '@framework/Navigator' import * as Constructor from '@framework/Constructor' -import { Entity, Lite, is, JavascriptMessage } from '@framework/Signum.Entities' +import { Entity, Lite, is, JavascriptMessage, toLite, liteKey } from '@framework/Signum.Entities' import * as UserChartClient from '../../Chart/UserChart/UserChartClient' import * as ChartClient from '../../Chart/ChartClient' import { ChartRequestModel, UserChartEntity } from '../../Chart/Signum.Entities.Chart' @@ -18,6 +18,8 @@ import { QueryDescription } from '@framework/FindOptions' import { ErrorBoundary } from '@framework/Components' import ChartRendererCombined from '../../Chart/Templates/ChartRendererCombined' import { MemoRepository } from '../../Chart/D3Scripts/Components/ReactChart' +import { getQueryKey } from '@framework/Reflection' +import { executeChartCached } from '../CachedQueryExecutor' export interface CombinedUserChartInfoTemp { @@ -35,7 +37,7 @@ export default function CombinedUserChartPart(p: PanelPartContentProps(() => p.part.userCharts.map(uc => ({ userChart: uc.element } as CombinedUserChartInfoTemp)), [p.part]); + const infos = React.useMemo(() => p.part.userCharts.map(uc => ({ userChart: uc.element.userChart } as CombinedUserChartInfoTemp)), [p.part]); const [showData, setShowData] = React.useState(p.part.showData); @@ -69,6 +71,24 @@ export default function CombinedUserChartPart(p: PanelPartContentProps cachedQuery.then(cq => executeChartCached(chartRequest, cs, cq))) + .then(result => { + if (!signal.aborted) { + c.result = result; + forceUpdate(); + } + }) + .catch(error => { + if (!signal.aborted) { + c.error = error; + forceUpdate(); + } + }); + return ChartClient.API.executeChart(chartRequest!, c.chartScript!, signal) .then(result => { if (!signal.aborted) { @@ -94,8 +114,13 @@ export default function CombinedUserChartPart(p: PanelPartContentProps p.filterController.lastChange.get(e.userChart.query.key))]); + }, [p.part, ...p.deps ?? []]); + React.useEffect(() => { + infos.forEach(inf => { + inf.makeQuery?.().done(); + }); + }, [p.part, ...p.deps ?? [], infos.max(e => p.filterController.lastChange.get(e.userChart.query.key))]); function renderError(e: any, key: number) { const se = e instanceof ServiceError ? (e as ServiceError) : undefined; diff --git a/Signum.React.Extensions/Dashboard/View/DashboardFilterController.tsx b/Signum.React.Extensions/Dashboard/View/DashboardFilterController.tsx index c9ca81585e..1e7ba244d5 100644 --- a/Signum.React.Extensions/Dashboard/View/DashboardFilterController.tsx +++ b/Signum.React.Extensions/Dashboard/View/DashboardFilterController.tsx @@ -58,7 +58,6 @@ export class DashboardFilterController { if (fops.length == 0) return fo; - var newFilters = Finder.toFilterOptions(fops); return { ...fo, diff --git a/Signum.React.Extensions/Dashboard/View/DashboardPage.tsx b/Signum.React.Extensions/Dashboard/View/DashboardPage.tsx index c71dedab73..538c9ac855 100644 --- a/Signum.React.Extensions/Dashboard/View/DashboardPage.tsx +++ b/Signum.React.Extensions/Dashboard/View/DashboardPage.tsx @@ -1,7 +1,8 @@ import * as React from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Link } from 'react-router-dom' -import { Entity, parseLite, getToString, JavascriptMessage, EntityPack } from '@framework/Signum.Entities' +import { Entity, parseLite, getToString, JavascriptMessage, EntityPack, liteKey } from '@framework/Signum.Entities' +import * as Finder from '@framework/Finder' import * as Navigator from '@framework/Navigator' import { DashboardEntity } from '../Signum.Entities.Dashboard' import DashboardView from './DashboardView' @@ -10,6 +11,10 @@ import "../Dashboard.css" import { useAPI, useAPIWithReload, useInterval } from '@framework/Hooks' import { QueryString } from '@framework/QueryString' import { translated } from '../../Translation/TranslatedInstanceTools' +import * as DashboardClient from "../DashboardClient" +import { newLite } from '@framework/Reflection' +import { downloadFile } from '../../Files/FileDownloader' +import { CachedQueryJS } from '../CachedQueryExecutor' interface DashboardPageProps extends RouteComponentProps<{ dashboardId: string }> { @@ -21,16 +26,28 @@ function getQueryEntity(props: DashboardPageProps): string { export default function DashboardPage(p: DashboardPageProps) { - const [dashboard, reloadDashboard] = useAPIWithReload(signal => Navigator.API.fetchEntity(DashboardEntity, p.match.params.dashboardId), [p.match.params.dashboardId]); + const [dashboardWithQueries, reloadDashboard] = useAPIWithReload(signal => DashboardClient.API.get(newLite(DashboardEntity, p.match.params.dashboardId)), [p.match.params.dashboardId]); + + const dashboard = dashboardWithQueries?.dashboard; var entityKey = getQueryEntity(p); const entity = useAPI(signal => entityKey ? Navigator.API.fetch(parseLite(entityKey)) : Promise.resolve(null), [entityKey]); - const rtl = React.useMemo(() => document.body.classList.contains("rtl"), []); - const refreshCounter = useInterval(dashboard?.autoRefreshPeriod == null ? null : dashboard.autoRefreshPeriod * 1000, 0, old => old + 1); + React.useEffect(() => { + + if (dashboardWithQueries && dashboardWithQueries.cachedQueries.length > 0) + reloadDashboard(); + + }, [refreshCounter]); + + var cachedQueries = React.useMemo(() => dashboardWithQueries?.cachedQueries + .map(a => ({ userAssets: a.userAssets, promise: downloadFile(a.file).then(r => r.json() as Promise).then(cq => { Finder.decompress(cq.resultTable); return cq; })})) //share promise + .flatMap(a => a.userAssets.map(mle => ({ ua: mle.element, promise: a.promise }))) + .toObject(a => liteKey(a.ua), a => a.promise), [dashboardWithQueries]); + return (
{!dashboard ?

{JavascriptMessage.loading.niceToString()}

: @@ -42,7 +59,7 @@ export default function DashboardPage(p: DashboardPageProps) {
} {entityKey && -
+
{!entity ?

{JavascriptMessage.loading.niceToString()}

:

{Navigator.isViewable({ entity: entity, canExecute: {} } as EntityPack) ? @@ -56,7 +73,7 @@ export default function DashboardPage(p: DashboardPageProps) {

} - {dashboard && (!entityKey || entity) && } + {dashboard && (!entityKey || entity) && }
); } diff --git a/Signum.React.Extensions/Dashboard/View/DashboardView.tsx b/Signum.React.Extensions/Dashboard/View/DashboardView.tsx index 1ea0628efd..1fe986bbda 100644 --- a/Signum.React.Extensions/Dashboard/View/DashboardView.tsx +++ b/Signum.React.Extensions/Dashboard/View/DashboardView.tsx @@ -4,7 +4,7 @@ import { classes } from '@framework/Globals' import { Entity, getToString, is, Lite, MListElement, SearchMessage, toLite } from '@framework/Signum.Entities' import { TypeContext, mlistItemContext } from '@framework/TypeContext' import * as DashboardClient from '../DashboardClient' -import { DashboardEntity, PanelPartEmbedded, IPartEntity, DashboardMessage } from '../Signum.Entities.Dashboard' +import { DashboardEntity, PanelPartEmbedded, IPartEntity, DashboardMessage, CachedQueryEntity } from '../Signum.Entities.Dashboard' import "../Dashboard.css" import { ErrorBoundary } from '@framework/Components'; import { coalesceIcon } from '@framework/Operations/ContextualOperations'; @@ -13,10 +13,10 @@ import { parseIcon } from '../../Basics/Templates/IconTypeahead' import { translated } from '../../Translation/TranslatedInstanceTools' import { DashboardFilterController } from './DashboardFilterController' +import { FilePathEmbedded } from '../../Files/Signum.Entities.Files' +import { CachedQueryJS } from '../CachedQueryExecutor' - - -export default function DashboardView(p: { dashboard: DashboardEntity, entity?: Entity, deps?: React.DependencyList; reload: () => void; }) { +export default function DashboardView(p: { dashboard: DashboardEntity, cachedQueries: { [userAssetKey: string]: Promise }, entity?: Entity, deps?: React.DependencyList; reload: () => void; }) { const forceUpdate = useForceUpdate(); var filterController = React.useMemo(() => new DashboardFilterController(forceUpdate), [p.dashboard]); @@ -42,7 +42,7 @@ export default function DashboardView(p: { dashboard: DashboardEntity, entity?: return (
- +
); })} @@ -78,7 +78,7 @@ export default function DashboardView(p: { dashboard: DashboardEntity, entity?: const offset = c.startColumn! - (last ? (last.startColumn! + last.columnWidth!) : 0); return (
- {c.parts.map((pctx, i) => )} + {c.parts.map((pctx, i) => )}
); })} @@ -174,6 +174,7 @@ export interface PanelPartProps { deps?: React.DependencyList; filterController: DashboardFilterController; reload: () => void; + cachedQueries: { [userAssetKey: string]: Promise } } export function PanelPart(p: PanelPartProps) { @@ -197,8 +198,9 @@ export function PanelPart(p: PanelPartProps) { part: content, entity: lite, deps: p.deps, - filterController: p.filterController - }); + filterController: p.filterController, + cachedQueries: p.cachedQueries + } as DashboardClient.PanelPartContentProps); } const titleText = translated(part, p => p.title) ?? (renderer.defaultTitle ? renderer.defaultTitle(content) : getToString(content)); @@ -248,7 +250,8 @@ export function PanelPart(p: PanelPartProps) { part: content, entity: lite, deps: p.deps, - filterController: p.filterController + filterController: p.filterController, + cachedQueries: p.cachedQueries, } as DashboardClient.PanelPartContentProps) } diff --git a/Signum.React.Extensions/Dashboard/View/UserChartPart.tsx b/Signum.React.Extensions/Dashboard/View/UserChartPart.tsx index 398c5069a7..6c8b62d5e9 100644 --- a/Signum.React.Extensions/Dashboard/View/UserChartPart.tsx +++ b/Signum.React.Extensions/Dashboard/View/UserChartPart.tsx @@ -3,7 +3,7 @@ import { ServiceError } from '@framework/Services' import * as Finder from '@framework/Finder' import * as Navigator from '@framework/Navigator' import * as Constructor from '@framework/Constructor' -import { Entity, Lite, is, JavascriptMessage } from '@framework/Signum.Entities' +import { Entity, Lite, is, JavascriptMessage, liteKey, toLite } from '@framework/Signum.Entities' import * as UserChartClient from '../../Chart/UserChart/UserChartClient' import * as ChartClient from '../../Chart/ChartClient' import { ChartMessage, ChartRequestModel } from '../../Chart/Signum.Entities.Chart' @@ -17,6 +17,7 @@ import { getTypeInfos } from '@framework/Reflection' import SelectorModal from '@framework/SelectorModal' import { DashboardFilter, DashboardFilterController, DashboardFilterRow, equalsDFR } from "./DashboardFilterController" import { filterOperations, isFilterGroupOptionParsed } from '@framework/FindOptions' +import { CachedQueryJS, executeChartCached } from '../CachedQueryExecutor' export default function UserChartPart(p: PanelPartContentProps) { @@ -50,12 +51,23 @@ export default function UserChartPart(p: PanelPartContentProps(() => chartRequest == null ? Promise.resolve(undefined) : - ChartClient.getChartScript(chartRequest!.chartScript) + + const cachedQuery = p.cachedQueries[liteKey(toLite(p.part.userChart))]; + + const [resultOrError, reloadQuery] = useAPIWithReload(() => { + if (chartRequest == null) + return Promise.resolve(undefined); + + if (cachedQuery) + return ChartClient.getChartScript(chartRequest!.chartScript) + .then(cs => cachedQuery.then(cq => executeChartCached(chartRequest, cs, cq))) + .then(result => ({ result }), error => ({ error })); + + return ChartClient.getChartScript(chartRequest!.chartScript) .then(cs => ChartClient.API.executeChart(chartRequest!, cs)) - .then(result => ({ result })) - .catch(error => ({ error })), - [chartRequest && ChartClient.Encoder.chartPath(ChartClient.Encoder.toChartOptions(chartRequest, null)), ...p.deps ?? []], { avoidReset: true }); + .then(result => ({ result }), error => ({ error })); + + }, [chartRequest && ChartClient.Encoder.chartPath(ChartClient.Encoder.toChartOptions(chartRequest, null)), ...p.deps ?? []], { avoidReset: true }); const [showData, setShowData] = React.useState(p.part.showData); @@ -88,7 +100,7 @@ export default function UserChartPart(p: PanelPartContentProps) { e?.preventDefault(); - makeQuery(); + reloadQuery(); } const typeInfos = qd && getTypeInfos(qd.columns["Entity"].type).filter(ti => Navigator.isCreable(ti, { isSearch: true })); @@ -101,13 +113,13 @@ export default function UserChartPart(p: PanelPartContentProps ti && Finder.getPropsFromFilters(ti, chartRequest!.filterOptions) .then(props => Constructor.constructPack(ti.name, props))) .then(pack => pack && Navigator.view(pack)) - .then(() => makeQuery()) + .then(() => reloadQuery()) .done(); } return (
- makeQuery()} extraSmall={true} /> + reloadQuery()} extraSmall={true} /> {p.part.allowChangeShowData &&

- + p.cachedQuery!.then(cq=> executeQueryValueCached(req, fop, token,cq)))} + />

diff --git a/Signum.React.Extensions/Dashboard/View/ValueUserQueryListPart.tsx b/Signum.React.Extensions/Dashboard/View/ValueUserQueryListPart.tsx index b6ebcedc6a..3464c3bbf9 100644 --- a/Signum.React.Extensions/Dashboard/View/ValueUserQueryListPart.tsx +++ b/Signum.React.Extensions/Dashboard/View/ValueUserQueryListPart.tsx @@ -3,14 +3,15 @@ import * as React from 'react' import { FormGroup } from '@framework/Lines' import { FindOptions } from '@framework/FindOptions' import { getQueryNiceName } from '@framework/Reflection' -import { Entity, Lite, is, JavascriptMessage } from '@framework/Signum.Entities' +import { Entity, Lite, is, JavascriptMessage, liteKey, toLite } from '@framework/Signum.Entities' import { ValueSearchControlLine } from '@framework/Search' import { TypeContext, mlistItemContext } from '@framework/TypeContext' import * as UserQueryClient from '../../UserQueries/UserQueryClient' -import { ValueUserQueryListPartEntity, ValueUserQueryElementEmbedded, PanelPartEmbedded } from '../Signum.Entities.Dashboard' +import { ValueUserQueryListPartEntity, ValueUserQueryElementEmbedded, PanelPartEmbedded, CachedQueryEntity } from '../Signum.Entities.Dashboard' import { useAPI } from '@framework/Hooks' import { PanelPartContentProps } from '../DashboardClient' import { DashboardFilterController } from './DashboardFilterController' +import { CachedQueryJS, executeQueryValueCached } from '../CachedQueryExecutor' export default function ValueUserQueryListPart(p: PanelPartContentProps) { const entity = p.part; @@ -21,7 +22,9 @@ export default function ValueUserQueryListPart(p: PanelPartContentProps a.userQueries)) .map((ctx, i) =>
- +
) } @@ -33,6 +36,7 @@ export interface ValueUserQueryElementProps { entity?: Lite; filterController: DashboardFilterController; partEmbedded: PanelPartEmbedded; + cachedQuery?: Promise; } export function ValueUserQueryElement(p: ValueUserQueryElementProps) { @@ -56,7 +60,8 @@ export function ValueUserQueryElement(p: ValueUserQueryElementProps) { {ctx.value.label ?? getQueryNiceName(fo.queryName)}
- + p.cachedQuery!.then(cq => executeQueryValueCached(qr, fo, token, cq)))} />
diff --git a/Signum.React.Extensions/Dynamic/DynamicController.cs b/Signum.React.Extensions/Dynamic/DynamicController.cs index 7aefeccf4f..8273688a96 100644 --- a/Signum.React.Extensions/Dynamic/DynamicController.cs +++ b/Signum.React.Extensions/Dynamic/DynamicController.cs @@ -12,6 +12,8 @@ using Signum.Entities.Basics; using Microsoft.Extensions.Hosting; using Signum.Engine.Authorization; +using Signum.Engine.Json; +using Signum.React.Facades; namespace Signum.React.Dynamic; @@ -102,7 +104,7 @@ public async Task> GetEvalErrors([Required, FromBody]Query { DynamicPanelPermission.ViewDynamicPanel.AssertAuthorized(); - var allEntities = await QueryLogic.Queries.GetEntitiesLite(request.ToQueryEntitiesRequest()).Select(a => a.Entity).ToListAsync(); + var allEntities = await QueryLogic.Queries.GetEntitiesLite(request.ToQueryEntitiesRequest(SignumServer.JsonSerializerOptions)).Select(a => a.Entity).ToListAsync(); return allEntities.Select(entity => { diff --git a/Signum.React.Extensions/Excel/ExcelController.cs b/Signum.React.Extensions/Excel/ExcelController.cs index f7fea4db6c..980d1aadd3 100644 --- a/Signum.React.Extensions/Excel/ExcelController.cs +++ b/Signum.React.Extensions/Excel/ExcelController.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using Signum.React.Filters; using Signum.Engine.Authorization; +using Signum.Engine.Json; +using Signum.React.Facades; namespace Signum.React.Excel; @@ -20,7 +22,7 @@ public async Task ToPlainExcel([Required, FromBody]QueryReques { ExcelPermission.PlainExcel.AssertAuthorized(); - var queryRequest = request.ToQueryRequest(); + var queryRequest = request.ToQueryRequest(SignumServer.JsonSerializerOptions); ResultTable queryResult = await QueryLogic.Queries.ExecuteQueryAsync(queryRequest, token); byte[] binaryFile = PlainExcelGenerator.WritePlainExcel(queryResult, QueryUtils.GetNiceName(queryRequest.QueryName)); @@ -39,7 +41,7 @@ public IEnumerable> GetExcelReports(string queryKey) [HttpPost("api/excel/excelReport")] public FileStreamResult GenerateExcelReport([Required, FromBody]ExcelReportRequest request) { - byte[] file = ExcelLogic.ExecuteExcelReport(request.excelReport, request.queryRequest.ToQueryRequest()); + byte[] file = ExcelLogic.ExecuteExcelReport(request.excelReport, request.queryRequest.ToQueryRequest(SignumServer.JsonSerializerOptions)); var fileName = request.excelReport.ToString() + "-" + Clock.Now.ToString("yyyyMMdd-HHmmss") + ".xlsx"; diff --git a/Signum.React.Extensions/Files/FileDownloader.tsx b/Signum.React.Extensions/Files/FileDownloader.tsx index 5de32e1593..bce74a9db4 100644 --- a/Signum.React.Extensions/Files/FileDownloader.tsx +++ b/Signum.React.Extensions/Files/FileDownloader.tsx @@ -3,7 +3,7 @@ import * as Services from '@framework/Services' import * as AppContext from '@framework/AppContext' import * as Navigator from '@framework/Navigator' import { ModifiableEntity, Lite, Entity, isModifiableEntity, getToString } from '@framework/Signum.Entities' -import { IFile, FileEntity, FilePathEntity, FileEmbedded, FilePathEmbedded } from './Signum.Entities.Files' +import { IFile, FileEntity, FilePathEntity, FileEmbedded, FilePathEmbedded, IFilePath } from './Signum.Entities.Files' import { ExtensionInfo, extensionInfo } from './FilesClient' import { Type } from '@framework/Reflection'; import "./Files.css" @@ -133,6 +133,11 @@ registerConfiguration(FilePathEmbedded, { fileUrl: file => AppContext.toAbsoluteUrl(`~/api/files/downloadEmbeddedFilePath/${file.rootType}/${file.entityId}?${QueryString.stringify({ route: file.propertyRoute, rowId: file.mListRowId })}`) }); +export function downloadFile(file: IFilePath & ModifiableEntity): Promise { + var fileUrl = configurtions[file.Type].fileUrl!(file); + return Services.ajaxGetRaw({ url: fileUrl }); +} + function downloadUrl(e: React.MouseEvent, url: string) { e.preventDefault(); diff --git a/Signum.React.Extensions/Tree/TreeController.cs b/Signum.React.Extensions/Tree/TreeController.cs index 9ae7bfbf29..cd70d659d5 100644 --- a/Signum.React.Extensions/Tree/TreeController.cs +++ b/Signum.React.Extensions/Tree/TreeController.cs @@ -7,6 +7,8 @@ using Signum.Entities.Basics; using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; +using Signum.Engine.Json; +using Signum.React.Facades; namespace Signum.React.Tree; @@ -59,8 +61,8 @@ static List FindNodesGeneric(FindNodesRequest request) where T : TreeEntity { var qd = QueryLogic.Queries.QueryDescription(typeof(T)); - var userFilters = request.userFilters.Select(f => f.ToFilter(qd, false)).ToList(); - var frozenFilters = request.frozenFilters.Select(f => f.ToFilter(qd, false)).ToList(); + var userFilters = request.userFilters.Select(f => f.ToFilter(qd, false, SignumServer.JsonSerializerOptions)).ToList(); + var frozenFilters = request.frozenFilters.Select(f => f.ToFilter(qd, false, SignumServer.JsonSerializerOptions)).ToList(); var frozenQuery = QueryLogic.Queries.GetEntitiesLite(new QueryEntitiesRequest { QueryName = typeof(T), Filters = frozenFilters, Orders = new List() }) diff --git a/Signum.React.Extensions/UserQueries/UserQueryClient.tsx b/Signum.React.Extensions/UserQueries/UserQueryClient.tsx index d6aff9c895..b5e837da39 100644 --- a/Signum.React.Extensions/UserQueries/UserQueryClient.tsx +++ b/Signum.React.Extensions/UserQueries/UserQueryClient.tsx @@ -69,7 +69,7 @@ export function start(options: { routes: JSX.Element[] }) { }) .done(); }).done(); - }, { isVisible: AuthClient.isPermissionAuthorized(UserQueryPermission.ViewUserQuery) })); + }, { isVisible: AuthClient.isPermissionAuthorized(UserQueryPermission.ViewUserQuery), group: null, icon: "eye", iconColor: "blue", color: "info" })); onContextualItems.push(getGroupUserQueriesContextMenu); diff --git a/Signum.React.Extensions/Word/WordServer.cs b/Signum.React.Extensions/Word/WordServer.cs index 21edd93016..10c0708df6 100644 --- a/Signum.React.Extensions/Word/WordServer.cs +++ b/Signum.React.Extensions/Word/WordServer.cs @@ -91,7 +91,7 @@ private static void CustomizeFiltersModel() var qd = QueryLogic.Queries.QueryDescription(cr.QueryName); - cr.Filters = list.Select(l => l.ToFilter(qd, canAggregate: true)).ToList(); + cr.Filters = list.Select(l => l.ToFilter(qd, canAggregate: true, SignumServer.JsonSerializerOptions)).ToList(); }, CustomWriteJsonProperty = (Utf8JsonWriter writer, WriteJsonPropertyContext ctx) => { diff --git a/Signum.React.Extensions/Workflow/Case/CaseFrameModal.tsx b/Signum.React.Extensions/Workflow/Case/CaseFrameModal.tsx index 4bffe1887d..b277feae20 100644 --- a/Signum.React.Extensions/Workflow/Case/CaseFrameModal.tsx +++ b/Signum.React.Extensions/Workflow/Case/CaseFrameModal.tsx @@ -179,16 +179,17 @@ export default class CaseFrameModal extends React.Component this.state.executing == true, - execute: action => { + execute: async action => { if (this.state.executing) return; this.setState({ executing: true }); - action() - .finally(() => this.setState({ executing: undefined })) - .done(); + try { + await action(); + } finally { + this.setState({ executing: undefined }); + } } - }; var activityPack = { entity: pack.activity, canExecute: pack.canExecuteActivity }; @@ -248,15 +249,16 @@ export default class CaseFrameModal extends React.Component this.state.executing == true, - execute: action => { + execute: async action => { if (this.state.executing) return; this.setState({ executing: true }); - - action() - .finally(() => this.setState({ executing: undefined })) - .done(); + try { + await action(); + } finally { + this.setState({ executing: undefined }) + } } }; diff --git a/Signum.React.Extensions/Workflow/Case/CaseFramePage.tsx b/Signum.React.Extensions/Workflow/Case/CaseFramePage.tsx index 14666b2b3f..cab48dd9ee 100644 --- a/Signum.React.Extensions/Workflow/Case/CaseFramePage.tsx +++ b/Signum.React.Extensions/Workflow/Case/CaseFramePage.tsx @@ -164,14 +164,16 @@ export default class CaseFramePage extends React.Component this.state.executing == true, - execute: action => { + execute: async action => { if (this.state.executing) return; this.setState({ executing: true }); - action() - .finally(() => { this.setState({ executing: undefined }) }) - .done(); + try { + await action(); + } finally { + this.setState({ executing: undefined }); + } } }; @@ -245,14 +247,16 @@ export default class CaseFramePage extends React.Component this.state.executing == true, - execute: action => { + execute: async action => { if (this.state.executing) return; this.setState({ executing: true }); - action() - .finally(() => this.setState({ executing: undefined })) - .done(); + try { + await action(); + } finally { + this.setState({ executing: undefined }); + } } }; diff --git a/Signum.React.Extensions/Workflow/WorkflowController.cs b/Signum.React.Extensions/Workflow/WorkflowController.cs index e332213505..74e2882280 100644 --- a/Signum.React.Extensions/Workflow/WorkflowController.cs +++ b/Signum.React.Extensions/Workflow/WorkflowController.cs @@ -4,7 +4,6 @@ using System.Threading; using Signum.Entities.Basics; using Signum.Engine.Authorization; -using Signum.React.ApiControllers; using Microsoft.AspNetCore.Mvc; using Signum.React.Filters; using static Signum.React.ApiControllers.OperationController; @@ -12,6 +11,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; using System.Text.Json.Serialization; +using Signum.Engine.Json; namespace Signum.React.Workflow; @@ -291,7 +291,7 @@ public WorkflowActivityMonitorRequest ToRequest() return new WorkflowActivityMonitorRequest { Workflow = workflow, - Filters = filters.Select(f => f.ToFilter(qd, true)).ToList(), + Filters = filters.Select(f => f.ToFilter(qd, true, SignumServer.JsonSerializerOptions)).ToList(), Columns = columns.Select(c => c.ToColumn(qd, true)).ToList(), }; } diff --git a/Signum.React.Extensions/tsconfig.json b/Signum.React.Extensions/tsconfig.json index e597a1d668..8e8b09b473 100644 --- a/Signum.React.Extensions/tsconfig.json +++ b/Signum.React.Extensions/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2017", "sourceMap": true, "module": "esnext", "moduleResolution": "node", diff --git a/Signum.React/ApiControllers/QueryController.cs b/Signum.React/ApiControllers/QueryController.cs index 5d38159d1f..7f7ddd7b7d 100644 --- a/Signum.React/ApiControllers/QueryController.cs +++ b/Signum.React/ApiControllers/QueryController.cs @@ -8,9 +8,9 @@ using Signum.Engine.Maps; using System.Threading; using System.Threading.Tasks; -using Signum.React.Json; using System.Text.Json.Serialization; using System.Text.Json; +using Signum.Engine.Json; namespace Signum.React.ApiControllers; @@ -103,321 +103,30 @@ public class SubTokensRequest [HttpPost("api/query/executeQuery"), ProfilerActionSplitter] public async Task ExecuteQuery([Required, FromBody]QueryRequestTS request, CancellationToken token) { - var result = await QueryLogic.Queries.ExecuteQueryAsync(request.ToQueryRequest(), token); + var result = await QueryLogic.Queries.ExecuteQueryAsync(request.ToQueryRequest(SignumServer.JsonSerializerOptions), token); return result; } [HttpPost("api/query/entitiesLiteWithFilter"), ProfilerActionSplitter] public async Task>> GetEntitiesLiteWithFilter([Required, FromBody]QueryEntitiesRequestTS request, CancellationToken token) { - return await QueryLogic.Queries.GetEntitiesLite(request.ToQueryEntitiesRequest()).ToListAsync(token); + return await QueryLogic.Queries.GetEntitiesLite(request.ToQueryEntitiesRequest(SignumServer.JsonSerializerOptions)).ToListAsync(token); } [HttpPost("api/query/entitiesFullWithFilter"), ProfilerActionSplitter] public async Task> GetEntitiesFullWithFilter([Required, FromBody] QueryEntitiesRequestTS request, CancellationToken token) { - return await QueryLogic.Queries.GetEntitiesFull(request.ToQueryEntitiesRequest()).ToListAsync(token); + return await QueryLogic.Queries.GetEntitiesFull(request.ToQueryEntitiesRequest(SignumServer.JsonSerializerOptions)).ToListAsync(token); } [HttpPost("api/query/queryValue"), ProfilerActionSplitter] public async Task QueryValue([Required, FromBody]QueryValueRequestTS request, CancellationToken token) { - return await QueryLogic.Queries.ExecuteQueryValueAsync(request.ToQueryValueRequest(), token); + return await QueryLogic.Queries.ExecuteQueryValueAsync(request.ToQueryValueRequest(SignumServer.JsonSerializerOptions), token); } } -public class QueryValueRequestTS -{ - public string querykey; - public List filters; - public string valueToken; - public bool? multipleValues; - public SystemTimeTS/*?*/ systemTime; - - public QueryValueRequest ToQueryValueRequest() - { - var qn = QueryLogic.ToQueryName(this.querykey); - var qd = QueryLogic.Queries.QueryDescription(qn); - - var value = valueToken.HasText() ? QueryUtils.Parse(valueToken, qd, SubTokensOptions.CanAggregate | SubTokensOptions.CanElement) : null; - - return new QueryValueRequest - { - QueryName = qn, - MultipleValues = multipleValues ?? false, - Filters = this.filters.EmptyIfNull().Select(f => f.ToFilter(qd, canAggregate: false)).ToList(), - ValueToken = value, - SystemTime = this.systemTime?.ToSystemTime(), - }; - } - - public override string ToString() => querykey; -} - -public class QueryRequestTS -{ - public string queryUrl; - public string queryKey; - public bool groupResults; - public List filters; - public List orders; - public List columns; - public PaginationTS pagination; - public SystemTimeTS/*?*/ systemTime; - - public QueryRequest ToQueryRequest() - { - var qn = QueryLogic.ToQueryName(this.queryKey); - var qd = QueryLogic.Queries.QueryDescription(qn); - return new QueryRequest - { - QueryUrl = queryUrl, - QueryName = qn, - GroupResults = groupResults, - Filters = this.filters.EmptyIfNull().Select(f => f.ToFilter(qd, canAggregate: groupResults)).ToList(), - Orders = this.orders.EmptyIfNull().Select(f => f.ToOrder(qd, canAggregate: groupResults)).ToList(), - Columns = this.columns.EmptyIfNull().Select(f => f.ToColumn(qd, canAggregate: groupResults)).ToList(), - Pagination = this.pagination.ToPagination(), - SystemTime = this.systemTime?.ToSystemTime(), - }; - } - - - public override string ToString() => queryKey; -} - -public class QueryEntitiesRequestTS -{ - public string queryKey; - public List filters; - public List orders; - public int? count; - - public override string ToString() => queryKey; - - public QueryEntitiesRequest ToQueryEntitiesRequest() - { - var qn = QueryLogic.ToQueryName(queryKey); - var qd = QueryLogic.Queries.QueryDescription(qn); - return new QueryEntitiesRequest - { - QueryName = qn, - Count = count, - Filters = filters.EmptyIfNull().Select(f => f.ToFilter(qd, canAggregate: false)).ToList(), - Orders = orders.EmptyIfNull().Select(f => f.ToOrder(qd, canAggregate: false)).ToList(), - }; - } -} - -public class OrderTS -{ - public string token; - public OrderType orderType; - - public Order ToOrder(QueryDescription qd, bool canAggregate) - { - return new Order(QueryUtils.Parse(this.token, qd, SubTokensOptions.CanElement | (canAggregate ? SubTokensOptions.CanAggregate : 0)), orderType); - } - - public override string ToString() => $"{token} {orderType}"; -} - -[JsonConverter(typeof(FilterJsonConverter))] -public abstract class FilterTS -{ - public abstract Filter ToFilter(QueryDescription qd, bool canAggregate); - - public static FilterTS FromFilter(Filter filter) - { - if (filter is FilterCondition fc) - return new FilterConditionTS - { - token = fc.Token.FullKey(), - operation = fc.Operation, - value = fc.Value - }; - - if (filter is FilterGroup fg) - return new FilterGroupTS - { - token = fg.Token?.FullKey(), - groupOperation = fg.GroupOperation, - filters = fg.Filters.Select(f => FromFilter(f)).ToList(), - }; - - throw new UnexpectedValueException(filter); - } -} - -public class FilterConditionTS : FilterTS -{ - public string token; - public FilterOperation operation; - public object? value; - - public override Filter ToFilter(QueryDescription qd, bool canAggregate) - { - var options = SubTokensOptions.CanElement | SubTokensOptions.CanAnyAll | (canAggregate ? SubTokensOptions.CanAggregate : 0); - var parsedToken = QueryUtils.Parse(token, qd, options); - var expectedValueType = operation.IsList() ? typeof(ObservableCollection<>).MakeGenericType(parsedToken.Type.Nullify()) : parsedToken.Type; - - var val = value is JsonElement jtok ? - jtok.ToObject(expectedValueType, SignumServer.JsonSerializerOptions) : - value; - - if (val is DateTime dt) - val = dt.FromUserInterface(); - else if (val is ObservableCollection col) - val = col.Select(dt => dt?.FromUserInterface()).ToObservableCollection(); - - return new FilterCondition(parsedToken, operation, val); - } - - public override string ToString() => $"{token} {operation} {value}"; -} - -public class FilterGroupTS : FilterTS -{ - public FilterGroupOperation groupOperation; - public string? token; - public List filters; - - public override Filter ToFilter(QueryDescription qd, bool canAggregate) - { - var options = SubTokensOptions.CanElement | SubTokensOptions.CanAnyAll | (canAggregate ? SubTokensOptions.CanAggregate : 0); - var parsedToken = token == null ? null : QueryUtils.Parse(token, qd, options); - - var parsedFilters = filters.Select(f => f.ToFilter(qd, canAggregate)).ToList(); - - return new FilterGroup(groupOperation, parsedToken, parsedFilters); - } -} - -public class ColumnTS -{ - public string token; - public string displayName; - - public Column ToColumn(QueryDescription qd, bool canAggregate) - { - var queryToken = QueryUtils.Parse(token, qd, SubTokensOptions.CanElement | (canAggregate ? SubTokensOptions.CanAggregate : 0)); - - return new Column(queryToken, displayName ?? queryToken.NiceName()); - } - - public override string ToString() => $"{token} '{displayName}'"; - -} - -public class PaginationTS -{ - public PaginationMode mode; - public int? elementsPerPage; - public int? currentPage; - - public PaginationTS() { } - - public PaginationTS(Pagination pagination) - { - this.mode = pagination.GetMode(); - this.elementsPerPage = pagination.GetElementsPerPage(); - this.currentPage = (pagination as Pagination.Paginate)?.CurrentPage; - } - - public override string ToString() => $"{mode} {elementsPerPage} {currentPage}"; - - - public Pagination ToPagination() - { - return mode switch - { - PaginationMode.All => new Pagination.All(), - PaginationMode.Firsts => new Pagination.Firsts(this.elementsPerPage!.Value), - PaginationMode.Paginate => new Pagination.Paginate(this.elementsPerPage!.Value, this.currentPage!.Value), - _ => throw new InvalidOperationException($"Unexpected {mode}"), - }; - } -} - -public class SystemTimeTS -{ - public SystemTimeMode mode; - public SystemTimeJoinMode? joinMode; - public DateTimeOffset? startDate; - public DateTimeOffset? endDate; - - public SystemTimeTS() { } - - public SystemTimeTS(SystemTime systemTime) - { - if (systemTime is SystemTime.AsOf asOf) - { - mode = SystemTimeMode.AsOf; - startDate = asOf.DateTime; - } - else if (systemTime is SystemTime.Between between) - { - mode = SystemTimeMode.Between; - joinMode = ToSystemTimeJoinMode(between.JoinBehaviour); - startDate = between.StartDateTime; - endDate = between.EndtDateTime; - } - else if (systemTime is SystemTime.ContainedIn containedIn) - { - mode = SystemTimeMode.ContainedIn; - joinMode = ToSystemTimeJoinMode(containedIn.JoinBehaviour); - startDate = containedIn.StartDateTime; - endDate = containedIn.EndtDateTime; - } - else if (systemTime is SystemTime.All all) - { - mode = SystemTimeMode.All; - joinMode = ToSystemTimeJoinMode(all.JoinBehaviour); - startDate = null; - endDate = null; - } - else - throw new InvalidOperationException("Unexpected System Time"); - } - - public override string ToString() => $"{mode} {startDate} {endDate}"; - - - public SystemTime ToSystemTime() - { - return mode switch - { - SystemTimeMode.AsOf => new SystemTime.AsOf(startDate!.Value), - SystemTimeMode.Between => new SystemTime.Between(startDate!.Value, endDate!.Value, ToJoinBehaviour(joinMode!.Value)), - SystemTimeMode.ContainedIn => new SystemTime.ContainedIn(startDate!.Value, endDate!.Value, ToJoinBehaviour(joinMode!.Value)), - SystemTimeMode.All => new SystemTime.All(ToJoinBehaviour(joinMode!.Value)), - _ => throw new InvalidOperationException($"Unexpected {mode}"), - }; - } - - public static JoinBehaviour ToJoinBehaviour(SystemTimeJoinMode joinMode) - { - return joinMode switch - { - SystemTimeJoinMode.Current => JoinBehaviour.Current, - SystemTimeJoinMode.FirstCompatible => JoinBehaviour.FirstCompatible, - SystemTimeJoinMode.AllCompatible => JoinBehaviour.AllCompatible, - _ => throw new UnexpectedValueException(joinMode), - }; - } - - public static SystemTimeJoinMode ToSystemTimeJoinMode(JoinBehaviour joinBehaviour) - { - return joinBehaviour switch - { - JoinBehaviour.Current => SystemTimeJoinMode.Current, - JoinBehaviour.FirstCompatible => SystemTimeJoinMode.FirstCompatible, - JoinBehaviour.AllCompatible => SystemTimeJoinMode.AllCompatible, - _ => throw new UnexpectedValueException(joinBehaviour), - }; - } -} public class QueryDescriptionTS { diff --git a/Signum.React/Facades/SignumServer.cs b/Signum.React/Facades/SignumServer.cs index 2621bfe4eb..0d4461cb27 100644 --- a/Signum.React/Facades/SignumServer.cs +++ b/Signum.React/Facades/SignumServer.cs @@ -13,7 +13,6 @@ using Signum.Entities.Reflection; using Signum.React.ApiControllers; using Signum.React.Filters; -using Signum.React.Json; using Signum.React.JsonModelValidators; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Signum.React/JsonConverters/FilterJsonConverter.cs b/Signum.React/JsonConverters/FilterJsonConverter.cs deleted file mode 100644 index 3b20df9d1e..0000000000 --- a/Signum.React/JsonConverters/FilterJsonConverter.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Signum.Entities.DynamicQuery; -using Signum.React.ApiControllers; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Signum.React.Json; - -public class FilterJsonConverter : JsonConverter -{ - public override bool CanConvert(Type objectType) - { - return typeof(FilterTS).IsAssignableFrom(objectType); - } - - public override void Write(Utf8JsonWriter writer, FilterTS value, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - - public override FilterTS? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using (var doc = JsonDocument.ParseValue(ref reader)) - { - var elem = doc.RootElement; - - if (elem.TryGetProperty("operation", out var oper)) - { - return new FilterConditionTS - { - token = elem.GetProperty("token").GetString()!, - operation = oper.GetString()!.ToEnum(), - value = elem.TryGetProperty("value", out var val) ? val.ToObject(options) : null, - }; - } - - if (elem.TryGetProperty("groupOperation", out var groupOper)) - return new FilterGroupTS - { - groupOperation = groupOper.GetString()!.ToEnum(), - token = elem.TryGetProperty("token", out var token)? token.GetString() : null, - filters = elem.GetProperty("filters").EnumerateArray().Select(a => a.ToObject()!).ToList() - }; - - throw new InvalidOperationException("Impossible to determine type of filter"); - } - } -} - - diff --git a/Signum.React/JsonConverters/ResultTableConverter.cs b/Signum.React/JsonConverters/ResultTableConverter.cs deleted file mode 100644 index 61af5d43b9..0000000000 --- a/Signum.React/JsonConverters/ResultTableConverter.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Signum.Engine.Json; -using Signum.Entities.DynamicQuery; -using Signum.React.ApiControllers; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Signum.React.Json; - -public class ResultTableConverter : JsonConverter -{ - public override void Write(Utf8JsonWriter writer, ResultTable value, JsonSerializerOptions options) - { - using (HeavyProfiler.LogNoStackTrace("ReadJson", () => typeof(ResultTable).Name)) - { - var rt = (ResultTable)value!; - - writer.WriteStartObject(); - - writer.WritePropertyName("entityColumn"); - writer.WriteStringValue(rt.EntityColumn?.Name); - - writer.WritePropertyName("columns"); - JsonSerializer.Serialize(writer, rt.Columns.Select(c => c.Column.Token.FullKey()).ToList(), typeof(List), options); - - writer.WritePropertyName("pagination"); - JsonSerializer.Serialize(writer, new PaginationTS(rt.Pagination), typeof(PaginationTS), options); - - writer.WritePropertyName("totalElements"); - if (rt.TotalElements == null) - writer.WriteNullValue(); - else - writer.WriteNumberValue(rt.TotalElements!.Value); - - - writer.WritePropertyName("rows"); - writer.WriteStartArray(); - foreach (var row in rt.Rows) - { - writer.WriteStartObject(); - if (rt.EntityColumn != null) - { - writer.WritePropertyName("entity"); - JsonSerializer.Serialize(writer, row.Entity, options); - } - - writer.WritePropertyName("columns"); - writer.WriteStartArray(); - foreach (var column in rt.Columns) - { - using (EntityJsonContext.SetCurrentPropertyRouteAndEntity((column.Column.Token.GetPropertyRoute()!, null, null))) - { - JsonSerializer.Serialize(writer, row[column], options); - } - } - writer.WriteEndArray(); - - - writer.WriteEndObject(); - - } - writer.WriteEndArray(); - - - writer.WriteEndObject(); - } - } - - public override ResultTable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } -} - - diff --git a/Signum.React/Scripts/Exceptions/Exception.tsx b/Signum.React/Scripts/Exceptions/Exception.tsx index 99880cdbd7..24a0894406 100644 --- a/Signum.React/Scripts/Exceptions/Exception.tsx +++ b/Signum.React/Scripts/Exceptions/Exception.tsx @@ -64,7 +64,7 @@ export default function Exception(p: { ctx: TypeContext }) { } } -function FormatJson(p: { code: string | undefined | null }) { +export function FormatJson(p: { code: string | undefined | null }) { const [formatJson, setFormatJson] = React.useState(false); diff --git a/Signum.React/Scripts/FindOptions.ts b/Signum.React/Scripts/FindOptions.ts index 41008e5e3b..eff31bf3f8 100644 --- a/Signum.React/Scripts/FindOptions.ts +++ b/Signum.React/Scripts/FindOptions.ts @@ -348,21 +348,14 @@ export interface QueryValueRequest { systemTime?: SystemTime; } -export interface ResultColumn { - displayName: string; - token: QueryToken; -} - export interface ResultTable { - queryKey: string; - entityColumn: string; columns: string[]; + uniqueValues: { [token: string]: any[] } rows: ResultRow[]; pagination: Pagination totalElements?: number; } - export interface ResultRow { entity?: Lite; columns: any[]; diff --git a/Signum.React/Scripts/Finder.tsx b/Signum.React/Scripts/Finder.tsx index 3f55c29000..3dc2c8ef66 100644 --- a/Signum.React/Scripts/Finder.tsx +++ b/Signum.React/Scripts/Finder.tsx @@ -8,7 +8,7 @@ import { ajaxGet, ajaxPost } from './Services'; import { QueryDescription, QueryValueRequest, QueryRequest, QueryEntitiesRequest, FindOptions, FindOptionsParsed, FilterOption, FilterOptionParsed, OrderOptionParsed, ValueFindOptionsParsed, - QueryToken, ColumnDescription, ColumnOption, ColumnOptionParsed, Pagination, ResultColumn, + QueryToken, ColumnDescription, ColumnOption, ColumnOptionParsed, Pagination, ResultTable, ResultRow, OrderOption, SubTokensOptions, toQueryToken, isList, ColumnOptionsMode, FilterRequest, ModalFindOptions, OrderRequest, ColumnRequest, isFilterGroupOption, FilterGroupOptionParsed, FilterConditionOptionParsed, isFilterGroupOptionParsed, FilterGroupOption, FilterConditionOption, FilterGroupRequest, FilterConditionRequest, PinnedFilter, SystemTime, QueryTokenType, hasAnyOrAll, hasAggregate, hasElement, toPinnedFilterParsed, isActive } from './FindOptions'; @@ -1412,6 +1412,25 @@ export function useFetchAllLite(type: Type, deps?: any[]): return useAPI(() => API.fetchAllLites({ types: type.typeName }), deps ?? []) as Lite[] | undefined; } +export function decompress(rt: ResultTable): ResultTable { + var rows = rt.rows; + var columns = rt.columns; + + for (var i = 0; i < columns.length; i++) { + var uniqueValues = rt.uniqueValues[columns[i]]; + + if (uniqueValues != null) { + for (var j = 0; j < rows.length; j++) { + var row = rows[j]; + var index = row.columns[i] as number | null; + if (index != null) + row.columns[i] = uniqueValues[index]; + } + } + } + return rt; +} + export module API { export function fetchQueryDescription(queryKey: string): Promise { @@ -1429,8 +1448,9 @@ export module API { export function executeQuery(request: QueryRequest, signal?: AbortSignal): Promise { const queryUrl = AppContext.history.location.pathname + AppContext.history.location.search; - const qr: QueryRequestUrl = { ...request, queryUrl: queryUrl}; - return ajaxPost({ url: "~/api/query/executeQuery", signal }, qr); + const qr: QueryRequestUrl = { ...request, queryUrl: queryUrl }; + return ajaxPost({ url: "~/api/query/executeQuery", signal }, qr) + .then(rt => decompress(rt)); } export function queryValue(request: QueryValueRequest, avoidNotifyPendingRequest: boolean | undefined = undefined, signal?: AbortSignal): Promise { diff --git a/Signum.React/Scripts/Frames/FrameModal.tsx b/Signum.React/Scripts/Frames/FrameModal.tsx index 0d70c9d755..8e22e27493 100644 --- a/Signum.React/Scripts/Frames/FrameModal.tsx +++ b/Signum.React/Scripts/Frames/FrameModal.tsx @@ -265,18 +265,19 @@ export const FrameModal = React.forwardRef(function FrameModal(p: FrameModalProp allowExchangeEntity: p.buttons == "close" && (p.allowExchangeEntity ?? true), prefix: prefix, isExecuting: () => pc.executing == true, - execute: action => { + execute: async action => { if (pc.executing) return; pc.executing = true; forceUpdate(); - action() - .finally(() => { - pc.executing = undefined; - forceUpdate(); - }) - .done(); + try { + await action(); + + } finally { + pc.executing = undefined; + forceUpdate(); + } } }; diff --git a/Signum.React/Scripts/Frames/FramePage.tsx b/Signum.React/Scripts/Frames/FramePage.tsx index 9e26578d31..e790a9b71c 100644 --- a/Signum.React/Scripts/Frames/FramePage.tsx +++ b/Signum.React/Scripts/Frames/FramePage.tsx @@ -182,15 +182,18 @@ export default function FramePage(p: FramePageProps) { entityComponent: entityComponent.current, pack: state.pack, isExecuting: () => state.executing == true, - execute: action => { + execute: async action => { if (state.executing) return; state.executing = true; forceUpdate(); - action() - .finally(() => { state.executing = undefined; forceUpdate(); }) - .done(); + try { + await action(); + } finally { + state.executing = undefined; + forceUpdate(); + } }, onReload: (pack, reloadComponent, callback) => { diff --git a/Signum.React/Scripts/Operations.tsx b/Signum.React/Scripts/Operations.tsx index 9e486ac9fb..202b3c18d6 100644 --- a/Signum.React/Scripts/Operations.tsx +++ b/Signum.React/Scripts/Operations.tsx @@ -410,7 +410,7 @@ export class EntityOperationContext { return this.settings.onClick(this); else return defaultOnClick(this); - }); + }).done(); } textOrNiceName() { diff --git a/Signum.React/Scripts/Reflection.ts b/Signum.React/Scripts/Reflection.ts index 7727ddf4d8..3ca55b1ff3 100644 --- a/Signum.React/Scripts/Reflection.ts +++ b/Signum.React/Scripts/Reflection.ts @@ -892,7 +892,7 @@ export function createBinding(parentValue: any, lambdaMembers: LambdaMember[]): const functionRegex = /^function\s*\(\s*([$a-zA-Z_][0-9a-zA-Z_$]*)\s*\)\s*{\s*(\"use strict\"\;)?\s*(var [^;]*;)?\s*return\s*([^;]*)\s*;?\s*}$/; -const lambdaRegex = /^\s*\(?\s*([$a-zA-Z_][0-9a-zA-Z_$]*)\s*\)?\s*=>\s*{?\s*(return\s+)?([^;]*)\s*;?\s*}?$/; +const lambdaRegex = /^\s*\(?\s*([$a-zA-Z_][0-9a-zA-Z_$]*)\s*\)?\s*=>(\s*{?\s*(return\s+)?([^;]*)\s*;?\s*}?)$/; const memberRegex = /^(.*)\.([$a-zA-Z_][0-9a-zA-Z_$]*)$/; const memberIndexerRegex = /^(.*)\["([$a-zA-Z_][0-9a-zA-Z_$]*)"\]$/; const mixinMemberRegex = /^(.*)\.mixins\["([$a-zA-Z_][0-9a-zA-Z_$]*)"\]$/; //Necessary for some crazy minimizers diff --git a/Signum.React/Scripts/SearchControl/SearchControl.tsx b/Signum.React/Scripts/SearchControl/SearchControl.tsx index 9c2ed24577..0d0c38689a 100644 --- a/Signum.React/Scripts/SearchControl/SearchControl.tsx +++ b/Signum.React/Scripts/SearchControl/SearchControl.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import * as Finder from '../Finder' import { CellFormatter, EntityFormatter } from '../Finder' -import { ResultTable, ResultRow, FindOptions, FindOptionsParsed, FilterOptionParsed, FilterOption, QueryDescription } from '../FindOptions' +import { ResultTable, ResultRow, FindOptions, FindOptionsParsed, FilterOptionParsed, FilterOption, QueryDescription, QueryRequest } from '../FindOptions' import { Lite, Entity, ModifiableEntity, EntityPack } from '../Signum.Entities' import { tryGetTypeInfos, getQueryKey, getTypeInfos } from '../Reflection' import * as Navigator from '../Navigator' @@ -65,6 +65,7 @@ export interface SearchControlProps { onCreate?: (scl: SearchControlLoaded) => Promise | ModifiableEntity | "no_change">; onCreateFinished?: (entity: EntityPack | ModifiableEntity | Lite | undefined, scl: SearchControlLoaded) => void; styleContext?: StyleContext; + customRequest?: (req: QueryRequest, fop: FindOptionsParsed) => Promise, } export interface SearchControlState { @@ -221,6 +222,7 @@ const SearchControl = React.forwardRef(function SearchControl(p: SearchControlPr onResult={p.onResult} styleContext={p.styleContext} + customRequest={p.customRequest} /> ); diff --git a/Signum.React/Scripts/SearchControl/SearchControlLoaded.tsx b/Signum.React/Scripts/SearchControl/SearchControlLoaded.tsx index 840fc9ff1d..1c3e29f842 100644 --- a/Signum.React/Scripts/SearchControl/SearchControlLoaded.tsx +++ b/Signum.React/Scripts/SearchControl/SearchControlLoaded.tsx @@ -89,6 +89,7 @@ export interface SearchControlLoadedProps { onSearch?: (fo: FindOptionsParsed, dataChange: boolean) => void; onResult?: (table: ResultTable, dataChange: boolean) => void; styleContext?: StyleContext; + customRequest?: (req: QueryRequest, fop: FindOptionsParsed) => Promise, } export interface SearchControlLoadedState { @@ -225,8 +226,17 @@ export default class SearchControlLoaded extends React.Component Finder.API.executeQuery(request, signal)); - abortableSearchSummary = new AbortableRequest((signal, request: QueryRequest) => Finder.API.executeQuery(request, signal)); + abortableSearch = new AbortableRequest((signal, a: { + request: QueryRequest; + fop: FindOptionsParsed, + customRequest?: (req: QueryRequest, fop: FindOptionsParsed) => Promise + }) => a.customRequest ? a.customRequest(a.request, a.fop) : Finder.API.executeQuery(a.request, signal)); + + abortableSearchSummary = new AbortableRequest((signal, a: { + request: QueryRequest; + fop: FindOptionsParsed, + customRequest?: (req: QueryRequest, fop: FindOptionsParsed) => Promise + }) => a.customRequest ? a.customRequest(a.request, a.fop) : Finder.API.executeQuery(a.request, signal)); dataChanged(): Promise { if (this.isManualRefreshOrAllPagination()) { @@ -253,13 +263,15 @@ export default class SearchControlLoaded extends React.Component this.handleHeightChanged()); - var resultFindOptions = JSON.parse(JSON.stringify(this.props.findOptions)); + var resultFindOptions = JSON.parse(JSON.stringify(fop)); const qr = this.getQueryRequest(); const qrSummary = this.getSummaryQueryRequest(); - return Promise.all([this.abortableSearch.getData(qr), - qrSummary == null ? Promise.resolve(undefined) : this.abortableSearchSummary.getData(qrSummary) + const customRequest = this.props.customRequest; + + return Promise.all([this.abortableSearch.getData({ request: qr, fop, customRequest }), + qrSummary ? this.abortableSearchSummary.getData({ request: qrSummary, fop, customRequest }) : Promise.resolve(undefined) ]).then(([rt, summaryRt]) => { this.setState({ resultTable: rt, diff --git a/Signum.React/Scripts/SearchControl/ValueSearchControl.tsx b/Signum.React/Scripts/SearchControl/ValueSearchControl.tsx index 01f4015255..ea3b16b1ab 100644 --- a/Signum.React/Scripts/SearchControl/ValueSearchControl.tsx +++ b/Signum.React/Scripts/SearchControl/ValueSearchControl.tsx @@ -12,6 +12,7 @@ import { BsColor, BsSize } from '../Components'; import { toFilterRequests } from '../Finder'; import { PropertyRoute } from '../Lines' import * as Hooks from '../Hooks' + export interface ValueSearchControlProps extends React.Props { valueToken?: string | QueryTokenString; findOptions: FindOptions; @@ -35,14 +36,15 @@ export interface ValueSearchControlProps extends React.Props modalSize?: BsSize; onRender?: (value: any | undefined, vsc: ValueSearchControl) => React.ReactNode; htmlAttributes?: React.HTMLAttributes, + customRequest?: (req: QueryValueRequest, fop: FindOptionsParsed, token?: QueryToken) => Promise, } export interface ValueSearchControlState { value?: any; - token?: QueryToken; + valueToken?: QueryToken; } -function getQueryRequest(fo: FindOptionsParsed, valueToken?: string | QueryTokenString, multipleValues?: boolean): QueryValueRequest { +function getQueryRequestValue(fo: FindOptionsParsed, valueToken?: string | QueryTokenString, multipleValues?: boolean): QueryValueRequest { return { queryKey: fo.queryKey, @@ -67,8 +69,9 @@ export default class ValueSearchControl extends React.Component this.refreshValue(this.props, t)) + .done(); } } @@ -82,45 +85,67 @@ export default class ValueSearchControl extends React.Component { + if (newProps.initialValue == undefined) + this.refreshValue(newProps, valTok); + }) + .done(); + } } - loadToken(props: ValueSearchControlProps) { + loadToken(props: ValueSearchControlProps): Promise { - if (props.valueToken == (this.state.token && this.state.token.fullKey)) - return; + if (props.valueToken?.toString() == this.state.valueToken?.fullKey) + return Promise.resolve(this.state.valueToken); - this.setState({ token: undefined, value: undefined }); - if (props.valueToken) - Finder.parseSingleToken(props.findOptions.queryName, props.valueToken.toString(), SubTokensOptions.CanAggregate | SubTokensOptions.CanAnyAll | SubTokensOptions.CanElement) - .then(st => { - this.setState({ token: st }); - this.props.onTokenLoaded && this.props.onTokenLoaded(); - }) - .done(); + this.setState({ valueToken: undefined, value: undefined }); + if (!props.valueToken) + return Promise.resolve(undefined); + + return Finder.parseSingleToken(props.findOptions.queryName, props.valueToken.toString(), SubTokensOptions.CanAggregate | SubTokensOptions.CanAnyAll | SubTokensOptions.CanElement) + .then(st => { + this.setState({ valueToken: st }); + this.props.onTokenLoaded && this.props.onTokenLoaded(); + return st; + }); } componentWillUnmount() { this.abortableQuery.abort(); } - abortableQuery = new AbortableRequest<{ findOptions: FindOptions; valueToken?: string | QueryTokenString, multipleValues: boolean | undefined, avoidNotify: boolean | undefined }, number>( + abortableQuery = new AbortableRequest<{ + findOptions: FindOptions; + valueToken?: QueryToken, + multipleValues: boolean | undefined, + avoidNotify: boolean | undefined, + customRequest?: (req: QueryValueRequest, fop: FindOptionsParsed, token: QueryToken | undefined) => any + }, any>( (abortSignal, a) => Finder.getQueryDescription(a.findOptions.queryName) .then(qd => Finder.parseFindOptions(a.findOptions, qd, false)) - .then(fop => Finder.API.queryValue(getQueryRequest(fop, a.valueToken, a.multipleValues), a.avoidNotify, abortSignal))); + .then(fop => { + var req = getQueryRequestValue(fop, a.valueToken?.fullKey, a.multipleValues); - refreshValue(props?: ValueSearchControlProps) { - if (!props) + if (a.customRequest) + return a.customRequest(req, fop, a.valueToken); + else + return Finder.API.queryValue(req, a.avoidNotify, abortSignal); + })); + + refreshValue(props?: ValueSearchControlProps, token?: QueryToken | null) { + + if (props === undefined) props = this.props; + if (token === undefined) + token = this.state.valueToken; + var fo = props.findOptions; if (!Finder.isFindable(fo.queryName, false)) { @@ -132,7 +157,13 @@ export default class ValueSearchControl extends React.Component { const fixedValue = value === undefined ? null : value; this.setState({ value: fixedValue }); @@ -142,12 +173,12 @@ export default class ValueSearchControl extends React.Component 0 ? "count-with-results" : "count-no-results"), - s.token && (s.token.type.isLite || s.token!.type.isEmbedded) && "sf-entity-line-entity", + s.valueToken && (s.valueToken.type.isLite || s.valueToken!.type.isEmbedded) && "sf-entity-line-entity", p.formControlClass, p.formControlClass && this.isNumeric() && "numeric", p.formControlClass && this.isMultiLine() && "sf-multi-line", @@ -273,7 +304,7 @@ export default class ValueSearchControl extends React.Component { this.refreshValue(); this.props.onExplored && this.props.onExplored(); }) + .then(() => { this.refreshValue(this.props, this.state.valueToken); this.props.onExplored && this.props.onExplored(); }) .done(); } @@ -288,7 +319,7 @@ export default class ValueSearchControl extends React.Component { if (!this.props.avoidAutoRefresh) - this.refreshValue(this.props); + this.refreshValue(this.props, this.state.valueToken); if (this.props.onExplored) this.props.onExplored(); diff --git a/Signum.React/Scripts/SearchControl/ValueSearchControlLine.tsx b/Signum.React/Scripts/SearchControl/ValueSearchControlLine.tsx index b7492de96f..66f1134a36 100644 --- a/Signum.React/Scripts/SearchControl/ValueSearchControlLine.tsx +++ b/Signum.React/Scripts/SearchControl/ValueSearchControlLine.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { classes } from '../Globals' import * as Finder from '../Finder' import * as Constructor from '../Constructor' -import { FindOptions, QueryDescription } from '../FindOptions' +import { FindOptions, FindOptionsParsed, QueryDescription, QueryToken, QueryValueRequest } from '../FindOptions' import { Lite, Entity, isEntity, EntityControlMessage, isLite } from '../Signum.Entities' import { getQueryKey, getQueryNiceName, QueryTokenString, tryGetTypeInfos, getTypeInfos } from '../Reflection' import * as Navigator from '../Navigator' @@ -45,6 +45,7 @@ export interface ValueSearchControlLineProps extends React.Props void; onViewEntity?: (entity: Lite) => void; onValueChanged?: (value: any) => void; + customRequest?: (req: QueryValueRequest, fop: FindOptionsParsed, token?: QueryToken) => Promise, } export default class ValueSearchControlLine extends React.Component { @@ -101,7 +102,7 @@ export default class ValueSearchControlLine extends React.Component {unit} diff --git a/Signum.React/Scripts/TypeContext.ts b/Signum.React/Scripts/TypeContext.ts index 050ca59612..4e7c8a1265 100644 --- a/Signum.React/Scripts/TypeContext.ts +++ b/Signum.React/Scripts/TypeContext.ts @@ -482,7 +482,7 @@ export interface EntityFrame { allowExchangeEntity: boolean; isExecuting(): boolean; - execute: (action: () => Promise) => void; + execute: (action: () => Promise) => Promise; createNew?: (oldPack: EntityPack) => (Promise | undefined>) | undefined; prefix: string; diff --git a/Signum.React/tsconfig.json b/Signum.React/tsconfig.json index 8654a7f28d..faa618778f 100644 --- a/Signum.React/tsconfig.json +++ b/Signum.React/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2017", "sourceMap": false, "module": "esnext", "moduleResolution": "node",