Skip to content

Commit

Permalink
more on Token Equivalences
Browse files Browse the repository at this point in the history
  • Loading branch information
olmobrutall committed Dec 25, 2021
1 parent 21aafb1 commit 85b2648
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 53 deletions.
78 changes: 52 additions & 26 deletions Signum.Engine.Extensions/Dashboard/DashboardLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static void Start(SchemaBuilder sb, IFileTypeAlgorithm cachedQueryAlgorit
OnGetCachedQueryDefinition.Register((LinkListPartEntity uqp, PanelPartEmbedded pp) => Array.Empty<CachedQueryDefinition>());

sb.Include<DashboardEntity>()
.WithVirtualMList(a => a.TokenEquivalences, e => e.Dashboard)
.WithVirtualMList(a => a.TokenEquivalencesGroups, e => e.Dashboard)
.WithQuery(() => cp => new
{
Entity = cp,
Expand All @@ -83,6 +83,9 @@ public static void Start(SchemaBuilder sb, IFileTypeAlgorithm cachedQueryAlgorit
cp.DashboardPriority,
});

sb.Schema.EntityEvents<DashboardEntity>().Retrieved += DashboardLogic_Retrieved; ;


sb.Include<CachedQueryEntity>()
.WithExpressionFrom((DashboardEntity d) => d.CachedQueries())
.WithExpressionFrom((UserChartEntity d) => d.CachedQueries())
Expand Down Expand Up @@ -175,6 +178,18 @@ public static void Start(SchemaBuilder sb, IFileTypeAlgorithm cachedQueryAlgorit
}
}

private static void DashboardLogic_Retrieved(DashboardEntity db, PostRetrievingContext ctx)
{
db.ParseData(query =>
{
object? queryName = query.ToQueryNameCatch();
if (queryName == null)
return null;
return QueryLogic.Queries.QueryDescription(queryName);
});
}

class DashboardGraph : Graph<DashboardEntity>
{
public static void Register()
Expand Down Expand Up @@ -432,41 +447,40 @@ public static List<CachedQueryDefinition> GetCachedQueryDefinitions(DashboardEnt
if (!writers.Any())
continue;

var equivalences = db.TokenEquivalences.Where(a => a.InteractionGroup == key || a.InteractionGroup == null);
var equivalences = db.TokenEquivalencesGroups.Where(a => a.InteractionGroup == key || a.InteractionGroup == null);

foreach (var wr in writers)
{
if (wr.QueryRequest.GroupResults)
{
var keyColumns = wr.QueryRequest.Columns.Where(c => c.Token is not AggregateToken);
var keyColumns = wr.QueryRequest.GroupResults ?
wr.QueryRequest.Columns.Where(c => c.Token is not AggregateToken) :
wr.QueryRequest.Columns;

var equivalencesDictionary = (from gr in equivalences
from t in gr.TokenEquivalences.Where(a => a.Query.ToQueryName() == wr.QueryRequest.QueryName)
select KeyValuePair.Create(t.QueryToken.Token, gr.TokenEquivalences.GroupToDictionary(a => a.Query.ToQueryName(), a => a.QueryToken.Token)))
.ToDictionaryEx();
var equivalencesDictionary = (from gr in equivalences
from t in gr.TokenEquivalences.Where(a => a.Query.ToQueryName() == wr.QueryRequest.QueryName)
select KeyValuePair.Create(t.Token.Token, gr.TokenEquivalences.GroupToDictionary(a => a.Query.ToQueryName(), a => a.Token.Token)))
.ToDictionaryEx();

foreach (var cqd in cqdefs.Where(e => e != wr))
foreach (var cqd in cqdefs.Where(e => e != wr))
{
var extraColumns = keyColumns.Select(k =>
{
var extraColumns = keyColumns.Select(k =>
{
var translatedToken = TranslatedToken(k.Token, cqd.QueryRequest.QueryName, equivalencesDictionary);
var translatedToken = TranslatedToken(k.Token, cqd.QueryRequest.QueryName, equivalencesDictionary);
if (translatedToken == null)
return null;
if (translatedToken == null)
return null;
if (!cqd.QueryRequest.Columns.Any(c => translatedToken.Contains(c.Token)))
return translatedToken.FirstEx(); //Doesn't really matter if we add "Product" or "Entity.Product";
if (!cqd.QueryRequest.Columns.Any(c => translatedToken.Contains(c.Token)))
return translatedToken.FirstEx(); //Doesn't really matter if we add "Product" or "Entity.Product";
return null;
}).NotNull().ToList();
}).NotNull().ToList();

if (extraColumns.Any())
{
ExpandColumns(cqd, extraColumns);
}

cqd.QueryRequest.Pagination = new Pagination.All();
if (extraColumns.Any())
{
ExpandColumns(cqd, extraColumns);
}

cqd.QueryRequest.Pagination = new Pagination.All();
}
}
}
Expand Down Expand Up @@ -499,10 +513,20 @@ private static void ExpandColumns(CachedQueryDefinition cqd, List<QueryToken> ex
var toAppend = new List<QueryToken>();
for (var t = original; t != null; t = t.Parent)
{
if (equivalences.TryGetValue(t, out var dic) && dic.TryGetValue(targetQueryName, out var list))
return list.Select(t => AppendTokens(t, toAppend)).ToList();
{
if (equivalences.TryGetValue(t, out var dic) && dic.TryGetValue(targetQueryName, out var list))
return list.Select(t => AppendTokens(t, toAppend)).ToList();
}

toAppend.Insert(0, t);

if(t.Parent == null)
{
var entityToken = QueryUtils.Parse("Entity", QueryLogic.Queries.QueryDescription(original.QueryName), 0);

if(equivalences.TryGetValue(entityToken, out var dic) && dic.TryGetValue(targetQueryName, out var list))
return list.Select(t => AppendTokens(t, toAppend)).ToList();
}
}

if (original.QueryName == targetQueryName)
Expand All @@ -522,6 +546,8 @@ private static QueryToken AppendTokens(QueryToken t, List<QueryToken> toAppend)

if (newToken == null)
throw new FormatException("Token with key '{0}' not found on {1} of query {2}".FormatWith(nt.Key, t, QueryUtils.GetKey(qd.QueryName)));

t = newToken;
}

return t;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public IUserAssetEntity GetEntity(Guid guid)
Guid = guid,
Action = entity.IsNew ? EntityAction.New :
customResolutionModel.ContainsKey(entity.Guid) ? EntityAction.Different :
GraphExplorer.FromRoot((Entity)entity).Any(a => a.Modified != ModifiedState.Clean) ? EntityAction.Different :
GraphExplorer.FromRootVirtual((Entity)entity).Any(a => a.Modified != ModifiedState.Clean) ? EntityAction.Different :
EntityAction.Identical,
CustomResolution = customResolutionModel.TryGetCN(entity.Guid),
});
Expand Down
74 changes: 70 additions & 4 deletions Signum.Entities.Extensions/Dashboard/DashboardEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public Lite<TypeEntity>? EntityType
public MList<PanelPartEmbedded> Parts { get; set; } = new MList<PanelPartEmbedded>();

[Ignore, QueryableProperty]
public MList<TokenEquivalenceGroupEntity> TokenEquivalences { get; set; } = new MList<TokenEquivalenceGroupEntity>();
public MList<TokenEquivalenceGroupEntity> TokenEquivalencesGroups { get; set; } = new MList<TokenEquivalenceGroupEntity>();

[UniqueIndex]
public Guid Guid { get; set; } = Guid.NewGuid();
Expand Down Expand Up @@ -105,6 +105,19 @@ protected override void ChildCollectionChanged(object? sender, NotifyCollectionC
}


internal void ParseData(Func<QueryEntity, QueryDescription?> getDescription)
{
foreach (var f in TokenEquivalencesGroups)
{
foreach (var t in f.TokenEquivalences)
{
var description = getDescription(t.Query);
if (description != null)
t.Token.ParseData(this, description, SubTokensOptions.CanElement);
}
}
}

[Ignore]
bool invalidating = false;
protected override void ChildPropertyChanged(object sender, PropertyChangedEventArgs e)
Expand Down Expand Up @@ -151,7 +164,9 @@ public XElement ToXml(IToXmlContext ctx)
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))));
new XElement("Parts", Parts.Select(p => p.ToXml(ctx))),
new XElement(nameof(TokenEquivalencesGroups), TokenEquivalencesGroups.Select(teg => teg.ToXml(ctx)))
);
}


