Skip to content

Commit

Permalink
add InteractionGroup
Browse files Browse the repository at this point in the history
  • Loading branch information
olmobrutall committed Nov 1, 2021
1 parent 7622f05 commit 55def71
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 12 deletions.
19 changes: 16 additions & 3 deletions Signum.Entities.Extensions/Dashboard/PanelPart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public class PanelPartEmbedded : EmbeddedEntity, IGridEntity
[NumberBetweenValidator(1, 12)]
public int Columns { get; set; }

public InteractionGroup? InteractionGroup { get; set; }

public BootstrapStyle Style { get; set; }

[ImplementedBy(
Expand Down Expand Up @@ -91,6 +93,7 @@ internal XElement ToXml(IToXmlContext ctx)
Title == null ? null! : new XAttribute("Title", Title),
IconName == null ? null! : new XAttribute("IconName", IconName),
IconColor == null ? null! : new XAttribute("IconColor", IconColor),
InteractionGroup == null ? null! : new XAttribute("InteractionGroup", InteractionGroup),
new XAttribute("Style", Style),
Content.ToXml(ctx));
}
Expand All @@ -103,7 +106,8 @@ internal void FromXml(XElement x, IFromXmlContext ctx)
Title = x.Attribute("Title")?.Value;
IconName = x.Attribute("IconName")?.Value;
IconColor = x.Attribute("IconColor")?.Value;
Style = (BootstrapStyle)(x.Attribute("Style")?.Let(a => Enum.Parse(typeof(BootstrapStyle), a.Value)) ?? BootstrapStyle.Light);
Style = x.Attribute("Style")?.Value.TryToEnum<BootstrapStyle>() ?? BootstrapStyle.Light;
InteractionGroup = x.Attribute("InteractionGroup")?.Value.ToEnum<InteractionGroup>();
Content = ctx.GetPart(Content, x.Elements().Single());
}

Expand All @@ -113,8 +117,6 @@ internal Interval<int> ColumnInterval()
}
}



public interface IGridEntity
{
int Row { get; set; }
Expand Down Expand Up @@ -185,6 +187,17 @@ public void FromXml(XElement element, IFromXmlContext ctx)
}
}

