Skip to content

Commit

Permalink
more on Dashboard Pinned Filters
Browse files Browse the repository at this point in the history
  • Loading branch information
olmobrutall committed Jan 5, 2022
1 parent b3f0d11 commit 5520795
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 70 deletions.
110 changes: 78 additions & 32 deletions Signum.Engine.Extensions/Dashboard/DashboardLogic.cs
Expand Up @@ -64,10 +64,10 @@ public static void Start(SchemaBuilder sb, IFileTypeAlgorithm cachedQueryAlgorit

SchedulerLogic.ExecuteTask.Register((DashboardEntity db, ScheduledTaskContext ctx) => { db.Execute(DashboardOperation.RegenerateCachedQueries); return null; });

OnGetCachedQueryDefinition.Register((UserChartPartEntity ucp, PanelPartEmbedded pp) => new[] { new CachedQueryDefinition(ucp.UserChart.ToChartRequest().ToQueryRequest(), ucp.UserChart.Filters.GetPinnedFilterTokens(), pp, ucp.UserChart, ucp.IsQueryCached, canWriteFilters: true) });
OnGetCachedQueryDefinition.Register((CombinedUserChartPartEntity cucp, PanelPartEmbedded pp) => cucp.UserCharts.Select(uc => new CachedQueryDefinition(uc.UserChart.ToChartRequest().ToQueryRequest(), uc.UserChart.Filters.GetPinnedFilterTokens(), pp, uc.UserChart, uc.IsQueryCached, canWriteFilters: false)));
OnGetCachedQueryDefinition.Register((UserQueryPartEntity uqp, PanelPartEmbedded pp) => new[] { new CachedQueryDefinition(uqp.RenderMode == UserQueryPartRenderMode.BigValue ? uqp.UserQuery.ToQueryRequestValue() : uqp.UserQuery.ToQueryRequest(), uqp.UserQuery.Filters.GetPinnedFilterTokens(), pp, uqp.UserQuery, uqp.IsQueryCached, canWriteFilters: false) });
OnGetCachedQueryDefinition.Register((ValueUserQueryListPartEntity vuql, PanelPartEmbedded pp) => vuql.UserQueries.Select(uqe => new CachedQueryDefinition(uqe.UserQuery.ToQueryRequestValue(), uqe.UserQuery.Filters.GetPinnedFilterTokens(), pp, uqe.UserQuery, uqe.IsQueryCached, canWriteFilters: false)));
OnGetCachedQueryDefinition.Register((UserChartPartEntity ucp, PanelPartEmbedded pp) => new[] { new CachedQueryDefinition(ucp.UserChart.ToChartRequest().ToQueryRequest(), ucp.UserChart.Filters.GetDashboardPinnedFilterTokens(), pp, ucp.UserChart, ucp.IsQueryCached, canWriteFilters: true) });
OnGetCachedQueryDefinition.Register((CombinedUserChartPartEntity cucp, PanelPartEmbedded pp) => cucp.UserCharts.Select(uc => new CachedQueryDefinition(uc.UserChart.ToChartRequest().ToQueryRequest(), uc.UserChart.Filters.GetDashboardPinnedFilterTokens(), pp, uc.UserChart, uc.IsQueryCached, canWriteFilters: false)));
OnGetCachedQueryDefinition.Register((UserQueryPartEntity uqp, PanelPartEmbedded pp) => new[] { new CachedQueryDefinition(uqp.RenderMode == UserQueryPartRenderMode.BigValue ? uqp.UserQuery.ToQueryRequestValue() : uqp.UserQuery.ToQueryRequest(), uqp.UserQuery.Filters.GetDashboardPinnedFilterTokens(), pp, uqp.UserQuery, uqp.IsQueryCached, canWriteFilters: false) });
OnGetCachedQueryDefinition.Register((ValueUserQueryListPartEntity vuql, PanelPartEmbedded pp) => vuql.UserQueries.Select(uqe => new CachedQueryDefinition(uqe.UserQuery.ToQueryRequestValue(), uqe.UserQuery.Filters.GetDashboardPinnedFilterTokens(), pp, uqe.UserQuery, uqe.IsQueryCached, canWriteFilters: false)));
OnGetCachedQueryDefinition.Register((UserTreePartEntity ute, PanelPartEmbedded pp) => Array.Empty<CachedQueryDefinition>());
OnGetCachedQueryDefinition.Register((LinkListPartEntity uqp, PanelPartEmbedded pp) => Array.Empty<CachedQueryDefinition>());

Expand Down Expand Up @@ -447,57 +447,103 @@ public static List<CachedQueryDefinition> GetCachedQueryDefinitions(DashboardEnt
if (!writers.Any())
continue;

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

foreach (var wr in writers)
{
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.Token.Token, gr.TokenEquivalences.GroupToDictionary(a => a.Query.ToQueryName(), a => a.Token.Token)))
.ToDictionaryEx();
var keyColumns = wr.QueryRequest.GroupResults ?
wr.QueryRequest.Columns.Where(c => c.Token is not AggregateToken).Select(c => c.Token).Distinct().ToList() :
wr.QueryRequest.Columns.Select(c => c.Token).Distinct().ToList();

Dictionary<QueryToken, Dictionary<object, List<QueryToken>>> equivalencesDictionary = GetEquivalenceDictionary(equivalenceGroups, fromQuery: wr.QueryRequest.QueryName);

foreach (var cqd in cqdefs.Where(e => e != wr))
{
var extraColumns = keyColumns.Select(k =>
{
var translatedToken = TranslatedToken(k.Token, cqd.QueryRequest.QueryName, equivalencesDictionary);
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";
return null;
}).NotNull().ToList();
List<QueryToken> extraColumns = ExtraColumns(keyColumns, cqd, equivalencesDictionary);

if (extraColumns.Any())
{
ExpandColumns(cqd, extraColumns);
ExpandColumns(cqd, extraColumns, "Dashboard Filters from " + key);
}

cqd.QueryRequest.Pagination = new Pagination.All();
}
}
}

