Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Earn a 50% discount on the DP-600 certification exam by completing the Fabric 30 Days to Learn It challenge.

Reply
JERN
Regular Visitor

Use conditional formating to set font to transparent

My goal is to create a sales report where each sales team member can see the Total Sales measure for each sales team member, but the only "employee_last" field they will be able to see is their own. Assume I can derive the report viewer's email address from userprincipalname().  For example:

 

table 1 is the user table

employee_lastemployee_emailemployee_id 
Adamsaadams@acme.com1
Jonesajones@acme.com2
Smithasmith@acme.com3
Thompsonathompson@acme.com4

 

table 2 is the sales table.

employee_idsale_amount
15
17
116
28
23
29
36
34
32
48
49
43

 

Table 3 is the full result table.

employee_lastTotal Sales
Adams28
Jones20
Smith12
Thompson20

 

Let's say employee Smith is looking at the report, what I want them to see is: 

employee_lastTotal Sales
 28
 20
Smith12
 20

 

This way they can see all of the sales data but can only put a name with their own results.  I've tapped out my knowledge on conditional formatting and I'm really not looking for RLS.  Any help would be appreciated.

1 ACCEPTED SOLUTION
Barthel
Solution Sage
Solution Sage

Hey @JERN 

To achieve this you will have to use an RLS, by indeed using the userprincipalname() function. Create a role (e.g. 'RLS') and apply the following rule to the empolye email column in table 1.

Barthel_0-1670072299773.png

The row level security filters table 1 based on which account (email) the report is viewed with. After filtering, there should be one row left in table 1 (with the email address of the person viewing the report), with wich we can generate a specific view for that person.

Working with transparent colors can be risky: if you hover over the table, the names are still visible in the tooltip and if you export the data you can also trace the names. I believe it's more safe to anonymize the data itself. First you assign an anonymous label to an employee in table 1. You can use this query code (a new anonymous label is created every time you refresh the data):

 

let
    Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WckxJzC1W0lFKTAQxHBKTc1P1kvNzgSKGSrE60Upe+XmpYPksEANZ3ggsH5ybWZIBki8GMZDljcHyIRn5uQXF+XkgJSVQNrIqE6XYWAA=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [employee_last = _t, employee_email = _t, employee_id = _t]),
    #"Changed Type" = Table.TransformColumnTypes(Source,{{"employee_last", type text}, {"employee_email", type text}, {"employee_id", Int64.Type}}),
    #"Added Custom" = Table.AddColumn(#"Changed Type", "anonymize", each let 
    StringLength = 8,
    ValidCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456879",
    fnRandomCharacter = (text) => Text.Range(ValidCharacters,Int32.From(Number.RandomBetween(0, Text.Length(ValidCharacters)-1)),1),
    GenerateList = List.Generate(()=> [Counter=0, Character=fnRandomCharacter(ValidCharacters)],
                   each [Counter] < StringLength,
                   each [Counter=[Counter]+1, Character=fnRandomCharacter(ValidCharacters)],
                   each [Character]),
    RandomString = List.Accumulate(GenerateList, "", (a,b) => a & b)
