Skip to content

Commit

Permalink
DashboardFilter -> InitialSelectionDashboardFilter and DefaultDashboa…
Browse files Browse the repository at this point in the history
…rdFilter
  • Loading branch information
olmobrutall committed Dec 1, 2021
1 parent 87cffc2 commit 90392c1
Show file tree
Hide file tree
Showing 12 changed files with 55 additions and 25 deletions.
8 changes: 5 additions & 3 deletions Signum.Engine.Extensions/Dashboard/DashboardLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,9 @@ public static List<CachedQueryDefinition> GetCachedQueryDefinitions(DashboardEnt
}
}

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

return cached.ToList();
return cached;
}

public static List<CombinedCachedQueryDefinition> CombineCachedQueryDefinitions(List<CachedQueryDefinition> cachedQueryDefinition)
Expand Down Expand Up @@ -505,6 +505,8 @@ public CachedQueryDefinition(QueryRequest queryRequest, PanelPartEmbedded panelP
public Lite<IUserAssetEntity> UserAsset { get; set; }
public bool IsQueryCached { get; }
public bool CanWriteFilters { get; }

public override string ToString() => $"{UserAsset} IsQueryCached={IsQueryCached} CanWriteFilters={CanWriteFilters}";
}

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
Expand Down Expand Up @@ -541,7 +543,7 @@ public bool CombineIfPossible(CachedQueryDefinition definition)
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();
var otherKeys = other.Columns.Select(a => a.Token).Where(t => t is not AggregateToken).ToHashSet();
if (!meKeys.SetEquals(otherKeys))
return false;
}
Expand Down
22 changes: 17 additions & 5 deletions Signum.Entities.Extensions/UserQueries/UserQueryEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -456,24 +456,36 @@ public static List<Filter> ToFilterList(this IEnumerable<QueryFilterEmbedded> fi
{
return filters.GroupWhen(filter => filter.Indentation == indent).Select(gr =>
{
var filter = gr.Key;

if (filter.Pinned != null)
{
if (filter.Pinned.Active == PinnedFilterActive.Checkbox_StartUnchecked ||
filter.Pinned.Active == PinnedFilterActive.DefaultDashboardFilter /*TODO, works for CachedQueries but maybe not in other cases*/ ||
filter.Pinned.Active == PinnedFilterActive.InitialSelectionDashboardFilter)
return null;
}

if (!gr.Key.IsGroup)
{
if (gr.Count() != 0)
throw new InvalidOperationException("Unexpected childrens of condition");

var filter = gr.Key;

var value = FilterValueConverter.Parse(filter.ValueString, filter.Token!.Token.Type, filter.Operation!.Value.IsList());

if (filter.Pinned?.Active == PinnedFilterActive.WhenHasValue && value == null)
return null;

return (Filter)new FilterCondition(filter.Token.Token, filter.Operation.Value, value);
}
else
{
var group = gr.Key;
if (filter.Pinned?.Active == PinnedFilterActive.WhenHasValue /*TODO, works for empty groups */)
return null;

return (Filter)new FilterGroup(group.GroupOperation!.Value, group.Token?.Token, gr.ToFilterList(indent + 1).ToList());
return (Filter)new FilterGroup(filter.GroupOperation!.Value, filter.Token?.Token, gr.ToFilterList(indent + 1).ToList());
}
}).ToList();
}).NotNull().ToList();
}
}

Expand Down
3 changes: 2 additions & 1 deletion Signum.Entities/DynamicQuery/Filter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,5 +268,6 @@ public enum PinnedFilterActive
Checkbox_StartChecked,
[Description("Checkbox (start unchecked)")]
Checkbox_StartUnchecked,
DashboardFilter,
InitialSelectionDashboardFilter,
DefaultDashboardFilter,
}
2 changes: 1 addition & 1 deletion Signum.Entities/EnumMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ public enum SearchMessage
HideHiddenColumns,

GroupKey,
DerivedGroupKey
DerivedGroupKey,
}

