diff --git a/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs b/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs index fa0f75943e..e00a69f534 100644 --- a/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs +++ b/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs @@ -1,6 +1,7 @@ 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; @@ -11,15 +12,21 @@ 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 Polymorphic>> GetCachedQueryDefinition = new(); + public static Polymorphic>> OnGetCachedQueryDefinition = new(); [AutoExpressionField] @@ -28,18 +35,20 @@ public static IQueryable CachedQueries(this DashboardEntity d [AutoExpressionField] public static IQueryable CachedQueries(this UserQueryEntity uq) => - As.Expression(() => Database.Query().Where(a => a.UserAsset.Is(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.UserAsset.Is(uc))); + As.Expression(() => Database.Query().Where(a => a.UserAssets.Contains(uc.ToLite()))); - public static void Start(SchemaBuilder sb) + 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 @@ -52,12 +61,12 @@ public static void Start(SchemaBuilder sb) {"UserTreePart", typeof(UserTreePartEntity)}, }); - GetCachedQueryDefinition.Register((UserChartPartEntity ucp, PanelPartEmbedded pp) => new[] { new CachedQueryDefinition(ucp.UserChart.ToChartRequest().ToQueryRequest(), pp, ucp.UserChart, ucp.CachedQuery, canWriteFilters: true) }); - GetCachedQueryDefinition.Register((CombinedUserChartPartEntity cucp, PanelPartEmbedded pp) => cucp.UserCharts.Select(uc => new CachedQueryDefinition(uc.ToChartRequest().ToQueryRequest(), pp, uc, cucp.CachedQuery, canWriteFilters: false))); - GetCachedQueryDefinition.Register((UserQueryPartEntity uqp, PanelPartEmbedded pp) => new[] { new CachedQueryDefinition(uqp.UserQuery.ToQueryRequest(), pp, uqp.UserQuery, uqp.CachedQuery, canWriteFilters: false) }); - GetCachedQueryDefinition.Register((LinkListPartEntity uqp, PanelPartEmbedded pp) => Array.Empty()); - GetCachedQueryDefinition.Register((ValueUserQueryListPartEntity vuql, PanelPartEmbedded pp) => vuql.UserQueries.Select(uqe => new CachedQueryDefinition(uqe.UserQuery.ToQueryRequest(), pp, uqe.UserQuery, uqe.CachedQuery, canWriteFilters: false))); - GetCachedQueryDefinition.Register((UserTreePartEntity ute, PanelPartEmbedded pp) => Array.Empty()); + 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.UserQuery.ToQueryRequest(), pp, uqp.UserQuery, uqp.IsQueryCached, canWriteFilters: false) }); + OnGetCachedQueryDefinition.Register((LinkListPartEntity uqp, PanelPartEmbedded pp) => Array.Empty()); + OnGetCachedQueryDefinition.Register((ValueUserQueryListPartEntity vuql, PanelPartEmbedded pp) => vuql.UserQueries.Select(uqe => new CachedQueryDefinition(uqe.UserQuery.ToQueryRequest(), pp, uqe.UserQuery, uqe.IsQueryCached, canWriteFilters: false))); + OnGetCachedQueryDefinition.Register((UserTreePartEntity ute, PanelPartEmbedded pp) => Array.Empty()); sb.Include() .WithQuery(() => cp => new @@ -71,7 +80,6 @@ public static void Start(SchemaBuilder sb) }); sb.Include() - .WithUniqueIndex(cq => new { cq.UserAsset, cq.Dashboard }) .WithExpressionFrom((DashboardEntity d) => d.CachedQueries()) .WithExpressionFrom((UserChartEntity d) => d.CachedQueries()) .WithExpressionFrom((UserQueryEntity d) => d.CachedQueries()) @@ -79,9 +87,14 @@ public static void Start(SchemaBuilder sb) { Entity = e, e.Id, - e.Dashboard, - e.UserAsset, + 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))) @@ -114,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; }; @@ -131,12 +143,9 @@ 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); }; } @@ -146,6 +155,9 @@ public static void Start(SchemaBuilder sb) 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(), @@ -181,18 +193,73 @@ public static void Register() new Execute(DashboardOperation.RegenerateCachedFiles) { + 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 list = DashboardLogic.GetCachedQueries(db).ToList(); + var definitions = DashboardLogic.GetCachedQueryDefinitions(db).ToList(); - foreach (var def in list) + var combined = DashboardLogic.CombineCachedQueryDefinitions(definitions); + + foreach (var c in combined) { - var rt = QueryLogic.Queries.ExecuteQuery(def.QueryRequest); + 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.Columns, 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(); } } @@ -297,6 +364,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)); @@ -335,18 +407,17 @@ public static void RegisterPartsTypeCondition(TypeConditionSymbol typeCondition) uqp => Database.Query().WhereCondition(typeCondition).Any(d => d.ContainsContent(uqp))); } - public static IEnumerable GetCachedQueries(DashboardEntity db) + public static List GetCachedQueryDefinitions(DashboardEntity db) { - var definitions = db.Parts.SelectMany(p => GetCachedQueryDefinition.Invoke(p.Content, p)); + var definitions = db.Parts.SelectMany(p => OnGetCachedQueryDefinition.Invoke(p.Content, p)).ToList(); var groups = definitions .Where(a => a.PanelPart.InteractionGroup != null) - .GroupBy(a => a.PanelPart.InteractionGroup) - .ToList(); + .GroupToDictionary(a => a.PanelPart.InteractionGroup!.Value); - foreach (var gr in groups) + foreach (var (key, value) in groups) { - var writers = gr.Where(a => a.CanWriteFilters).ToList(); + var writers = value.Where(a => a.CanWriteFilters).ToList(); if (!writers.Any()) continue; @@ -356,11 +427,21 @@ public static IEnumerable GetCachedQueries(DashboardEntit { var keyColumns = wr.QueryRequest.Columns.Where(c => c.Token is not AggregateToken); - foreach (var item in gr.Where(e => e != wr)) + foreach (var item in value.Where(e => e != wr)) { - var extraColumns = keyColumns.Where(k => !item.QueryRequest.Columns.Any(c => c.Token.Equals(k))).ToList(); - - item.QueryRequest.Columns.AddRange(extraColumns.Select(c => new Column(c.Token, null))); + 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(); } @@ -368,21 +449,43 @@ public static IEnumerable GetCachedQueries(DashboardEntit } } - var cached = definitions.Where(a => a.CachedQuery == CachedQuery.CachedFile); + 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, CachedQuery cachedQuery, bool canWriteFilters) + public CachedQueryDefinition(QueryRequest queryRequest, PanelPartEmbedded panelPart, IUserAssetEntity userAsset, bool isQueryCached, bool canWriteFilters) { QueryRequest = queryRequest; PanelPart = panelPart; Guid = userAsset.Guid; UserAsset = userAsset.ToLite(); - CachedQuery = cachedQuery; + IsQueryCached = isQueryCached; CanWriteFilters = canWriteFilters; } @@ -390,6 +493,141 @@ public CachedQueryDefinition(QueryRequest queryRequest, PanelPartEmbedded panelP public PanelPartEmbedded PanelPart { get; set; } public Guid Guid { get; set; } public Lite UserAsset { get; set; } - public CachedQuery CachedQuery { get; } + 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; + + var groupResults = me.GroupResults; + + if (me.Pagination is Pagination.All && meExtraFilters.Count == 0) + { + this.QueryRequest = WithExtraColumns(me, other); + + this.UserAssets.Add(definition.UserAsset); + + return true; + + } + + if (other.Pagination is Pagination.All && otherExtraFilters.Count == 0) + { + this.QueryRequest = WithExtraColumns(other, me); + + this.UserAssets.Add(definition.UserAsset); + + return true; + } + + if (me.Pagination.Equals(other.Pagination) && meExtraFilters.Count == 0 && otherExtraFilters.Count == 0 && 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.MachineLearning.TensorFlow/Signum.Engine.MachineLearning.TensorFlow.csproj b/Signum.Engine.MachineLearning.TensorFlow/Signum.Engine.MachineLearning.TensorFlow.csproj index e54296287f..3c6d49916f 100644 --- a/Signum.Engine.MachineLearning.TensorFlow/Signum.Engine.MachineLearning.TensorFlow.csproj +++ b/Signum.Engine.MachineLearning.TensorFlow/Signum.Engine.MachineLearning.TensorFlow.csproj @@ -13,7 +13,7 @@ - + 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 index c2039ac4d5..fd97d290d7 100644 --- a/Signum.Engine/Json/FilterJsonConverter.cs +++ b/Signum.Engine/Json/FilterJsonConverter.cs @@ -15,7 +15,12 @@ public override bool CanConvert(Type objectType) public override void Write(Utf8JsonWriter writer, FilterTS value, JsonSerializerOptions options) { - throw new NotImplementedException(); + 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) @@ -121,7 +126,7 @@ public override Filter ToFilter(QueryDescription qd, bool canAggregate, JsonSeri public class ColumnTS { public string token; - public string displayName; + public string? displayName; public Column ToColumn(QueryDescription qd, bool canAggregate) { @@ -280,7 +285,22 @@ public class QueryRequestTS public List orders; public List columns; public PaginationTS pagination; - public SystemTimeTS/*?*/ systemTime; + 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) { diff --git a/Signum.Entities.Extensions/Dashboard/CachedQuery.cs b/Signum.Entities.Extensions/Dashboard/CachedQuery.cs index 8f19926363..1ae3f16b27 100644 --- a/Signum.Entities.Extensions/Dashboard/CachedQuery.cs +++ b/Signum.Entities.Extensions/Dashboard/CachedQuery.cs @@ -5,16 +5,29 @@ namespace Signum.Entities.Dashboard; -[EntityKind(EntityKind.System, EntityData.Master), TicksColumn(false)] +[EntityKind(EntityKind.System, EntityData.Master)] public class CachedQueryEntity : Entity { public Lite Dashboard { get; set; } + [PreserveOrder, NoRepeatValidator] [ImplementedBy(typeof(UserQueryEntity), typeof(UserChartEntity))] - public Lite UserAsset { get; set; } + 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] diff --git a/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs b/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs index fc22e84a2f..e690f4242a 100644 --- a/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs +++ b/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs @@ -43,7 +43,7 @@ public Lite? EntityType public bool CombineSimilarRows { get; set; } = true; - public CachedQueriesEmbedded? CachedQueries { get; set; } + public CacheQueryConfigurationEmbedded? CacheQueryConfiguration { get; set; } [NotifyCollectionChanged, NotifyChildProperty] [NoRepeatValidator] @@ -123,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 }; } @@ -144,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)))); } @@ -156,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)); } @@ -170,7 +174,7 @@ public void FromXml(XElement element, IFromXmlContext ctx) return ValidationMessage._0IsNotAllowed.NiceToString(pi.NiceName()); } - if(pi.Name == nameof(CachedQueries) && CachedQueries != null && EntityType != null) + if(pi.Name == nameof(CacheQueryConfiguration) && CacheQueryConfiguration != null && EntityType != null) { return ValidationMessage._0ShouldBeNullWhen1IsSet.NiceToString(pi.NiceName(), NicePropertyName(() => EntityType)); } @@ -179,10 +183,29 @@ public void FromXml(XElement element, IFromXmlContext ctx) } } -public class CachedQueriesEmbedded : EmbeddedEntity +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] diff --git a/Signum.Entities.Extensions/Dashboard/PanelPart.cs b/Signum.Entities.Extensions/Dashboard/PanelPart.cs index 1c08f75b70..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,18 +129,13 @@ public interface IPartEntity : IEntity void FromXml(XElement element, IFromXmlContext ctx); } -public enum CachedQuery -{ - DatabaseQuery, - CachedFile -} [EntityKind(EntityKind.Part, EntityData.Master)] public class UserQueryPartEntity : Entity, IPartEntity { public UserQueryEntity UserQuery { get; set; } - public CachedQuery CachedQuery { get; set; } + public bool IsQueryCached { get; set; } public UserQueryPartRenderMode RenderMode { get; set; } @@ -165,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; } } @@ -221,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)) ); } @@ -249,7 +247,7 @@ public class UserChartPartEntity : Entity, IPartEntity { public UserChartEntity UserChart { get; set; } - public CachedQuery CachedQuery { get; set; } + public bool IsQueryCached { get; set; } public bool ShowData { get; set; } = false; @@ -267,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) @@ -301,9 +301,7 @@ public void FromXml(XElement element, IFromXmlContext ctx) public class CombinedUserChartPartEntity : Entity, IPartEntity { [PreserveOrder, NoRepeatValidator] - public MList UserCharts { get; set; } = new MList(); - - public CachedQuery CachedQuery { get; set; } + public MList UserCharts { get; set; } = new MList(); public bool ShowData { get; set; } = false; @@ -323,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 @@ -368,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) { @@ -395,7 +410,7 @@ public class ValueUserQueryElementEmbedded : EmbeddedEntity public UserQueryEntity UserQuery { get; set; } - public CachedQuery CachedQuery { get; set; } + public bool IsQueryCached { get; set; } [StringLengthValidator(Max = 200)] public string? Href { get; set; } @@ -407,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 1852f960d1..1ae78485c3 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. @@ -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 int TopElements { get; } - public override int? MaxElementIndex - { - get { return TopElements; } - } - - public override PaginationMode GetMode() - { - return PaginationMode.Firsts; - } - - 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 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 int TotalPages(int totalElements) - { - return (totalElements + ElementsPerPage - 1) / ElementsPerPage; //Round up - } - - public override int? MaxElementIndex - { - get { return (ElementsPerPage * (CurrentPage + 1)) - 1; } - } - - 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; } } diff --git a/Signum.React.Extensions/Chart/ChartClient.tsx b/Signum.React.Extensions/Chart/ChartClient.tsx index 7cc66d08f0..895faa43de 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..53a920691e 100644 --- a/Signum.React.Extensions/Dashboard/Admin/CombinedUserChartPart.tsx +++ b/Signum.React.Extensions/Dashboard/Admin/CombinedUserChartPart.tsx @@ -1,22 +1,33 @@ 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%" } }, + }, + ])} + /> 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 2a765b2258..0825cfea12 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 } 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,11 @@ 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'; -export default function Dashboard(p : { ctx: TypeContext }){ +export default function Dashboard(p: { ctx: TypeContext }) { const forceUpdate = useForceUpdate(); function handleEntityTypeChange() { if (!p.ctx.value.entityType) @@ -86,44 +89,64 @@ 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 }] }} />} +
+
} /> + + 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/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/CachedQuery.ts b/Signum.React.Extensions/Dashboard/CachedQuery.ts index 1046674b81..754e158d79 100644 --- a/Signum.React.Extensions/Dashboard/CachedQuery.ts +++ b/Signum.React.Extensions/Dashboard/CachedQuery.ts @@ -1,7 +1,10 @@ import * as Finder from '@framework/Finder' -import { ColumnRequest, FilterOperation, FilterRequest, isFilterGroupRequest, OrderRequest, Pagination, QueryRequest, QueryToken, ResultRow, ResultTable } from '@framework/FindOptions' +import { ColumnRequest, FilterOperation, FilterOptionParsed, FilterRequest, isFilterGroupOptionParsed, isFilterGroupRequest, OrderRequest, Pagination, QueryRequest, QueryToken, ResultRow, ResultTable } from '@framework/FindOptions' import { Entity, is, Lite } from '@framework/Signum.Entities'; -import { TypeAllowedAndConditions } from '../Authorization/Signum.Entities.Authorization'; +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 CachedQuery { @@ -10,34 +13,63 @@ export interface CachedQuery { resultTable: ResultTable; } -export function getCachedResultTable(cachedQuery: CachedQuery, request: QueryRequest, parsedTokens: { [token: string]: QueryToken }): ResultTable | string /*Error*/ { + +export function executeChartCached(request: ChartRequestModel, chartScript: ChartClient.ChartScript, cachedQuery: CachedQuery): 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: CachedQuery, request: QueryRequest, parsedTokens: { [token: string]: QueryToken }): ResultTable { if (request.queryKey != cachedQuery.queryRequest.queryKey) - return "Invalid queryKey"; + throw new CachedQueryError("Invalid queryKey"); - var pagProblems = paginatinProblems(request.pagination, cachedQuery.queryRequest.pagination); - if (typeof pagProblems == "string") - return pagProblems; + var pagProblems = pagionationRestriction(request.pagination, cachedQuery.queryRequest.pagination); - const onlyIfExactFiltersAndOrders = typeof pagProblems == "symbol"; + const exactFiltersAndOrders = pagProblems == "ExactFiltersAndOrders"; const sameOrders = ordersEquals(cachedQuery.queryRequest.orders, request.orders) - if (!sameOrders && onlyIfExactFiltersAndOrders) - return "Incompatible pagination if the orders are not identical"; + if (!sameOrders && exactFiltersAndOrders) + throw new CachedQueryError("Incompatible pagination if the orders are not identical"); const extraFilters = extractRequestedFilters(cachedQuery.queryRequest.filters, request.filters); - if (typeof extraFilters == "string") - return extraFilters; - - if (extraFilters.length && onlyIfExactFiltersAndOrders) - return "Incompatible pagination if the filters are not identical"; + if (extraFilters.length && exactFiltersAndOrders) + throw new CachedQueryError("Incompatible pagination if the filters are not identical"); if (request.groupResults) { - if (onlyIfExactFiltersAndOrders) { + if (exactFiltersAndOrders) { if (!cachedQuery.queryRequest.groupResults) - return "Incompatible pagination if the request is grouping but the cached query is not"; + 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"); @@ -45,55 +77,39 @@ export function getCachedResultTable(cachedQuery: CachedQuery, request: QueryReq const cachedKeyColumns = cachedQuery.queryRequest.columns.map(a => parsedTokens[a.token].queryTokenType != "Aggregate"); var extraColumns = cachedKeyColumns.filter(c => !requestKeyColumns.contains(c)); - if (extraColumns.length && onlyIfExactFiltersAndOrders) - return "Incompatible pagination if the key columns are not identical"; + 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); - if (typeof filtered == "string") - return filtered; - + 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); - if (typeof grouped == "string") - return grouped; - + const reFiltered = filterRows(grouped, aggregateFilters); - if (typeof reFiltered == "string") - return reFiltered; - + const ordered = sameOrders ? reFiltered : orderRows(reFiltered, request.orders, parsedTokens); - if (typeof ordered == "string") - return ordered; - + const select = selectRows(ordered, request.columns); - if (typeof select == "string") - return select; - + const paginate = paginateRows(select, request.pagination); return paginate; } else { if (cachedQuery.queryRequest.groupResults) - return "Cached query is grouping but request is not"; + throw new CachedQueryError("Cached query is grouping but request is not"); else { const filtered = filterRows(cachedQuery.resultTable, extraFilters); - if (typeof filtered == "string") - return filtered; - + const ordered = sameOrders ? filtered : orderRows(filtered, request.orders, parsedTokens); - if (typeof ordered == "string") - return ordered; - + const select = selectRows(ordered, request.columns); - if (typeof select == "string") - return select; - + const paginate = paginateRows(select, request.pagination); return paginate; } @@ -101,13 +117,12 @@ export function getCachedResultTable(cachedQuery: CachedQuery, request: QueryReq } -function groupByRows(rt: ResultTable, alreadyGrouped: boolean, tokens: string[], parsedTokens: { [token: string]: QueryToken }): ResultTable | string { +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]; @@ -123,7 +138,26 @@ function groupByRows(rt: ResultTable, alreadyGrouped: boolean, tokens: string[], var result: ResultRow[]; - function getGetter(token: string): string | ((gr: ResultRow[]) => any) { + 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); @@ -135,9 +169,7 @@ function groupByRows(rt: ResultTable, alreadyGrouped: boolean, tokens: string[], if (qt.key == "Count") return gr => gr.length; - const index = rt.columns.indexOf(qt.parent!.fullKey); - if (index == -1) - return qt.parent!.fullKey; + const index = getColumnIndex(qt.parent!.fullKey); switch (qt.key) { case "Min": return rows => rows.map(a => a.columns[index]).min(); @@ -151,28 +183,19 @@ function groupByRows(rt: ResultTable, alreadyGrouped: boolean, tokens: string[], } else { if (qt.key == "Count") { - const indexCount = rt.columns.indexOf(qt.fullKey); - if (indexCount == -1) - return qt.fullKey; + const indexCount = getColumnIndex(qt.fullKey); return rows => rows.sum(a => a.columns[indexCount]); - } else if (qt.key == "Avg") { + } else if (qt.key == "Average") { const sumToken = qt.parent!.fullKey + ".Sum"; - const indexSum = rt.columns.indexOf(sumToken); - if (indexSum == -1) - return sumToken; + const indexSum = getColumnIndex(sumToken); const countToken = qt.parent!.fullKey + ".CountNotNull"; - const indexCount = rt.columns.indexOf(countToken); - if (indexCount == -1) - return countToken; - - return rows => rows.sum(a => a.columns[indexSum]) / rows.sum(a => a.columns[indexCount]); + const indexCount2 = getColumnIndex(countToken); + return rows => rows.sum(a => a.columns[indexSum]) / rows.sum(a => a.columns[indexCount2]); } else { - const index = rt.columns.indexOf(qt.parent!.fullKey); - if (index == -1) - return qt.parent?.fullKey!; + const index = tryColumnIndex(qt.fullKey) ?? getColumnIndex(qt.parent!.fullKey); switch (qt.key) { case "Min": return rows => rows.map(a => a.columns[index]).min(); @@ -186,18 +209,12 @@ function groupByRows(rt: ResultTable, alreadyGrouped: boolean, tokens: string[], throw new Error("Unexpected " + token); } + var getters = tokens.map(t => { + var g = getGetter(t); - - var getters: ((gr: ResultRow[]) => any)[] = []; - for (let i = 0; i < tokens.length; i++) { - var g = getGetter(tokens[i]); - - if (typeof g == "string") { - return `Column ${g} not found` + (g != tokens[i]) ? `(required for ${tokens[i]})` : ""; - } - - getters.push(g); - } + + return g; + }); const newRows: ResultRow[] = []; groups.forEach(rows => { @@ -229,20 +246,24 @@ function getRowKey(rt: ResultTable, keyTokens: string[], parsedTokens: { [token: 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 `(rt.columns[${index}] && (rt.columns[${index}].EntityType + ";" + rt.columns[${index}].id))` + return `(rr.columns[${index}] && (rr.columns[${index}].EntityType + ";" + rr.columns[${index}].id))` - return `rt.columns[${index}]`; + return `rr.columns[${index}]`; } - const parts = keyTokens.map(a => columnKey(a)).join("|"); + + 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 | string { +function orderRows(rt: ResultTable, orders: OrderRequest[], parseTokens: { [token: string]: QueryToken }): ResultTable { var newRows = Array.from(rt.rows); @@ -255,7 +276,7 @@ function orderRows(rt: ResultTable, orders: OrderRequest[], parseTokens: { [toke var index = rt.columns.indexOf(o.token); if (index == -1) - return "Unable to order by token " + o.token; + throw new CachedQueryError("Unable to order by token " + o.token); if (o.orderType == "Ascending") { if (pt.filterType == "Lite") @@ -281,13 +302,13 @@ function orderRows(rt: ResultTable, orders: OrderRequest[], parseTokens: { [toke } -function selectRows(rt: ResultTable, columns: ColumnRequest[]): ResultTable | string { +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) - return "Unable to select by token " + columns[i].token; + throw new CachedQueryError("Unable to select by token " + columns[i].token); indexes.push(idx); } @@ -312,19 +333,16 @@ function selectRows(rt: ResultTable, columns: ColumnRequest[]): ResultTable | st }); } -function filterRows(rt: ResultTable, filters: FilterRequest[]): ResultTable | string{ +function filterRows(rt: ResultTable, filters: FilterRequest[]): ResultTable{ if (filters.length == 0) return rt; if (rt.pagination.mode != "All") - return "Unable to filter " + rt.pagination.mode; + throw new CachedQueryError("Unable to filter " + rt.pagination.mode); var filterer = createFilterer(rt, filters); - if (typeof filterer == "string") - return filterer; - var newRows = filterer(rt.rows); return { @@ -336,26 +354,35 @@ function filterRows(rt: ResultTable, filters: FilterRequest[]): ResultTable | st }; } -function createFilterer(result: ResultTable, filters: FilterRequest[]): ((rows: ResultRow[]) => ResultRow[]) | string{ +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 | string[] /*errors*/{ + function getExpression(f: FilterRequest): string { if (isFilterGroupRequest(f)) { const parts = f.filters.map(ff => getExpression(ff)); - var errors = parts.filter(a => Array.isArray(a)).flatMap(a => a as string[]); - if (errors.length > 0) - return errors; - if (f.groupOperation == "Or") return "( " + parts.join(" || ") + ")"; return parts.join(" && "); @@ -365,20 +392,20 @@ function createFilterer(result: ResultTable, filters: FilterRequest[]): ((rows: var index = result.columns.indexOf(f.token); if (index == -1) - return [f.token]; + throw new CachedQueryError(f.token); var op = "cls[" + index + "]"; if (f.operation == "IsIn" || f.operation == "IsNotIn") { var values = f.value as unknown[]; - var exps = allValues.map(v => op + "===" + getVarName(v)).join(" || "); + var exps = allValues.map(v => op + "===" + getVarName(getUniqueValue(v, f.token))).join(" || "); return f.operation == "IsIn" ? exps : ("!(" + exps + ")"); } else { - var vn = getVarName(f.value); + var vn = getVarName(getUniqueValue(f.value, f.token)); switch (f.operation) { case "EqualTo": return `${op} === ${vn}`; case "DistinctTo": return `${op} !== ${vn}`; @@ -393,8 +420,8 @@ function createFilterer(result: ResultTable, filters: FilterRequest[]): ((rows: case "StartsWith": return `${op} !== null && ${op}.startsWith(${vn})`; case "NotStartsWith": return `!(${op} !== null && ${op}.startsWith(${vn}))`; - case "Like": return ["Like not supported"]; - case "NotLike": return ["NotLike not supported"]; + case "Like": throw new CachedQueryError("Like not supported"); + case "NotLike": throw new CachedQueryError("NotLike not supported"); default: throw new Error("Unexpected " + f.operation); } @@ -411,7 +438,7 @@ function createFilterer(result: ResultTable, filters: FilterRequest[]): ((rows: const result = []; for(let i = 0; i < rows.length; i++) { var cls = rows[i].columns; - if (expression) { + if (${expression}) { result.push(rows[i]); } } @@ -421,29 +448,6 @@ function createFilterer(result: ResultTable, filters: FilterRequest[]): ((rows: return factory(...allValues); } - - -function splitAggregate(token: string) { - const suffix = token.tryAfterLast(".") ?? token; - - const prefix = token.tryBeforeLast("."); - - if ( - suffix == "Count" || - suffix == "Average" || - suffix == "Sum" || - suffix == "Min" || - suffix == "Max") { - return ({ aggregate: suffix, token: prefix }); - } - - - return ({ aggregate: null, token: token }); -} - - - - function ordersEquals(cached: OrderRequest[], requested: OrderRequest[]) { if (cached.length != requested.length) return false; @@ -459,7 +463,7 @@ function ordersEquals(cached: OrderRequest[], requested: OrderRequest[]) { return true; } -function extractRequestedFilters(cached: FilterRequest[], request: FilterRequest[]): FilterRequest[] | string { +function extractRequestedFilters(cached: FilterRequest[], request: FilterRequest[]): FilterRequest[] { var cloned = JSON.parse(JSON.stringify(request)) as FilterRequest[]; @@ -470,7 +474,7 @@ function extractRequestedFilters(cached: FilterRequest[], request: FilterRequest const toRemove = cloned.filter(rf => equalFilter(c, rf)); if (toRemove.length) - return "Cached filter not found in requet"; + throw new CachedQueryError("Cached filter not found in requet"); toRemove.forEach(r => cloned.remove(r)); } @@ -510,7 +514,7 @@ function equalFilter(c: FilterRequest, r: FilterRequest): boolean { } } -function paginateRows(rt: ResultTable, req: Pagination): ResultTable | string { +function paginateRows(rt: ResultTable, req: Pagination): ResultTable{ switch (rt.pagination.mode) { case "All": { @@ -529,7 +533,7 @@ function paginateRows(rt: ResultTable, req: Pagination): ResultTable | string { if (rt.pagination.currentPage == 1 && req.elementsPerPage! <= rt.pagination.elementsPerPage!) return { ...rt, rows: rt.rows.slice(0, rt.pagination.elementsPerPage), pagination: req }; - return `Invalid first`; + throw new CachedQueryError(`Invalid first`); case "Paginate": if (((req.elementsPerPage! == rt.pagination.elementsPerPage! && req.currentPage == rt.pagination.currentPage) || @@ -539,7 +543,7 @@ function paginateRows(rt: ResultTable, req: Pagination): ResultTable | string { return { ...rt, rows: rt.rows.slice(startIndex, startIndex + rt.pagination.elementsPerPage!), pagination: req }; } - return "Invalid paginate"; + throw new CachedQueryError("Invalid paginate"); } } case "Firsts": { @@ -550,13 +554,13 @@ function paginateRows(rt: ResultTable, req: Pagination): ResultTable | string { throw new Error(`Invalid first`); case "Paginate": - case "All": return `Requesting ${req.mode} but cached is ${rt.pagination.mode}`; + case "All": throw new CachedQueryError(`Requesting ${req.mode} but cached is ${rt.pagination.mode}`); } } } } -function paginatinProblems(req: Pagination, cached: Pagination): symbol| string | null { +function pagionationRestriction(req: Pagination, cached: Pagination): null | "ExactFiltersAndOrders" { switch (cached.mode) { case "All": return null; @@ -566,20 +570,20 @@ function paginatinProblems(req: Pagination, cached: Pagination): symbol| string case "Firsts": { if (cached.currentPage == 1 && req.elementsPerPage! <= cached.elementsPerPage!) - return Symbol("OnlyIfExactFiltersAndOrders"); + return "ExactFiltersAndOrders"; - return "Invalid First"; + 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 Symbol("OnlyIfExactFiltersAndOrders"); + return "ExactFiltersAndOrders"; - return "Invalid Paginate"; + throw new CachedQueryError("Invalid Paginate"); } - case "All": return `Requesting ${req.mode} but cached is ${cached.mode}`; + case "All": throw new CachedQueryError(`Requesting ${req.mode} but cached is ${cached.mode}`); } } @@ -588,27 +592,13 @@ function paginatinProblems(req: Pagination, cached: Pagination): symbol| string switch (req.mode) { case "Firsts": { if (req.elementsPerPage! <= cached.elementsPerPage!) - return Symbol("OnlyIfExactFiltersAndOrders"); + return "ExactFiltersAndOrders"; - return "Invalid First"; + throw new CachedQueryError("Invalid First"); } case "Paginate": - case "All": return `Requesting ${req.mode} but cached is ${cached.mode}`; + case "All": throw new CachedQueryError(`Requesting ${req.mode} but cached is ${cached.mode}`); } } } - - if (cached.mode == "All") - return null; - - if (cached.mode == "Paginate") - - if (cached.mode == "Firsts") { - if (req.mode == "Firsts") { - - } - } - - - throw new Error("Unexpected value"); } diff --git a/Signum.React.Extensions/Dashboard/DashboardClient.tsx b/Signum.React.Extensions/Dashboard/DashboardClient.tsx index 3e0171573f..190bfdc487 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 { CachedQuery } from './CachedQuery'; 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,8 @@ 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.RegenerateCachedFiles, { hideOnCanExecute: true, color: "warning", icon: "cogs" })); + Finder.addSettings({ queryName: DashboardEntity, defaultOrders: [{ token: DashboardEntity.token(d => d.dashboardPriority), orderType: "Descending" }] @@ -106,8 +112,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 +125,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 +222,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 +252,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 +288,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..ac89494f2c 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 RegenerateCachedFiles : Entities.ExecuteSymbol = registerSymbol("Operation", "DashboardOperation.RegenerateCachedFiles"); 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 529be13254..1e81aa7b3d 100644 --- a/Signum.React.Extensions/Dashboard/View/CombinedUserChartPart.tsx +++ b/Signum.React.Extensions/Dashboard/View/CombinedUserChartPart.tsx @@ -35,7 +35,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); diff --git a/Signum.React.Extensions/Dashboard/View/DashboardPage.tsx b/Signum.React.Extensions/Dashboard/View/DashboardPage.tsx index c71dedab73..5ede0ea001 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 { CachedQuery } from '../CachedQuery' 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); debugger; 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 710f992f79..b3747fb7bf 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 { CachedQuery } from '../CachedQuery' - - -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 d9d300a7d8..5174122cdd 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 { CachedQuery, executeChartCached } from '../CachedQuery' 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 &&