public enum InteractionGroup
{
Group1,
Group2,
Group3,
Group4,
Group5,
Group6,
Group7,
Group8,
}
public enum UserQueryPartRenderMode
{
SearchControl,
Expand Down
6 changes: 4 additions & 2 deletions Signum.React.Extensions/Basics/Templates/ColorTypeahead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export function ColorTypeaheadLine(p : { ctx: TypeContext<string | null | undefi
<FormGroup ctx={ctx} labelText={ctx.niceName()} >
<ColorTypeahead color={ctx.value}
formControlClass={ctx.formControlClass}
onChange={handleOnChange} />
onChange={handleOnChange}
placeholder={p.ctx.placeholderLabels ? p.ctx.niceName() : undefined} />
</FormGroup>
);
}
Expand All @@ -37,6 +38,7 @@ interface ColorTypeaheadProps {
color: string | null | undefined;
onChange: (newColor: string | null | undefined) => void;
formControlClass: string | undefined;
placeholder?: string;
}

export function ColorTypeahead(p : ColorTypeaheadProps){
Expand Down Expand Up @@ -79,7 +81,7 @@ export function ColorTypeahead(p : ColorTypeaheadProps){
return (
<Typeahead
value={p.color ?? ""}
inputAttrs={{ className: classes(p.formControlClass, "sf-entity-autocomplete") }}
inputAttrs={{ className: classes(p.formControlClass, "sf-entity-autocomplete"), placeholder: p.placeholder }}
getItems={handleGetItems}
onSelect={handleSelect}
onChange={handleSelect}
Expand Down
4 changes: 3 additions & 1 deletion Signum.React.Extensions/Basics/Templates/IconTypeahead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function IconTypeaheadLine(p : IconTypeaheadLineProps){
return (
<FormGroup ctx={ctx} labelText={ctx.niceName()} >
<IconTypeahead icon={ctx.value}
placeholder={p.ctx.placeholderLabels ? p.ctx.niceName() : undefined}
extraIcons={p.extraIcons}
formControlClass={ctx.formControlClass}
onChange={handleChange} />
Expand All @@ -42,6 +43,7 @@ export interface IconTypeaheadProps {
onChange: (newIcon: string | null | undefined) => void;
extraIcons?: string[];
formControlClass: string | undefined;
placeholder?: string;
}

export function IconTypeahead(p: IconTypeaheadProps) {
Expand Down Expand Up @@ -90,7 +92,7 @@ export function IconTypeahead(p: IconTypeaheadProps) {
return (
<Typeahead
value={(p.icon ?? "")}
inputAttrs={{ className: classes(p.formControlClass, "sf-entity-autocomplete") }}
inputAttrs={{ className: classes(p.formControlClass, "sf-entity-autocomplete"), placeholder: p.placeholder }}
getItems={handleGetItems}
onSelect={handleSelect}
onChange={handleSelect}
Expand Down
16 changes: 14 additions & 2 deletions Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import * as React from 'react'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ValueLine, EntityLine, RenderEntity } from '@framework/Lines'
import { ValueLine, EntityLine, RenderEntity, OptionItem } from '@framework/Lines'
import { tryGetTypeInfos, New, getTypeInfos } from '@framework/Reflection'
import SelectorModal from '@framework/SelectorModal'
import { TypeContext } from '@framework/TypeContext'
Expand All @@ -13,6 +13,7 @@ import { ColorTypeaheadLine } from "../../Basics/Templates/ColorTypeahead";
import "../Dashboard.css"
import { getToString } from '@framework/Signum.Entities';
import { useForceUpdate } from '@framework/Hooks'
import { softCast } from '../../../Signum.React/Scripts/Globals';

export default function Dashboard(p : { ctx: TypeContext<DashboardEntity> }){
const forceUpdate = useForceUpdate();
Expand Down Expand Up @@ -45,6 +46,7 @@ export default function Dashboard(p : { ctx: TypeContext<DashboardEntity> }){
});
}

var colors = ["#DFFF00", "#FFBF00", "#FF7F50", "#DE3163", "#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF"]

function renderPart(tc: TypeContext<PanelPartEmbedded>) {
const tcs = tc.subCtx({ formGroupStyle: "SrOnly", formSize: "ExtraSmall", placeholderLabels: true });
Expand All @@ -56,7 +58,17 @@ export default function Dashboard(p : { ctx: TypeContext<DashboardEntity> }){
<div className="d-flex">
{icon && <div className="mx-2"><FontAwesomeIcon icon={icon} style={{ color: tc.value.iconColor ?? undefined, fontSize: "25px", marginTop: "17px" }} /> </div>}
<div style={{ flexGrow: 1 }} className="mr-2">
<ValueLine ctx={tcs.subCtx(pp => pp.title)} labelText={getToString(tcs.value.content) ?? tcs.niceName(pp => pp.title)} />

<div className="row">
<div className="col-sm-8">
<ValueLine ctx={tcs.subCtx(pp => pp.title)} labelText={getToString(tcs.value.content) ?? tcs.niceName(pp => pp.title)} />
</div>
<div className="col-sm-4">
<ValueLine ctx={tcs.subCtx(pp => pp.interactionGroup)}
optionItems={colors.map((c, i) => ({ label: "Group " + (i + 1), value: "Group" + (i + 1), color: c }))} onRenderDropDownListItem={(io) => <span><span className="sf-dot" style={{ backgroundColor: (io as any).color }} />{io.label}</span>} />
</div>
</div>

<div className="row">
<div className="col-sm-4">
<ValueLine ctx={tcs.subCtx(pp => pp.style)} onChange={() => forceUpdate()} />
Expand Down
8 changes: 8 additions & 0 deletions Signum.React.Extensions/Dashboard/Dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,11 @@ div.row-control-panel {
font-size: 1rem;
margin-left: 2px;
}

.sf-dot {
height: 15px;
width: 15px;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
}
12 changes: 12 additions & 0 deletions Signum.React.Extensions/Dashboard/Signum.Entities.Dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ export module DashboardPermission {
export const ViewDashboard : Authorization.PermissionSymbol = registerSymbol("Permission", "DashboardPermission.ViewDashboard");
}

export const InteractionGroup = new EnumType<InteractionGroup>("InteractionGroup");
export type InteractionGroup =
"Group1" |
"Group2" |
"Group3" |
"Group4" |
"Group5" |
"Group6" |
"Group7" |
"Group8";

export interface IPartEntity extends Entities.Entity {
requiresTitle: boolean;
}
Expand Down Expand Up @@ -92,6 +103,7 @@ export interface PanelPartEmbedded extends Entities.EmbeddedEntity {
row: number;
startColumn: number;
columns: number;
interactionGroup: InteractionGroup | null;
style: Signum.BootstrapStyle;
content: IPartEntity;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export class DashboardFilterController {
}

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

if (partEmbedded.interactionGroup == null)
return [];

var otherFilters = Array.from(this.filters.values()).filter(f => f.partEmbedded != partEmbedded && f.partEmbedded.interactionGroup == partEmbedded.interactionGroup && f.rows?.length);

var result = otherFilters.filter(a => a.queryKey == queryKey).map(
df => groupFilter("Or", df.rows.map(
Expand Down
2 changes: 1 addition & 1 deletion Signum.React.Extensions/Dashboard/View/UserChartPart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export default function UserChartPart(p: PanelPartContentProps<UserChartPartEnti
dashboardFilter={p.filterController.filters.get(p.partEmbedded)}
onDrillDown={(row, e) => {
e.stopPropagation();
if (e.altKey)
if (e.altKey || p.partEmbedded.interactionGroup == null)
handleDrillDown(row, e, chartRequest, handleReload);
else {
const dashboardFilter = p.filterController.filters.get(p.partEmbedded);
Expand Down
3 changes: 3 additions & 0 deletions Signum.React/Scripts/Lines/Lines.css
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,12 @@ input[type=checkbox].form-control-xs {
margin-bottom: 0px;
}

.form-control-xs .rw-widget-input,
.rw-widget-xs .rw-widget-input {
min-height: 24px;
}

.form-control-xs .rw-dropdown-list-input,
.rw-widget-xs .rw-dropdown-list-input,
.rw-widget-xs .rw-combobox-input {
padding: 0 0.4em;
Expand All @@ -339,6 +341,7 @@ input[type=checkbox].form-control-xs {
padding: 0px;
}

.form-control-xs .rw-widget-picker,
.rw-widget-xs .rw-widget-picker {
min-height: 24px;
}
Expand Down
4 changes: 2 additions & 2 deletions Signum.React/Scripts/Lines/ValueLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ function internalDropDownList(vl: ValueLineController) {
}

return (
<FormGroup ctx={s.ctx} labelText={s.labelText} helpText={s.helpText} htmlAttributes={{ ...vl.baseHtmlAttributes(), ...s.formGroupHtmlAttributes }} labelHtmlAttributes={s.labelHtmlAttributes}>
<FormGroup ctx={s.ctx} labelText={s.labelText} helpText={s.helpText} htmlAttributes={{ ...vl.baseHtmlAttributes(), ...s.formGroupHtmlAttributes}} labelHtmlAttributes={s.labelHtmlAttributes}>
{vl.withItemGroup(
<FormControlReadonly htmlAttributes={{
...vl.props.valueHtmlAttributes,
Expand Down Expand Up @@ -343,7 +343,7 @@ function internalDropDownList(vl: ValueLineController) {
return (
<FormGroup ctx={s.ctx} labelText={s.labelText} helpText={s.helpText} htmlAttributes={{ ...vl.baseHtmlAttributes(), ...s.formGroupHtmlAttributes }} labelHtmlAttributes={s.labelHtmlAttributes}>
{vl.withItemGroup(
<DropdownList className={addClass(vl.props.valueHtmlAttributes, classes(s.ctx.formControlClass, vl.mandatoryClass))} data={optionItems} onChange={handleOptionItem} value={oi}
<DropdownList className={addClass(vl.props.valueHtmlAttributes, classes(s.ctx.formControlClass, vl.mandatoryClass, "p-0"))} data={optionItems} onChange={handleOptionItem} value={oi}
filter={false}
autoComplete="off"
dataKey="value"
Expand Down

5 comments on commit 55def71

@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.

Dashboard Filters and InteractionGroup

This commit and the previous ones merge the first milestone in a series of improvements that I want to do to Dashboards after taking a look at this Power BI video: https://www.youtube.com/watch?v=AGrl-H87pRU&t=3352s

Power BI has one important similarity with Signum Chartings and Dashboards: both put a lot of emphasis in creating a good model, relationships, expressions, and then making the dashboard is the last, simplest step.

Of course, Power BI works over multiple types of data sources and places very few restrictions on the schema (a very important feature if you want to sell a product). On the other side the Dashboard designer has more work since it has to first clean up the data and define the data model and relationships. A task that is typically not necessary if you are using Signum Charting / Dashboards over an already created Signum application because is necessary for the DB schema, UI, Search Control, Word reports, Email Templates, etc...

Anyway, one thing I liked a lot about Power BI is how all the widgets in the Dashboard are able to play with each other: When you click on one element of a chart it automatically filters the other widgets by the selected value.

This makes dashboard much more interactive and facilitates data exploration without needing to add non-visual components like EntityLines/EntityCombo/DateTime pickers for combos.

GIF Demo

Animation

What are we seeing:

  1. Clicking in a bubble for a customer in the BubblePack is filtering all the other charts by this customer.
  2. By Ctrl + Click on another bubble we filter by two customers at the same time (OR).
  3. By clicking in the parent bubble, we are filtering by the Customer's country.
  4. We can remove the filter by clicking in the x in the small filter badge on the Widget's filter.
  5. In the BubblePlot Producst share, we select a product to filter.
  6. Then we click in any empty place of the BubblePlot to remove the filter (this doesn't work on TreeMap because the is no empty place, so use 4. instead).
  7. In the StackedArea we can click in one particular number (not shown) but we can click on the horizontal axis to filter by month, or in the legend to filter by employee.

All the chart types have been upgraded, except for the maps (SVG or Google maps).
Not shown in this Gif Demo: Once you are filtering by some value in one widgets, you can further refine the filter by clicking in another value in another widget.
The CalendarStream has been updated, now shows the month names and days of the week.

How to activate it:

All this new functionality overrides the default behavior when clicking on a chart (opening a SearchModal with the values), so after some consideration we decided to make it opt-in.

All the Dashboard Parts have now a nullable InteractionGroup property:

image

By default the value is null, so:
Click => Opens SearchModal
Ctrl + Click => Opens SearchPage in another tab.

But if you choose an InteractionGroup then:
Click => Sets the selected value as filter for the other parts in the same InteractionGroup
Ctrl + Click => Adds/Removes another value as filter for the other parts in the same InteractionGroup
Alt + Click => Opens SearchModal
Ctrl + Alt + Click =>Opens SearchPage in another tab.

Future Improvements

I have thee ideas that could complement this feature:

Dashboard filters

Now that all the widgets can send/receive filters to each other through the Dashboard, could make sense to allow global filters on the top of the dashboard using traditional pinned filters.

Alternatively, or complementary, add the ability to set default Dashboard Filters: For example when a user navigates to a dashboard, it looks like he has already clicked in his own Employee, but he could clear the filter or add another filter.

Not sure which of this two features could be more usefull.

QueryToken equivalences between different queries

Right now a widgets can only interact with another widgets if they use the same queryName. Maybe is useful have another widgets... like OperationLog by Employee and somewhere express in the Dashboard that:

query: Order, token: Employee == query: OperationLog, token: User.Employee

But not sure if the feature will be understandable / useful enough.

Cached Queries (OLAP cubes)

Since currently each widget is doing direct SQL queries to the database, once many users start clicking in an interactive dashboard the performance problems could get worst.

What most BI products do is to work on some cached date that is refreshed on a regular basis. This cached data can contain only fine-grained aggregates that are combined together in the client computer to produce the desired chart. See OLAP cube.

image

The Dashboard could be configured to be cached (globally? or per part?) and analyze the different widgets to determine which key columns and aggregates would be needed.

Then, on a regular basis (with an Scheduled Task) save the results of this query to a JSON file (in Azure Blob storage for example). The web client will then automatically download this file make the queries client side, by combining the OLAP cubes.

I like this solution because will scale very well (only requires download from blob storage and client-side CPU processing) and can still be very integrated with the rest of the application, reusing Entity translations/format/unit as usual. Also will be easy to jump back to real-time SQL queries whenever is needed, like opening a SearchModal. Or give a permission to allow see real-time data to some roles.

But this also has some limitations:

  • If a lot of dimensions are added because of having many widgets in the same InteractionGroup filtering by different keys, the file could get quite big. The serialized result could be just a ResultTable json file, of slightly more optimized to remove redundancy on repeated keys.
  • For Entity-dependent dashboards, or widgets with pinned filters, this dimensional problem could get even more complicated, so cached queries won't be worth in this cases.
  • Finally cached queries could produces a security problem since they won't follow TypeConditions.

Anyway, I think there are use cases where this feature could be very useful: Global interactive dashboards without pinned filters and maybe restricted to a few roles.

Conclusion

I have implemented the first important step to make dashboards cooler,

🙏 Any feedback is welcome to see if future development in any of this three ideas (or any other) is worth.

@JafarMirzaie
Copy link
Contributor

@JafarMirzaie JafarMirzaie commented on 55def71 Nov 8, 2021 via email

Choose a reason for hiding this comment

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

@doganc
Copy link

@doganc doganc commented on 55def71 Nov 8, 2021

Choose a reason for hiding this comment

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

Awesome, very good job

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on 55def71 Nov 8, 2021 via email

Choose a reason for hiding this comment

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

@rezanos
Copy link
Contributor

@rezanos rezanos commented on 55def71 Nov 8, 2021

Choose a reason for hiding this comment

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

Great job! Fantastic feature 👏👏

Please sign in to comment.