foreach (var cqd in definitions)
foreach (var writer in definitions)
{
if (cqd.PinnedFiltersTokens.Any())
ExpandColumns(cqd, cqd.PinnedFiltersTokens);
if (writer.PinnedFiltersTokens.Any())
{
var pft = writer.PinnedFiltersTokens.Where(a => a.prototedToDashboard == false).Select(a=>a.token).ToList();
if (pft.Any())
ExpandColumns(writer, pft, "Pinned Filters");

var dpft = writer.PinnedFiltersTokens.Where(a => a.prototedToDashboard == true).Select(a => a.token).ToList();
if (dpft.Any())
{
var equivalenceGroups = db.TokenEquivalencesGroups.Where(a => /*a.InteractionGroup == writer.PanelPart.InteractionGroup Needed? ||*/ a.InteractionGroup == null);

Dictionary<QueryToken, Dictionary<object, List<QueryToken>>> equivalencesDictionary = GetEquivalenceDictionary(equivalenceGroups, fromQuery: writer.QueryRequest.QueryName);

foreach (var cqd in definitions.Where(e => e != writer))
{
List<QueryToken> extraColumns = ExtraColumns(dpft, cqd, equivalencesDictionary);
if (extraColumns.Any())
{
ExpandColumns(cqd, extraColumns, "Dashboard Pinned Filters");
}
}
}

}
}

var cached = definitions.Where(a => a.IsQueryCached).ToList();

return cached;
}

private static void ExpandColumns(CachedQueryDefinition cqd, List<QueryToken> extraColumns)
private static List<QueryToken> ExtraColumns(List<QueryToken> requiredTokens, CachedQueryDefinition cqd, Dictionary<QueryToken, Dictionary<object, List<QueryToken>>> equivalencesDictionary)
{
var extraColumns = requiredTokens.Select(t =>
{
var translatedToken = TranslatedToken(t, cqd.QueryRequest.QueryName, equivalencesDictionary);
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";
return null;
}).NotNull().ToList();
return extraColumns;
}

private static Dictionary<QueryToken, Dictionary<object, List<QueryToken>>> GetEquivalenceDictionary(IEnumerable<TokenEquivalenceGroupEntity> equivalences, object fromQuery)
{
return (from gr in equivalences
from t in gr.TokenEquivalences.Where(a => a.Query.ToQueryName() == fromQuery)
select KeyValuePair.Create(t.Token.Token, gr.TokenEquivalences.GroupToDictionary(a => a.Query.ToQueryName(), a => a.Token.Token)))
.ToDictionaryEx();
}

