Logo

Tag Archive for quick tip

The Model View Controller (MVC) Method of Building EA Dashboard Interactivity With Bindings – Part 2

In Part 1 we looked at the MVC method of building Einstein Analytics dashboards conceptually.  Now we are going to review specific examples and code to put it into practice.  First, let me say that I’m not sure everything that is accomplished is fully supported by the platform.  So know that it could potentially break at any point in time.  However, it has worked for a few years and while some examples for how you could use this can be accomplished via other methods, especially now with the ‘advanced editor’ via the UI – there are use cases where I’ve found this to be the only method that works.

A Few Use Cases

I have used this MVC design pattern in other programming languages so when I ran into roadblocks in trying to design a dashboard with certain functionality I tried it as a workaround and I was very happy when it ended up working!  I have used this in cases where we have a single widget (or multiple that leverage the same step/query) but need a variety of control from the end user.  Sure, if they just want to switch the grouping and/or the measure we would be fine.  But what if at the same time they needed to toggle the specific function that is being calculated on the fly (for example a windowing function).  These functions need to already have the stream organized and ‘grouped’ properly prior to the calculation.  Moreover, you need to specify that grouping and measure within the function.  On top of all of this, I’ve had to build dashboards that toggle between not only various groupings but completely different timeframes (hour, year-week, year-month-day, month) and use different summary calculations (avg, running total, etc.) and these all have to be specified within the function and the stream already ‘prepped’ with the proper information.

A Relatively Simple Example

What follows is a relatively simple example where we are only changing the measure, toggling between ActivityDate and CreatedDate and also toggling whether we are viewing the ‘actual’ count of records or a Moving Average period of 10, 15, 30, 45 or 60 days.  Just know that you could easily expand on this and add additional toggles for some of the use cases I mentioned above.  Once you set up the dashboard in MVC you have laid the groundwork to add additional functionality relatively easily.

 

In order to build this we need our visual components which are the widgets and the static steps for the Toggles.  These are fairly simple and can be input via the UI:

 

Then, you need to create a Controller Step that will use toggles to ‘feed’ code into the main step that feeds the chart.

Below is the full code for the Controller Step.  Essentially it takes a single ‘row’ (limit 1) of the dataset that we are using so we ‘know’ we will have at least one result and then we use a series of case statements in order to evaluate the user selections and output code based on those:

"ControllerStep": {

"broadcastFacet": true,

"groups": [],

"label": "ControllerStep",

"numbers": [],

"query": "q = load \"activity\";\nq = foreach q generate case when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"Actual\" then \"sum(count)\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"10 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-10..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"15 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-15..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"30 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-30..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"45 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-45..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"60 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-60..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"Actual\" then \"sum(count)\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"10 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-10..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"15 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-15..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"30 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-30..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"45 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-45..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"60 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-60..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" else \"avg(sum(count)) over ([-60..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" end as 'WindowFunction', 'ActivityDate_Year' + \"~~~\" + 'ActivityDate_Month' + \"~~~\" + 'ActivityDate_Day' as 'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day', count() as 'count';\nq = limit q 1;",

"receiveFacetSource": {

"mode": "all",

"steps": []

},

"selectMode": "single",

"strings": [],

"type": "saql",

"useGlobal": true,

"visualizationParameters": {

"parameters": {

"borderColor": "#e0e5ee",

"borderWidth": 1,

"cell": {

"backgroundColor": "#ffffff",

"fontColor": "#16325c",

"fontSize": 12

},

"columnProperties": {},

"columns": [],

"customBulkActions": [],

"header": {

"backgroundColor": "#f4f6f9",

"fontColor": "#16325c",

"fontSize": 12

},

"innerMajorBorderColor": "#a8b7c7",

"innerMinorBorderColor": "#e0e5ee",

"maxColumnWidth": 300,

"minColumnWidth": 40,

"mode": "variable",

"numberOfLines": 1,

"showActionMenu": true,

"showRowIndexColumn": true,

"totals": true,

"verticalPadding": 8

},

"type": "table"

}

},

Then, we use those outputs from the ‘ControllerStep’ in our primary step that feeds the visual chart on the dashboard:

"ActivityDate_Year_Ac_1": {

"broadcastFacet": false,

"groups": [],

"label": "ActivityDate_Year_Ac_1",

"numbers": [],

"query": "q = load \"activity\";\nq = filter q by date('ActivityDate_Year', 'ActivityDate_Month', 'ActivityDate_Day') in [dateRange([2018,4,28], [2018,8,12])];\nq = group q by ({{coalesce(cell(static_2.selection, 0, \"FirstGroup\"), \"'ActivityDate_Year', 'ActivityDate_Month', 'ActivityDate_Day'\").asString()}});\nq = foreach q generate {{coalesce(cell(static_2.selection, 0, \"Year\"), \"'ActivityDate_Year'\").asString()}} + \"~~~\" + {{coalesce(cell(static_2.selection, 0, \"Month\"), \"'ActivityDate_Month'\").asString()}} + \"~~~\" + {{coalesce(cell(static_2.selection, 0, \"Day\"), \"'ActivityDate_Day'\").asString()}} as {{coalesce(cell(static_2.selection, 0, \"2ndGroupOrder\"), \"'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'\").asString()}}, count(q) as 'count';\nresult = group q by {{coalesce(cell(static_2.selection, 0, \"2ndGroupOrder\"), \"'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'\").asString()}};\nresult = foreach result generate {{coalesce(cell(static_2.selection, 0, \"2ndGroupOrder\"), \"'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'\").asString()}}, {{coalesce(cell(ControllerStep.result, 0, \"WindowFunction\"), \"sum(count)\").asString()}} as 'MovingAvg';\nresult = order result by ({{coalesce(cell(static_2.selection, 0, \"2ndGroupOrder\"), \"'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'\").asString()}} asc);\nresult = limit result 2000;",

"receiveFacetSource": {

"mode": "none",

"steps": []

},

"selectMode": "single",

"start": [],

"strings": [],

"type": "saql",

"useGlobal": false,

"visualizationParameters": {

"parameters": {

"autoFitMode": "keepLabels",

"showPoints": false,

"legend": {

"descOrder": false,

"showHeader": true,

"show": true,

"customSize": "auto",

"position": "right-top",

"inside": false

},

"axisMode": "multi",

"tooltip": {

"showBinLabel": true,

"measures": "",

"showNullValues": true,

"showPercentage": true,

"showDimensions": true,

"showMeasures": true,

"customizeTooltip": false,

"dimensions": ""

},

"visualizationType": "time",

"missingValue": "connect",

"dashLine": {

"measures": "",

"showDashLine": false

},

"timeAxis": {

"showTitle": true,

"showAxis": true,

"title": ""

},

"title": {

"fontSize": 14,

"subtitleFontSize": 11,

"label": "",

"align": "center",

"subtitleLabel": ""

},

"trellis": {

"flipLabels": false,

"showGridLines": true,

"size": [

100,

100

],

"enable": false,

"type": "x",

"chartsPerLine": 4

},

"fillArea": true,

"showActionMenu": true,

"showZero": true,

"measureAxis2": {

"sqrtScale": false,

"showTitle": true,

"showAxis": true,

"title": "",

"customDomain": {

"showDomain": false

}

},

"measureAxis1": {

"sqrtScale": false,

"showTitle": true,

"showAxis": true,

"title": "",

"customDomain": {

"showDomain": false

}

},

"valueType": "none",

"theme": "wave",

"applyConditionalFormatting": true,

"drawArea": {

"measure": "",

"showDrawArea": false,

"bounding1": "",

"bounding2": ""

}

},

"type": "chart"

}

},






I didn’t do it here but for a more polished look you can even update all display names on the chart axis with the proper string based on the user selection.

Please let me know if you have had success with this.  Please let me know if you have a better solution!  Honestly this is a bit of a hack that I’ve come up with but it has saved me countless times!  Every time I think I can do it via normal binding functionality I run into gaps and limitations due to issues with having to already have the stream prepped for the function or specifying the measure and grouping in the function which you cannot do with normal binding syntax.  This has been a lifesaver and I hope it is helpful to you.  Until next time, keep riding the Wave (or Einstein Analytics)!  Once Wave Always Wave!

SAQL String Filter Bindings With Commenting

Salesforce Quick Tips

Use SAQL Bindings for Convenient Text Filters