in
    RandomString),
    #"Changed Type1" = Table.TransformColumnTypes(#"Added Custom",{{"anonymize", type text}}),
    Custom1 = Table.Buffer(#"Changed Type1")
in
    Custom1

 

Then you want to compile table 3 in such a way that the personal and anonymous data are combined. Use this code for this:

 

let
    Source = table1,
    #"Added Custom" = Table.AddColumn(Source, "table2", each table2),
    #"Expanded table2" = Table.ExpandTableColumn(#"Added Custom", "table2", {"employee_id", "sale_amount"}, {"table2.employee_id", "table2.sale_amount"}),
    #"Merged Queries" = Table.NestedJoin(#"Expanded table2", {"table2.employee_id"}, table1, {"employee_id"}, "table1", JoinKind.LeftOuter),
    #"Expanded table1" = Table.ExpandTableColumn(#"Merged Queries", "table1", {"anonymize"}, {"table1.anonymize"}),
    #"Added Custom1" = Table.AddColumn(#"Expanded table1", "employee_last_2", each if [employee_id] = [table2.employee_id] then [employee_last] else [table1.anonymize]),
    #"Removed Other Columns" = Table.SelectColumns(#"Added Custom1",{"employee_id", "table2.sale_amount", "employee_last_2"}),
    #"Renamed Columns" = Table.RenameColumns(#"Removed Other Columns",{{"table2.sale_amount", "sales_amount"}, {"employee_last_2", "employee_last"}}),
    #"Changed Type" = Table.TransformColumnTypes(#"Renamed Columns",{{"sales_amount", Int64.Type}}),
    #"Grouped Rows" = Table.Group(#"Changed Type", {"employee_id", "employee_last"}, {{"total_sales", each List.Sum([sales_amount]), type nullable number}})
in
    #"Grouped Rows"

 

Then connect table 1 and table 3 in the data model based on employee_id.

Barthel_2-1670075538533.png

Place the employee_last and total_sales columns from Table 3 in a visual. In the image below I have applied the email address from table 1 as a filter on the visual and you see that only the employee of the selected email address is shown and the rest are anonymous. 

Adams:

Barthel_3-1670075846361.png

 

Jones:

Barthel_4-1670075892368.png

Now it works through a filter, when you upload the report in server, the table is automatically filtered through the row level security and the filter is no longer needed.

As soon as you upload the report you can enable the row level security on the dataset with the option 'Security'. 

View solution in original post

4 REPLIES 4
JERN
Regular Visitor

Barthel, if you still have it could you post what your table 3 contains.  I've been semi-successful but I am getting a 1 to 1 relationship connecting table 1 and 3.

Hey @JERN,

Sure glad to help. Table 3 is a combination of Table 1 and Table 2. You start with Table 1 and add Table 2 as a calculated column. You then expand this, so you end up with the combination between table 1 and table 2. You do this because you want to show all sales for each employee, with only his own sales being personally visible and the rest anonymized. To achieve this, the following transformations are performed, where a merge takes place and another calculated column is added. In the end, the sales are aggregated per employee so that you have the total sales. 

This is how your table should look like:

total_salesemployee_idemployee_last

281Adams
201uHuUUfaF
121ZOCusvKO
201ucRQs2Ty
282aLxVVBRT
202Jones
122ZOCusvKO
202ucRQs2Ty
283aLxVVBRT
203uHuUUfaF
123Smith
203ucRQs2Ty
284aLxVVBRT
204uHuUUfaF
124ZOCusvKO
204Thompson
Barthel
Solution Sage
Solution Sage

Hey @JERN 

To achieve this you will have to use an RLS, by indeed using the userprincipalname() function. Create a role (e.g. 'RLS') and apply the following rule to the empolye email column in table 1.

Barthel_0-1670072299773.png

The row level security filters table 1 based on which account (email) the report is viewed with. After filtering, there should be one row left in table 1 (with the email address of the person viewing the report), with wich we can generate a specific view for that person.

Working with transparent colors can be risky: if you hover over the table, the names are still visible in the tooltip and if you export the data you can also trace the names. I believe it's more safe to anonymize the data itself. First you assign an anonymous label to an employee in table 1. You can use this query code (a new anonymous label is created every time you refresh the data):

 

let
    Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WckxJzC1W0lFKTAQxHBKTc1P1kvNzgSKGSrE60Upe+XmpYPksEANZ3ggsH5ybWZIBki8GMZDljcHyIRn5uQXF+XkgJSVQNrIqE6XYWAA=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [employee_last = _t, employee_email = _t, employee_id = _t]),
    #"Changed Type" = Table.TransformColumnTypes(Source,{{"employee_last", type text}, {"employee_email", type text}, {"employee_id", Int64.Type}}),
    #"Added Custom" = Table.AddColumn(#"Changed Type", "anonymize", each let 
    StringLength = 8,
    ValidCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456879",
    fnRandomCharacter = (text) => Text.Range(ValidCharacters,Int32.From(Number.RandomBetween(0, Text.Length(ValidCharacters)-1)),1),
    GenerateList = List.Generate(()=> [Counter=0, Character=fnRandomCharacter(ValidCharacters)],
                   each [Counter] < StringLength,
                   each [Counter=[Counter]+1, Character=fnRandomCharacter(ValidCharacters)],
                   each [Character]),
    RandomString = List.Accumulate(GenerateList, "", (a,b) => a & b)
in
    RandomString),
    #"Changed Type1" = Table.TransformColumnTypes(#"Added Custom",{{"anonymize", type text}}),
    Custom1 = Table.Buffer(#"Changed Type1")
in
    Custom1

 

Then you want to compile table 3 in such a way that the personal and anonymous data are combined. Use this code for this:

 

let
    Source = table1,
    #"Added Custom" = Table.AddColumn(Source, "table2", each table2),
    #"Expanded table2" = Table.ExpandTableColumn(#"Added Custom", "table2", {"employee_id", "sale_amount"}, {"table2.employee_id", "table2.sale_amount"}),
    #"Merged Queries" = Table.NestedJoin(#"Expanded table2", {"table2.employee_id"}, table1, {"employee_id"}, "table1", JoinKind.LeftOuter),
    #"Expanded table1" = Table.ExpandTableColumn(#"Merged Queries", "table1", {"anonymize"}, {"table1.anonymize"}),
    #"Added Custom1" = Table.AddColumn(#"Expanded table1", "employee_last_2", each if [employee_id] = [table2.employee_id] then [employee_last] else [table1.anonymize]),
    #"Removed Other Columns" = Table.SelectColumns(#"Added Custom1",{"employee_id", "table2.sale_amount", "employee_last_2"}),
    #"Renamed Columns" = Table.RenameColumns(#"Removed Other Columns",{{"table2.sale_amount", "sales_amount"}, {"employee_last_2", "employee_last"}}),
    #"Changed Type" = Table.TransformColumnTypes(#"Renamed Columns",{{"sales_amount", Int64.Type}}),
    #"Grouped Rows" = Table.Group(#"Changed Type", {"employee_id", "employee_last"}, {{"total_sales", each List.Sum([sales_amount]), type nullable number}})
in
    #"Grouped Rows"

 

Then connect table 1 and table 3 in the data model based on employee_id.

Barthel_2-1670075538533.png

Place the employee_last and total_sales columns from Table 3 in a visual. In the image below I have applied the email address from table 1 as a filter on the visual and you see that only the employee of the selected email address is shown and the rest are anonymous. 

Adams:

Barthel_3-1670075846361.png

 

Jones:

Barthel_4-1670075892368.png

Now it works through a filter, when you upload the report in server, the table is automatically filtered through the row level security and the filter is no longer needed.

As soon as you upload the report you can enable the row level security on the dataset with the option 'Security'. 

Barthel, thank you for the answer, I really appreciate the advice on the risk associated with the transparent color and I think it works for what I need.  My issue is I'm not well versed in M and I have to build this in to a far more complex report, I do believe I can work my way through it step-by-step to make it work, it will just take a bit of time.

Helpful resources

Announcements
LearnSurvey

Fabric certifications survey

Certification feedback opportunity for the community.

PBI_APRIL_CAROUSEL1

Power BI Monthly Update - April 2024

Check out the April 2024 Power BI update to learn about new features.

April Fabric Community Update

Fabric Community Update - April 2024

Find out what's new and trending in the Fabric Community.