private static void ExpandColumns(CachedQueryDefinition cqd, List<QueryToken> extraColumns, string errorContext)
{
if (cqd.QueryRequest.GroupResults)
{
var errors = extraColumns
.Select(a => new {
token = a,
error = QueryUtils.CanColumn(a) ?? (cqd.QueryRequest.GroupResults && !a.IsGroupable ? "Is not groupable" : null)
})
.Where(a => a.error != null);

if (errors.Any())
throw new InvalidOperationException($"Unable to expand columns in '{cqd.UserAsset.KeyLong()}' (query {QueryUtils.GetKey(cqd.QueryRequest.QueryName)}) requested by {errorContext} because: \r\n{errors.ToString(a => a.token.FullKey() + ": " + a.error, "\r\n")}");
}

cqd.QueryRequest.Columns.AddRange(extraColumns.Select(c => new Column(c, null)));
var avgs = cqd.QueryRequest.Columns.Extract(a => a.Token is AggregateToken at && at.AggregateFunction == AggregateFunction.Average);
foreach (var av in avgs)
Expand Down Expand Up @@ -578,7 +624,7 @@ public static List<CombinedCachedQueryDefinition> CombineCachedQueryDefinitions(

public class CachedQueryDefinition
{
public CachedQueryDefinition(QueryRequest queryRequest, List<QueryToken> pinnedFiltersTokens, PanelPartEmbedded panelPart, IUserAssetEntity userAsset, bool isQueryCached, bool canWriteFilters)
public CachedQueryDefinition(QueryRequest queryRequest, List<(QueryToken token, bool prototedToDashboard)> pinnedFiltersTokens, PanelPartEmbedded panelPart, IUserAssetEntity userAsset, bool isQueryCached, bool canWriteFilters)
{
QueryRequest = queryRequest;
PinnedFiltersTokens = pinnedFiltersTokens;
Expand All @@ -590,7 +636,7 @@ public CachedQueryDefinition(QueryRequest queryRequest, List<QueryToken> pinnedF
}

public QueryRequest QueryRequest { get; set; }
public List<QueryToken> PinnedFiltersTokens { get; set; }
public List<(QueryToken token, bool prototedToDashboard)> PinnedFiltersTokens { get; set; }
public PanelPartEmbedded PanelPart { get; set; }
public Guid Guid { get; set; }
public Lite<IUserAssetEntity> UserAsset { get; set; }
Expand Down
11 changes: 7 additions & 4 deletions Signum.Entities.Extensions/UserQueries/UserQueryEntity.cs
Expand Up @@ -499,19 +499,22 @@ public static List<Filter> ToFilterList(this IEnumerable<QueryFilterEmbedded> fi
}).NotNull().ToList();
}

public static List<QueryToken> GetPinnedFilterTokens(this IEnumerable<QueryFilterEmbedded> filters, int indent = 0)
public static List<(QueryToken, bool prototedToDashboard)> GetDashboardPinnedFilterTokens(this IEnumerable<QueryFilterEmbedded> filters, int indent = 0)
{
return filters.GroupWhen(filter => filter.Indentation == indent).SelectMany(gr =>
{
var filter = gr.Key;
if (filter.Pinned != null)
return gr.Select(a => a.Token?.Token).NotNull().Distinct();
{
var promotedToDashboard = filter.DashboardBehaviour == DashboardBehaviour.PromoteToDasboardPinnedFilter;
return gr.PreAnd(filter).Select(a => a.Token?.Token).NotNull().Distinct().Select(t => (t, promotedToDashboard));
}
if (filter.IsGroup)
return gr.GetPinnedFilterTokens(indent + 1);
return gr.GetDashboardPinnedFilterTokens(indent + 1);
else
return Enumerable.Empty<QueryToken>();
return Enumerable.Empty<(QueryToken, bool prototedToDashboard)>();
}).ToList();
}
}
Expand Down
2 changes: 1 addition & 1 deletion Signum.Entities/DynamicQuery/Filter.cs
Expand Up @@ -274,7 +274,7 @@ public enum PinnedFilterActive
public enum DashboardBehaviour
{
//ShowAsPartFilter = 0, //Pinned Filter shown in the Part Widget
PromoteToDasboardFilter = 1, //Pinned Filter promoted to dashboard
PromoteToDasboardPinnedFilter = 1, //Pinned Filter promoted to dashboard
UseAsInitialSelection, //Filters other parts in the same interaction group as if the user initially selected
UseWhenNoFilters
}
Expand Up @@ -121,7 +121,7 @@ export default function CombinedUserChartPart(p: PanelPartContentProps<CombinedU
infos.forEach(inf => {
inf.makeQuery?.().done();
});
}, [p.part, ...p.deps ?? [], infos.max(e => p.filterController.lastChange.get(e.userChart.query.key))]);
}, [p.part, ...p.deps ?? [], infos.max(e => p.filterController.getLastChange(e.userChart.query.key))]);