Using the “asEquality” function within bindings works in use cases where your data contains the fields you want to filter by.  This is great in many but not all cases.  I have recently ran across a number of scenarios where the request is to filter by some complex logic – multiple fields, aggregations, the result of another step (only pull the top 5% of performers by the date range selected) but only if the selection to filter at all is made by the user.

In these cases you want nothing (additional) to be filtered in the query unless the proper selection is made by the user interacting with the dashboard.  In these cases I have resorted to text/string based filters.  If no selection is made or the default is used we simply add two dashes “–” to comment out the line.  If the filter is to be used, we simply pass the appropriate text string from the static step or the new term, “custom query.”

This has been very handy in a pinch when either the filter criteria is not something that is contained within the dataset already or in cases where it would not be possible to add them to the necessary dataflow to include them in the dataset.

How It Works – String SAQL Filter Bindings

This is a basic use case that does not require a string binding to filter, but I wanted a simple and clear example to explain the concept.  In my case, I am starting with a pillbox toggle widget to control the filtering.  Then I go to create my custom query:

SAQL String Filter Binding Custom Query I simply add my three values I want to display visually to the user in the “Display” column, and then add a hidden “Text2” column that will be the corresponding filter values.  When the user selects “All” I don’t want any filter so I just add two dashes to comment out the line “–” and the two filter values that I would write manually within the SAQL if the user selects the other options.

This creates a query or step that looks like this in the JSON:

SAQL String Filter Binding Step

I change this query to “Single Selection (required)” and set the initial selection to “All.”

Then I need to place the binding within my original query/step.  I do that in the highlighted section here:

SAQL String Filter Binding Placement

That’s it.  Now when I toggle between the options, the binding either passes in two dashes to comment out the line or the necessary filter line:

 

When I “Show Details” to view the “Final Query” when running the dashboard and “All” is selected, it passes the two dashes “–” as expected:

SAQL String Filter Binding Comment

When I do the same with one of the other values selected, in this case “Event” – it passes the appropriate filter:

SAQL String Filter Binding Filter

Wrap

I believe that too often the ‘asString’ binding function is glossed over and use cases that are only possible with SAQL and passing the binding as a string are missed.  You can use this almost anywhere necessary within a SAQL query.  I’ve even used it to toggle between time zones and used a string binding within every filter, grouping, select/generate, order, etc. statement within the final query.  This allows the user to change the query entirely with a single click.  Going back to the filter example, I’ve had use cases where I need a 300 character filter statement within the query most of the time, so I add it to the original query.  But in some cases the user wants to disable that complex filter.  In these cases, I create a custom query and a binding that would return either the two dashes “–” to comment out the filter or nothing/empty string “” to keep the filter in place.

Is this helpful, silly, crazy?  There are so many use cases for this and it really does come in handy while working real world scenarios.  Would love to hear your thoughts in the comments!

Convert Decimal Time Into Hours and Minutes – HH:MM – Wave Einstein Analytics

Salesforce Quick Tips

Thought I would write about this Analytics Quick Tip as I have found zero information related to this online and I would imagine it would be a pretty common use case.

The Problem With Decimal Hours

Often you have ‘duration’ type measures stored as ‘number of seconds.’  Think of the duration of a service ticket from one stage to the next, or the duration of a sales call.  Many times you want to aggregate all of these together for a particular user or particular day.  Once you’ve aggregated them together, you can easily convert these into decimal hours by dividing by 60 to arrive at # of minutes and 60 again to arrive at # of hours (divide by 3600 once to shortcut this step).  The trouble is then you have a confusing metric of decimal hours.  3.45 hours does not equal 3 hours and 45 minutes.  It is closer to 3 hours and 30 minutes!  4.87 does not equal 4 hours and 87 minutes!  No matter how many times you try and educate your users on decimal hours it is confusing for them to discuss with their teams and properly rank and compare them to each other.

A SAQL Solution For Converting Decimal Hours to HH:MM

I think it’s helpful to break each step down into its component part before we get into the code of how we are going to accomplish this.