public enum SelectorMessage
Expand Down
4 changes: 2 additions & 2 deletions Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ export default function Dashboard(p: { ctx: TypeContext<DashboardEntity> }) {
<div className="col-sm-2">
{!ctx.value.isNew && <ValueSearchControlLine ctx={ectx} findOptions={{ queryName: CachedQueryEntity, filterOptions: [{ token: CachedQueryEntity.token(a => a.dashboard), value: ctxBasic.value }] }} />}
</div>
<div className="col-sm-2 pt-4">
{!ctx.value.isNew && <OperationButton eoc={EntityOperationContext.fromTypeContext(ctx, DashboardOperation.RegenerateCachedQueries)} className="w-100" />}
<div className="col-sm-3 pt-4">
{!ctx.value.isNew && <OperationButton eoc={EntityOperationContext.fromTypeContext(ctx, DashboardOperation.RegenerateCachedQueries)} hideOnCanExecute className="w-100" />}
</div>
</div>} />

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 @@ -306,7 +306,7 @@ function getRowKey(rt: ResultTable, keyTokens: string[], parsedTokens: { [token:
const index = rt.columns.indexOf(token);

if (index == -1)
throw new CachedQueryError("Token " + token + " not found for filtering");
throw new CachedQueryError("Token " + token + " not found for grouping");

const qt = parsedTokens[token];

Expand Down
19 changes: 14 additions & 5 deletions Signum.React.Extensions/Dashboard/View/UserChartPart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,30 @@ import { PanelPartContentProps } from '../DashboardClient'
import { getTypeInfos } from '@framework/Reflection'
import SelectorModal from '@framework/SelectorModal'
import { DashboardFilter, DashboardFilterController, DashboardFilterRow, equalsDFR } from "./DashboardFilterController"
import { filterOperations, isFilterGroupOptionParsed } from '@framework/FindOptions'
import { filterOperations, FilterOptionParsed, isFilterGroupOption, isFilterGroupOptionParsed, QueryToken } from '@framework/FindOptions'
import { CachedQueryJS, executeChartCached } from '../CachedQueryExecutor'

export default function UserChartPart(p: PanelPartContentProps<UserChartPartEntity>) {

const qd = useAPI(() => Finder.getQueryDescription(p.part.userChart.query.key), [p.part.userChart.query.key]);
const chartRequest = useAPI(() => UserChartClient.Converter.toChartRequest(p.part.userChart, p.entity), [p.part.userChart, p.entity, ...p.deps ?? []]);
const dbFop = React.useMemo(() => chartRequest?.filterOptions.singleOrNull(a => a.pinned?.active == "DashboardFilter"), [chartRequest]);
const originalFilters = React.useMemo(() => chartRequest?.filterOptions.filter(a => a.pinned == null || a.pinned.active != "DashboardFilter"), [chartRequest]);
const dbFop = React.useMemo(() => chartRequest?.filterOptions.singleOrNull(a => a.pinned?.active == "InitialSelectionDashboardFilter"), [chartRequest]);
const originalFilters = React.useMemo(() => chartRequest?.filterOptions.filter(a => a.pinned == null || a.pinned.active != "InitialSelectionDashboardFilter"), [chartRequest]);

if (chartRequest != null) {
chartRequest.filterOptions.clear();

var dashboardFilters = p.filterController.getFilterOptions(p.partEmbedded, chartRequest!.queryKey);

function allTokens(fs: FilterOptionParsed[]): QueryToken[] {
return fs.flatMap(f => isFilterGroupOptionParsed(f) ? [f.token, ...allTokens(f.filters)].notNull() : [f.token].notNull())
}

var tokens = allTokens(dashboardFilters);

chartRequest.filterOptions = [
...originalFilters!,
...p.filterController.getFilterOptions(p.partEmbedded, chartRequest!.queryKey),
...originalFilters!.filter(a => a.pinned == null || a.pinned.active !== "DefaultDashboardFilter" || !tokens.some(t => t.fullKey == a.token?.fullKey)),
...dashboardFilters,
];
}

Expand Down
2 changes: 1 addition & 1 deletion Signum.React/Scripts/FindOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function isFilterGroupOptionParsed(fo: FilterOptionParsed): fo is FilterG
}

export function isActive(fo: FilterOptionParsed) {
return !(fo.pinned && (fo.pinned.active == "Checkbox_StartUnchecked" || fo.pinned.active == "DashboardFilter" || fo.pinned.active == "WhenHasValue" && fo.value == null));
return !(fo.pinned && (fo.pinned.active == "Checkbox_StartUnchecked" || fo.pinned.active == "InitialSelectionDashboardFilter" || fo.pinned.active == "WhenHasValue" && fo.value == null));
}

export interface FilterConditionOptionParsed {
Expand Down
2 changes: 1 addition & 1 deletion Signum.React/Scripts/Finder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -875,7 +875,7 @@ export function toFilterRequest(fop: FilterOptionParsed, overridenValue?: Overri
if (fop.pinned && fop.pinned.active == "Checkbox_StartUnchecked")
return undefined;

if (fop.pinned && fop.pinned.active == "DashboardFilter")
if (fop.pinned && fop.pinned.active == "InitialSelectionDashboardFilter")
return undefined;

if (fop.pinned && overridenValue == null) {
Expand Down
8 changes: 6 additions & 2 deletions Signum.React/Scripts/Operations/EntityOperations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,11 @@ interface OperationButtonProps extends ButtonProps {
color?: BsColor;
avoidAlternatives?: boolean;
onOperationClick?: (eoc: EntityOperationContext<any /*Entity*/>, event: React.MouseEvent) => void;
children?: React.ReactNode
children?: React.ReactNode;
hideOnCanExecute?: boolean;
}

export function OperationButton({ group, onOperationClick, canExecute, eoc: eocOrNull, outline, color, avoidAlternatives, ...props }: OperationButtonProps): React.ReactElement<any> | null {
export function OperationButton({ group, onOperationClick, canExecute, eoc: eocOrNull, outline, color, avoidAlternatives, hideOnCanExecute, ...props }: OperationButtonProps): React.ReactElement<any> | null {

if (eocOrNull == null)
return null;
Expand All @@ -144,6 +145,9 @@ export function OperationButton({ group, onOperationClick, canExecute, eoc: eocO

const disabled = !!canExecute;

if (hideOnCanExecute && disabled)
return null;

var alternatives = avoidAlternatives ? undefined : eoc.alternatives && eoc.alternatives.filter(a => a.isVisible != false);

if (group) {
Expand Down
5 changes: 3 additions & 2 deletions Signum.React/Scripts/SearchControl/PinnedFilterBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ export default function PinnedFilterBuilder(p: PinnedFilterBuilderProps) {
if (allPinned.length == 0)
return null;


return (
<div onKeyUp={handleFiltersKeyUp }>
<div className={classes("row", p.extraSmall ? "" : "mt-3 mb-3")}>
{
allPinned
.filter(fo => fo.pinned?.active != "DashboardFilter")
.filter(fo => fo.pinned?.active != "InitialSelectionDashboardFilter")
.groupBy(fo => (fo.pinned!.column ?? 0).toString())
.orderBy(gr => parseInt(gr.key))
.map(gr => <div className="col-sm-3" key={gr.key}>
Expand Down Expand Up @@ -81,7 +82,7 @@ export default function PinnedFilterBuilder(p: PinnedFilterBuilderProps) {
</FormGroup>
);

return createFilterValueControl(ctx, f.token!, () => handleValueChange(f), labelText, f.pinned!.active == "WhenHasValue" || f.pinned!.active == "DashboardFilter");
return createFilterValueControl(ctx, f.token!, () => handleValueChange(f), labelText, f.pinned!.active == "WhenHasValue" || f.pinned!.active == "InitialSelectionDashboardFilter");
}


Expand Down
3 changes: 2 additions & 1 deletion Signum.React/Scripts/Signum.Entities.DynamicQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export type PinnedFilterActive =
"WhenHasValue" |
"Checkbox_StartChecked" |
"Checkbox_StartUnchecked" |
"DashboardFilter";
"InitialSelectionDashboardFilter" |
"DefaultDashboardFilter";

export module QueryTokenMessage {
export const _0As1 = new MessageKey("QueryTokenMessage", "_0As1");
Expand Down

3 comments on commit 90392c1

@olmobrutall
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

InitialSelectionDashboardFilter and DefaultDashboardFilter

Here are two new tricks for your interactive dashboards

PinnedFilterActive.InitialSelectionDashboardFilter

For some dashboards could make sense to have it opened with some filter already applied in one chart, as if you have just clicked it.

Example: Maybe you have a map and you wan't to have the User's country already selected. Or you want to select the current date or the current year.

You can implement this by adding a new filter to your UserChart and selecting as PinnedFilterActive the value InitialSelectionDashboardFilter.

image

Remember that, counter-intuitively, this filter does not apply to this UserChart itself, it will only highlight the chart regions. It will be applied however to all the other widgets in the same InteractionGroup.

PinnedFilterActive.DefaultDashboardFilter

There is another new option in PinnedFilterActive, this time when applied to a filter it will be used as default filter in case no compatible dashboard filter is being applied from another widgets in the same InteractionGroup.

This option only affect the selected UserChart / UserQuery, not like the previous one.

Example: You have two user charts:

  • Stacked Areas showing Sales by product category and time (about 10 categories)
  • Stacked Areas showing Sales by product and time (about 100 producst, 10 for each category).

If a category is selected everything looks good, but in no category is selected then there is no filter and you have a Stacked Area with too many products.

Using DefaultDashboardFilter you can have one or more filters that will be used when no filter using the same token is applied from the dashboard.

image

And this is the result:

Animation2

Note how when no category is selected, instead of all the products you see no products at all, and you have a pinned filter to indicated the problem. (is this a bug or a feature?)

Also note that in this case it works because there is no Product with Category equals to null, if you are not so lucky, you can add two contradictory default dashboard filters: Product.Category equals to null and Product.Category distinct to null. Both will be removed when a Dashboard filter for Product.Category will be applied.

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on 90392c1 Dec 5, 2021 via email

Choose a reason for hiding this comment

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

@goldenauge
Copy link

Choose a reason for hiding this comment

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

nice 👍

Please sign in to comment.