function renderError(e: any, key: number) {
const se = e instanceof ServiceError ? (e as ServiceError) : undefined;
Expand Down
@@ -1,5 +1,5 @@
import { DashboardEntity, PanelPartEmbedded } from '../Signum.Entities.Dashboard';
import { FilterConditionOptionParsed, FilterGroupOptionParsed, FilterOptionParsed, FindOptions, QueryToken } from '@framework/FindOptions';
import { FilterConditionOptionParsed, FilterGroupOptionParsed, FilterOptionParsed, FindOptions, isFilterGroupOptionParsed, QueryToken } from '@framework/FindOptions';
import { FilterGroupOperation } from '@framework/Signum.Entities.DynamicQuery';
import { ChartRequestModel } from '../../Chart/Signum.Entities.Chart';
import { ChartRow } from '../../Chart/ChartClient';
Expand All @@ -14,12 +14,17 @@ export class DashboardFilterController {
forceUpdate: () => void;

filters: Map<PanelPartEmbedded, DashboardFilter> = new Map();
pinnedFilters: Map<PanelPartEmbedded, DashboardPinnedFilters> = new Map();
lastChange: Map<string /*queryKey*/, number> = new Map();
dashboard: DashboardEntity;
queriesWithEquivalences: string/*queryKey*/[];

constructor(forceUpdate: () => void, dashboard: DashboardEntity) {
this.forceUpdate = forceUpdate;
this.dashboard = dashboard;

this.queriesWithEquivalences = dashboard.tokenEquivalencesGroups.flatMap(a => a.element.tokenEquivalences.map(a => a.element.query.key)).distinctBy(a => a);

}

setFilter(filter: DashboardFilter) {
Expand All @@ -28,23 +33,42 @@ export class DashboardFilterController {
this.forceUpdate();
}

clear(partEmbedded: PanelPartEmbedded) {
setPinnedFilter(filter: DashboardPinnedFilters) {
this.lastChange.set(filter.queryKey, new Date().getTime());
this.pinnedFilters.set(filter.partEmbedded, filter);
this.forceUpdate();
}

clearPinnesFilter(partEmbedded: PanelPartEmbedded) {
var current = this.pinnedFilters.get(partEmbedded);
if (current)
this.lastChange.set(current.queryKey, new Date().getTime());

this.pinnedFilters.delete(partEmbedded);
this.forceUpdate();
}

clearFilters(partEmbedded: PanelPartEmbedded) {
var current = this.filters.get(partEmbedded);
if (current) {
if (current)
this.lastChange.set(current.queryKey, new Date().getTime());
}
this.filters.delete(partEmbedded);
this.forceUpdate();
}

getFilterOptions(partEmbedded: PanelPartEmbedded, queryKey: string): FilterOptionParsed[] {
getLastChange(queryKey: string) {
if (this.queriesWithEquivalences.contains(queryKey))
return this.queriesWithEquivalences.max(qk => this.lastChange.get(qk));

if (partEmbedded.interactionGroup == null)
return [];
return this.lastChange.get(queryKey);
}

var otherFilters = Array.from(this.filters.values()).filter(f => f.partEmbedded != partEmbedded && f.partEmbedded.interactionGroup == partEmbedded.interactionGroup && f.rows?.length);
getFilterOptions(partEmbedded: PanelPartEmbedded, queryKey: string): FilterOptionParsed[] {

debugger;
var otherFilters = partEmbedded.interactionGroup == null ? [] : Array.from(this.filters.values()).filter(f => f.partEmbedded != partEmbedded && f.partEmbedded.interactionGroup == partEmbedded.interactionGroup && f.rows?.length);
var pinnedFilters = Array.from(this.pinnedFilters.values()).filter(a => a.pinnedFilters.length > 0);
if (otherFilters.length == 0 && pinnedFilters.length == 0)
return [];

var equivalences = this.dashboard.tokenEquivalencesGroups
.filter(a => a.element.interactionGroup == partEmbedded.interactionGroup || a.element.interactionGroup == null)
Expand All @@ -59,9 +83,8 @@ export class DashboardFilterController {
})));
}).groupToObject(a => a.fromQueryKey)

var result = otherFilters.map(
var resultFilters = otherFilters.map(
df => {


var tokenEquivalences = equivalences[df.queryKey]?.groupToObject(a => a.fromToken!.fullKey);

Expand All @@ -81,13 +104,21 @@ export class DashboardFilterController {
).notNull())
}).notNull();

return result;
var resultPinnedFilters = pinnedFilters.flatMap(a => {
if (a.queryKey == queryKey)
return a.pinnedFilters;

var tokenEquivalences = equivalences[a.queryKey]?.groupToObject(a => a.fromToken!.fullKey);

return a.pinnedFilters.map(fop => tokenEquivalences && translateFilterToken(fop, tokenEquivalences)).notNull();
})

return [...resultPinnedFilters, ...resultFilters];
}