We want to take 4.87 and convert to 4:52.

  1. First we want to take out the # preceding the decimal point as it is already a valid # of hours. (4)
  2. Then we want to add a colon to separate hours from minutes. (4:)
  3. Then we want to take the remaining number post decimal point and multiply by 60 (this number will be formatted to only 2 digits and placed after the colon). (4:52 – we arrive at this by taking the entire 4.87 and subtracting the truncated number of 4 which leaves .87 and then we multiply that by 60)

Simple corresponding example below without formatting and where ‘decimalhour’ is already in decimal hours.  You would just replace ‘decimalhours’ with your duration measure and divide by 3600 if converting from seconds.

1. trunc(sum('decimalhours'))
2. + ":" +
3. sum('decimalhours') - trunc(sum('decimalhours')) * 60

Here is an example of the full saql w/ formatting.

 number_to_string(trunc(sum('decimalhours')),"#") + ":" + number_to_string(((sum('decimalhours')) - (trunc(sum('decimalhours')))) * 60,"0#") as 'HH:MM'

Now we have a very easy to read table in hours and minutes!

Owner Total Time
Bruce Kennedy 4:36
Catherine Brown 4:02
Chris Riley 3:55
Dennis Howard 3:29

The only issue I’ve found is when someone is right on the hour, say 4:00 and it only displays a single zero. I’ve solved for this with a case statement as in all of my use cases we are 5 hours and below so I only need 5 case statements.  This is very easy to do.  I had first tried replace but it would then replace all scenarios where there is a zero after the colon.  I’m sure there is a more elegant solution but I have not had the chance to spend very much time on this piece and the case statement solved my particular situation.

Did this solution help you?  Do you have another method to solve the same problem?  Let me know in the comments.  I would love to hear from you!

Can’t Clone Date or Range Widgets – Multiple Pages Workaround

Date or Range Widgets – The Problem

I wanted to create an interactive, multiple page dashboard like the one below:

The issue is that I wanted the same date widget to be accessible on every page as your user may want to modify the dates at any time.  Typically, you can create all of the widgets (containers, numbers, charts, lists/dropdowns) on a single page and then select them to add them to other pages, as below:

The problem is you cannot do this with Date widgets.  As of Winter ’19 (in a pre-release org) you get an error message that states: “Warning! Can’t clone Date or Range widgets.”  hmmmm….OK

Warning! Can't clone Date or Range widgets.

Warning! Can’t clone Date or Range widgets.

As the video demonstrates above, the “Add to Page” feature works fine for dropdown lists for use in faceting or bindings along with many other widget types but not for dates…

Workaround #1 – Clone Entire Page

If you are lucky enough to place the date widget onto the main page and set it up prior to creating your additional pages you can just clone the entire page and your date widget will be cloned (despite the error message) onto every page using not only the same step but the same widget (a linked widget just as you would expect).

Date Widget Is Linked!

Of course, this is only an option if you have not yet created your additional pages. In my opinion, it is the preferred workaround because it create a truly ‘linked’ widget and changes to the widget will not have to be manually replicated onto additional pages.  Additionally, it is a no-code workaround which is another plus for many.

Workaround #2 – Update Steps

This workaround consists of creating a second, third, … (as many as necessary) date widgets for each additional page required in your dashboard.  Then, you will update the JSON to point these new date widgets to the original step that the original date widget is using from page 1.

After creating the date widget onto the canvas, actually set up the underlying step by clicking on the “Date” link and selecting a dataset and date field.  You can choose any date field because we will be updating the underlying step manually in a minute.

This step of actually setting up a step is necessary because you cannot drag the date step from the list of steps to the date widget.

Can’t drag date step to date widget – I guess that’s why it’s italicized!

Now that we have the original date widget and step from page 1 AND all of the additional date widgets on subsequent pages we CMD+E or Ctrl+E into the JSON!

Locate the original date widget and step from page 1 (you can also click on the widget prior to accessing the JSON and view the details there).

In this case I can see that my widget from page 1 is using the step “Last_Activity_1”.  You will want to save this value because we want to update the date widgets from the other pages to also use this step.  Once you have located the other date widgets and the steps they currently leverage you can update them.

I like to use the Find & Replace tool that is hidden until you CMD+F or Ctrl+F twice (that’s right, use the same keyboard shortcut twice in a row!  Magic!).  Now it’s simply a matter of finding the incorrect values and replacing them with the correct value.