Expand All @@ -165,6 +180,8 @@ public void FromXml(XElement element, IFromXmlContext ctx)
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));
TokenEquivalencesGroups.Synchronize(element.Element(nameof(TokenEquivalencesGroups))?.Elements().ToList() ?? new List<XElement>(), (teg, x) => teg.FromXml(x, ctx));
ParseData(q => ctx.GetQueryDescription(q));
}

protected override string? PropertyValidation(PropertyInfo pi)
Expand All @@ -183,6 +200,16 @@ public void FromXml(XElement element, IFromXmlContext ctx)
return ValidationMessage._0ShouldBeNullWhen1IsSet.NiceToString(pi.NiceName(), NicePropertyName(() => EntityType));
}

if(pi.Name == nameof(TokenEquivalencesGroups))
{
var dups = TokenEquivalencesGroups
.SelectMany(a => a.TokenEquivalences).Select(a => a.Token.Token).NotNull()
.GroupCount(a => a).Where(gr => gr.Value > 1).ToString(a => a.Value + " x " + a.Key.FullKey(), "\n");

if (dups.HasText())
return "Duplicated tokens: " + dups;
}

return base.PropertyValidation(pi);
}
}
Expand Down Expand Up @@ -270,18 +297,57 @@ public enum DashboardEmbedededInEntity
[EntityKind(EntityKind.Part, EntityData.Master)]
public class TokenEquivalenceGroupEntity : Entity
{
[NotNullValidator(DisabledInModelBinder = true)]
[NotNullValidator(Disabled = true)]
public Lite<DashboardEntity> Dashboard { get; set; }

public InteractionGroup? InteractionGroup { get; set; }

[PreserveOrder, NoRepeatValidator, CountIsValidator(ComparisonType.GreaterThan, 1)]
public MList<TokenEquivalenceEmbedded> TokenEquivalences { get; set; } = new MList<TokenEquivalenceEmbedded>();

internal void FromXml(XElement x, IFromXmlContext ctx)
{
InteractionGroup = x.Attribute("InteractionGroup")?.Value.ToEnum<InteractionGroup>();
TokenEquivalences.Synchronize(x.Elements("TokenEquivalence").ToList(), (teg, x) => teg.FromXml(x, ctx));
}

internal XElement ToXml(IToXmlContext ctx)
{
return new XElement("TokenEquivalenceGroup",
InteractionGroup == null ? null : new XAttribute(nameof(InteractionGroup), InteractionGroup.Value.ToString()),
TokenEquivalences.Select(te => te.ToXml(ctx)));
}

protected override string? PropertyValidation(PropertyInfo pi)
{
if(pi.Name == nameof(TokenEquivalences))
{
var list = TokenEquivalences.Select(a => a.Token.Token.Type.UnNullify().CleanType()).Distinct().ToList();
if(list.Count > 1)
{
if (!list.Any(t => list.All(t2 => t.IsAssignableFrom(t2))))
return "Types " + list.CommaAnd(t => t.TypeName()) + " are not compatible";
}
}

return base.PropertyValidation(pi);
}
}