applyToFindOptions(partEmbedded: PanelPartEmbedded, fo: FindOptions): FindOptions {

var fops = this.getFilterOptions(partEmbedded, getQueryKey(fo.queryName));
if (fops.length == 0)
return fo;
Expand All @@ -103,10 +134,28 @@ export class DashboardFilterController {
}
}

function translateFilterToken(fop: FilterOptionParsed, tokenEquivalences: { [token: string]: TokenEquivalenceTuple[] }): FilterOptionParsed | null {
var newToken: QueryToken | null | undefined = fop.token;
if (newToken != null) {
newToken = translateToken(newToken, tokenEquivalences);
if (newToken == null)
return null;
}

if (isFilterGroupOptionParsed(fop)) {
return ({ ...fop, token: newToken, filters: fop.filters.map(f => translateFilterToken(f, tokenEquivalences)).notNull() });
}
else
return ({ ...fop, token: newToken });
}

function translateToken(token: QueryToken, tokenEquivalences: { [token: string]: TokenEquivalenceTuple[] }) {

var toAdd: QueryToken[] = [];

if (tokenEquivalences == null)
return null;

for (var t = token; t != null; t = t.parent!) {

var equivalence = tokenEquivalences[t.fullKey];
Expand All @@ -118,7 +167,6 @@ function translateToken(token: QueryToken, tokenEquivalences: { [token: string]:
toAdd.insertAt(0, t);

if (t.parent == null) {//Is a Column, like 'Supplier', but maybe 'Entity' is mapped to we can interpret it as 'Entity.Supplier'
debugger;
equivalence = tokenEquivalences["Entity"];

if (equivalence != null) {
Expand Down Expand Up @@ -152,6 +200,18 @@ interface TokenEquivalenceTuple {
toToken: QueryToken;
}

export class DashboardPinnedFilters {
partEmbedded: PanelPartEmbedded;
queryKey: string;
pinnedFilters: FilterOptionParsed[];

constructor(partEmbedded: PanelPartEmbedded, queryKey: string, pinnedFilters: FilterOptionParsed[]) {
this.partEmbedded = partEmbedded;
this.queryKey = queryKey;
this.pinnedFilters = pinnedFilters;
}
}

export class DashboardFilter {
partEmbedded: PanelPartEmbedded;
queryKey: string;
Expand Down

2 comments on commit 5520795

@olmobrutall
Copy link
Collaborator Author

@olmobrutall olmobrutall commented on 5520795 Jan 8, 2022

Choose a reason for hiding this comment

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

Dashboards V: Dashboard pinned filters

Now that dashboards have a way to express equivalences between tokens of different queries, it is possible to implement a long-requested feature: Global pinned filters that affect the whole dashboard.

Traditionally the solution to this problem was to make a entity-dependent dashboard, but this doesn't work for more value-only filters, like filtering by start / end date, booleans, enums, etc..

In order to implement this behavior filters (FilterOption, FilterOptionParsed, QueryFilterEmbedded, ....) have a new field DashboardBehaviour. that allow to define how a particular filter should behave when the UserQuery or UserChart is shown inside a Dashboard, so is only editable in this two entities.

image

I've refactored the code and moved some of the options previously in PinnedFilterActive out to the new DashboardBehaviour for clarity.

This are the possible values:

  • null: The filter behaves as expected, as a background filter if not pinned, or as an interactive pinned filter inside the dashboard part if pinned.
  • Promote to dashboard pinned filter: Only applicable to pinned filters. Moves the pinned filter to the top of the dashboard, making it take effect to all the other dashboard parts that use the same query, or there is a Token Equivalence Group that can be used. NEW!!
  • Use as initial selection: Only applicable to non-pinned filters. It's used to determine the initial selected element in a chart that belongs to an interaction group, as if the user just selected an element. Presented last month but moved out from PinnedFilterActive.
  • Use when no filters: Only applicable to non-pinned filters. Used as a fallback filter when no dashboard filter is applied for this token (interaction filter or pinned filter). Typically to show no-results instead of way-to-many results. Presented last month but moved out from PinnedFilterActive.

image

Dashboard pinned filters are applied globally but defined inside one of the UserQuery or UserAssets, this means:

  • Maybe will be hard to know where the filter comes from, so you'll need to check each dashboard part. If this becomes an issue maybe is worth to add some help.
  • Currently there is no Distinct implemented, so if you defined many UserCharts with similar filters, all marked as Promote to dashboard pinned filter, expect duplicated filters in your dashboard.
  • Column/Row configurations of the pinned filter will be respected when promoted to dashboard pinned filters, so you can still use this field to layout your filters.

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on 5520795 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.