Once I have replaced all of them (only the ‘Step’ values), the widgets are linked to the same step and the user can now update the date widget on any page and their selection will persist while navigating to any other page.

Have you encountered this before?  I suppose a third workaround may be using code to actually add the same widget (linked) onto each page through JSON however this would be a lot more work for virtually the same result.

How long will we have to live without the ability to drag date steps to the widget or use the Add to Page functionality for date widgets? (Even though we can currently clone the entire page with the date widget included).  Let me know your thoughts and your best Einstein Analytics workarounds in the comments!

Copy Salesforce Matrix Report & Other Tables Into Excel Without Check Boxes

Salesforce Quick Tips

Copy Salesforce Matrix Report Into Excel

As much as we’d like to keep and share data in the cloud it’s sometimes necessary to take your salesforce data and place it in a spreadsheet to share with others.  You can always export the detail view (tabular view with one row per record) into a .csv or .xls format.  However, sometimes it’s beneficial to be able to take your summarized matrix view and paste directly into Excel.  Especially in cases where you may have written a lot of complex report formulas (these don’t export unless you use “Printable View”) or added other aggregations: sum, min, max, avg.  In order to replicate in Excel you would need to create a pivot table on the detailed export.  Why rebuild if you don’t have to?

Check Boxes: The Issue With Copying and Pasting From Salesforce Matrix Reports

The primary issue with copying and pasting from Salesforce matrix reports is that the copy/paste includes the checkboxes on the left side of the table.  I’ll show you how to get rid of those pesky boxes and copy/paste like a pro!  Note – you can also use the “Printable View” button, however this trick works in a pinch and works for more than just reports as noted below!

Salesforce Matrix Report

Step 1: Copy the entire table as displayed below.

Copy Salesforce Matrix Report Cells

Very important to copy every cell, so start above the table if necessary to ensure you’ve copied the first and last cells.

Make Sure to Copy From Above Report Results

Step 2: Ctrl + c to Copy!

Step 3: Paste into Excel.

Paste Salesforce Matrix Report in Excel

Salesforce Matrix Report In Excel…not pretty!

See the checkboxes in column A?  You can delete the column or try selecting them with your mouse, and still you won’t be able to remove them using those methods!  We’ll show you how you can select the checkboxes, so you can remove them!

Step 4: Press F5 and click “Special…”

F5 In Excel Click Special

Step 5: Select “Objects” and click OK.

Select Objects and OK

Step 6: Now the checkboxes are selected, so hit the “Delete” key on your keyboard to get rid of them!

The Check Boxes are Selected and Delete

Step 7: Now the formatting needs some work.

Fix Excel Formatting

Step 8: Remove Column A.  Fix colors, borders, font and size.  Now you have one version of a Salesforce Matrix Report in Excel without the check boxes!

Final Excel Table Copied From Matrix Report in Salesforce

Other Salesforce Tables

This trick also works for other Salesforce tables that you want to copy and paste from, like a list of standard or custom fields on an object:

You can just highlight, copy & paste and then use the trick above to remove the “Indexed” fields and/or check box images.

Conclusion