public class TokenEquivalenceEmbedded : EmbeddedEntity
{
public QueryEntity Query { get; set; }

public QueryTokenEmbedded QueryToken { get; set; }
public QueryTokenEmbedded Token { get; set; }

internal void FromXml(XElement element, IFromXmlContext ctx)
{
Query = ctx.GetQuery(element.Attribute("Query")!.Value);
Token = new QueryTokenEmbedded(element.Attribute("Token")!.Value);
}

internal XElement ToXml(IToXmlContext ctx) => new XElement("TokenEquivalence",
new XAttribute("Query", Query.Key),
new XAttribute("Token", Token.Token.FullKey())
);
}
2 changes: 1 addition & 1 deletion Signum.Entities/Translations/Signum.Entities.de.xml
Original file line number Diff line number Diff line change
Expand Up @@ -595,4 +595,4 @@
<Type Name="VoidEnumMessage">
<Member Name="Instance" Description="-" />
</Type>
</Translations>
</Translations>
11 changes: 6 additions & 5 deletions Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,19 +168,20 @@ export default function Dashboard(p: { ctx: TypeContext<DashboardEntity> }) {
<EntityGridRepeater ctx={ctx.subCtx(cp => cp.parts)} getComponent={renderPart} onCreate={handleOnCreate} />
</div>
</Tab>
<Tab title={ctxBasic.niceName(a => a.tokenEquivalences)} eventKey="equivalences">
<EntityRepeater ctx={ctxBasic.subCtx(a => a.tokenEquivalences)} avoidFieldSet getComponent={(ctxGr: TypeContext<TokenEquivalenceGroupEntity>) =>
<Tab title={ctxBasic.niceName(a => a.tokenEquivalencesGroups)} eventKey="equivalences">
<EntityRepeater ctx={ctx.subCtx(a => a.tokenEquivalencesGroups, { formSize: "ExtraSmall" })} avoidFieldSet getComponent={(ctxGr: TypeContext<TokenEquivalenceGroupEntity>) =>
<div>
<ValueLine ctx={ctxGr.subCtx(cp => cp.interactionGroup)} inlineCheckbox={true} />
<ValueLine ctx={ctxGr.subCtx(pp => pp.interactionGroup)}
onRenderDropDownListItem={(io) => <span><span className="sf-dot" style={{ backgroundColor: colors[InteractionGroup.values().indexOf(io.value)] }} />{io.label}</span>} />
<EntityTable ctx={ctxGr.subCtx(p => p.tokenEquivalences)} avoidFieldSet columns={EntityTable.typedColumns<TokenEquivalenceEmbedded>([
{
property: p => p.query,
template: (ectx, row) => <EntityCombo ctx={ectx.subCtx(p => p.query)} data={allQueryNames} onChange={row.forceUpdate} />,
headerHtmlAttributes: { style: { width: "30%" } },
},
{
property: p => p.query,
template: (ectx) => ectx.value.query && <QueryTokenEntityBuilder ctx={ectx.subCtx(p => p.queryToken)}
property: p => p.token,
template: (ectx) => ectx.value.query && <QueryTokenEntityBuilder ctx={ectx.subCtx(p => p.token)}
queryKey={ectx.value.query.key} subTokenOptions={SubTokensOptions.CanAggregate | SubTokensOptions.CanElement | SubTokensOptions.CanAnyAll} />,
headerHtmlAttributes: { style: { width: "100%" } },
},
Expand Down
2 changes: 1 addition & 1 deletion Signum.React.Extensions/Dashboard/CachedQueryExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ function paginateRows(rt: ResultTable, reqPag: Pagination): ResultTable{
{
switch (reqPag.mode) {
case "All": return rt;
case "Firsts": return { ...rt, rows: rt.rows.slice(0, rt.pagination.elementsPerPage), pagination: reqPag };
case "Firsts": return { ...rt, rows: rt.rows.slice(0, reqPag.elementsPerPage), pagination: reqPag };
case "Paginate":
var startIndex = reqPag.elementsPerPage! * (reqPag.currentPage! - 1);
return { ...rt, rows: rt.rows.slice(startIndex, startIndex + reqPag.elementsPerPage!), pagination: reqPag };
Expand Down
5 changes: 5 additions & 0 deletions Signum.React.Extensions/Dashboard/DashboardServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,10 @@ public static void Start(IApplicationBuilder app)
if (result != null)
ep.extension.Add("embeddedDashboards", result);
};

SignumServer.WebEntityJsonConverterFactory.AfterDeserilization.Register((DashboardEntity uq) =>
{
uq.ParseData(q => QueryLogic.Queries.QueryDescription(q.ToQueryName()));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export interface DashboardEntity extends Entities.Entity, UserAssets.IUserAssetE
combineSimilarRows: boolean;
cacheQueryConfiguration: CacheQueryConfigurationEmbedded | null;
parts: Entities.MList<PanelPartEmbedded>;
tokenEquivalences: Entities.MList<TokenEquivalenceGroupEntity>;
tokenEquivalencesGroups: Entities.MList<TokenEquivalenceGroupEntity>;
guid: string /*Guid*/;
key: string | null;
}
Expand Down Expand Up @@ -151,7 +151,7 @@ export const TokenEquivalenceEmbedded = new Type<TokenEquivalenceEmbedded>("Toke
export interface TokenEquivalenceEmbedded extends Entities.EmbeddedEntity {
Type: "TokenEquivalenceEmbedded";
query: Basics.QueryEntity;
queryToken: UserAssets.QueryTokenEmbedded;
token: UserAssets.QueryTokenEmbedded;
}

export const TokenEquivalenceGroupEntity = new Type<TokenEquivalenceGroupEntity>("TokenEquivalenceGroup");
Expand Down
Loading

2 comments on commit 85b2648

@olmobrutall
Copy link
Collaborator Author

@olmobrutall olmobrutall commented on 85b2648 Jan 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dashboard IV: Token Equivalences

This is another step more improving Dashboards. In this case Token equivalences allow to create a translation table between the query tokens (expressions) of different queryNames in the same dashboard.

This translation tables are used for interaction groups and for dashboard pinned filters (will be explained in the next change changelog item).

For example, you have:

  • A tree map showing products grouped by supplier
  • A line chart shows orders in the next month by date.

You want that the two charts are interactive, so when you click in a product in the first chart, the orders get filter showing only the ones that contain this product. This behavior is not automatic because the two charts are based in different queryNames. In order to enable this functionality you have to create an TokenEquivalenceGroupEntity with:

  • TokenEquivalence with queryName: Product and token: Entity
  • TokenEquivalence with queryName: Order and token Entity.Details.Element.Product

Like this:

image

The order of the elements inside an TokenEquivalenceGroupEntity doesn't matter, it's a reciprocal relationship.

You can add more TokenEquivalenceGroupEntity if you need, for example to give equivalences between date tokens in different queries.

Automatic generalization

Once you add an TokenEquivalenceGroupEntity it automatically generalizes. So if you click in the Product tree map in a parent box associated with the token Entity.Supplier and the value "Pepsi", he will be able to use the TokenEquivalenceGroupEntity above and filter the orders by Entity.Details.Element.Product.Supplier equals "Pepsi"

Restricting applicability

In the case that you want a TokenEquivalenceGroupEntity to affect only one InteractionGroup, you can select it. Otherwise is applied on the whole dashboard.

Dashboard Pinned filters only use TokenEquivalenceGroupEntity with InteractionGroup=null.

Token Equivalence Groups and Cached Queries

Token equivalence groups affect cached queries since they make previously unrelated user queries and user charts interactive with each other, potentially adding extra columns and therefore increasing the size of the cached query file.

Conclusion

Token equivalences are a little bit abstract concept, but you only need to understand them when you need them, and hopefully then they will be obvious enough.

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on 85b2648 Jan 9, 2022 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.