This saves you time from rebuilding and ensures accuracy (no more copying and pasting individual #’s).  You can keep the formulas and summaries that you worked so hard to build in Salesforce and get them into Excel for your users who need them presented this way.  Please remember to also try the “Printable View” button which also may meet your needs, however it won’t work for lists of fields and other tabular data out of your Salesforce org!  Do you have any other tips and suggestions for working with Salesforce reports in Excel?  I would love to hear them!  Comment below or tweet them @SFDC_r!

Avoid Long Load Time To Edit Dashboard Component Report

Salesforce Quick Tips Edit Dashboard Component Report

Edit Dashboard Component Report

Have you ever wanted to edit the underlying report for a dashboard component and found it cumbersome?  A dashboard executes with a ‘running user’ and this user feeds into all of the underlying reports.  This is great, because it means that you can create a single set of reports and not have to edit the reports for the dashboard to provide useful data to an entire set of your users.

The Problem – Edit Dashboard Component Report

Because your user is likely placed very high up in the role hierarchy (so that you have access to all or most of the Org’s records) when you click on a dashboard component to view the underlying report, it can take many minutes (or even timeout).  Your users don’t have this issue, they click the dashboard component and the user running the report would only see records at or below them in the role hierarchy – likely running quickly.  You as the admin, however, can only finally click “Customize” to edit the report after waiting a long time (and that’s if it doesn’t timeout).

The other option to avoid the above dilemma (and wait) is to navigate to “Reports” and locate the report you want to edit.  Instead of running the report, click the arrow and “Customize.”  The issue here is that you have to know which report you want to edit, which probably requires “Editing” the Dashboard to view the exact name of the report for the specific component.  This can be time consuming and cumbersome as well.

A Better Solution – Salesforce Quick Tip!

I present to you a better solution.  Simply right click the Dashboard Component you would like to edit the underlying report for and copy the URL.  Paste the URL into your browser (probably a new tab) it should look something like this: https://[YOURSERVERURL].salesforce.com/00O61000003p9Nj if it happens to have anything after the Report ID – just remove it so it looks like the above.  Now add “/e?” to the end of the URL.

Final URL: https://[YOURSERVERURL].salesforce.com/00O61000003p9Nj/e?

Problem Solved

Now just hit Enter and you will be taken directly to editing the underlying report – and it will load quick.  Never get stuck waiting for a report to load just to edit it!

What quick tips do you have related to Dashboards or Reports?  Feel free to post in the comments or on Twitter!

Multiple tabs are your Salesforce friends!

I’m sure you leverage multiple tabs in your everyday web browsing life, but have you utilized them in Salesforce.com?  No, I’m not talking about (my 3-year-old son says, “but but but that’s not what I was talking about!) the objects you add to apps to display in the tab bar at the top of every page within Salesforce.  I’m talking about using multiple browser tabs.  I’ve found them to be instrumental in day-to-day administration and configuration of the platform.

For example, I’ve opened multiple browser tabs to quickly compare 5 or 10 page layouts and quickly used the keyboard to toggle between them rapidly to ensure all layouts include/exclude particular fields with the correct access (read-only) and placement.  Sure, I could have used a single tab and used the Salesforce drop-down to select the next layout and repeated 10x, but opening them all up quickly and then cycling through is much faster.

Multiple Tabs – Profile – Object Permissions

While editing profile permissions, I’ve opened multiple a tab of each profile in question and quickly toggled between them to compare and contrast.  Sure, I do know that the enhanced profile interface would let me add a new view with columns for each CRED on specific objects and view those for whichever profiles I want.  But this is honestly a pain.  I can only have 10 columns and each CRED attribute (Create, Read, Edit, Delete, View All, Modify All, etc.) take up a column.  I can maybe get two objects within a view and that takes time to build if you are leveraging many standard and custom objects including managed packages.  Not to mention you can’t add page layout or record type assignments to these views.

Below is a quick example of this.  I was not able to use my keyboard shortcuts while creating the GIF, so I just cycled through with the mouse.  The key here is to learn the keyboard shortcut so you can toggle through much faster, and focus all of your attention on the text.  Any differences will quickly jump out at you!  Once you are done cycling through the current objects on the page, hit space bar on each tab as you cycle through.  It will scroll the window down by the same amount on each tab so that they all line up perfectly for you to continue your review.  In this case the only difference is the Tab for Leads is “Default Off” for the Support Profile.

Use Multiple Tabs to Check Profile Permissions

Multiple Tabs – CRED – Field Security – Page Layouts & Record Types

From here I can open up multiple tabs for various objects to edit/review or open the same object but for various profiles (example below).  Here I can quickly see CRED permissions, field security, and page layout & record type assignments and make the necessary changes.

Use Multiple Tabs to Check Assigned Page Layouts Record Types And Object Perms

Multiple Tabs – Available Picklist Values for Record Types

This also comes in handy when quickly reviewing the available picklist values for different record types.

Use Multiple Tabs to Check Record Type Available Picklist Values

Admittedly, these examples are simple and I wasn’t able to cycle through as fast as you can with the keyboard.  It may seem that you could just as easily accomplish a simple task with a single browser tab/window.  But what if you had 10 different picklist fields to compare for 10 different record types?  Using multiple tabs can help you quickly and efficiently review settings across the platform and make changes if necessary.

How do you leverage multiple tabs in your Salesforce Administration?  Would love to hear your comments!