├── JS Events.gif ├── JS_Query.png ├── JS_Post_filters.png ├── JS_iframe_height.png ├── Modules ├── Javascript │ ├── capture_events.js │ └── client_to_server.js ├── Embed Parameters │ ├── look.rb │ ├── explore.rb │ ├── query.rb │ ├── lookML_dashboard.rb │ ├── create_query.rb │ └── user_defined_dashboard.rb ├── API Calls │ ├── get_look_data.rb │ ├── get_sql_for_query.rb │ ├── get_query_slug.rb │ ├── create_look.md │ ├── data_dictionary.rb │ ├── list_of_looks_created_by_user.md │ ├── get_data_from_look_or_query.md │ ├── list_of_looks_in_a_space.md │ └── get_looks_in_user_space.rb └── Embed Authentication │ └── auth.rb ├── LICENSE ├── Use Cases ├── Data Dictionary.md ├── Custom Pages.md ├── Embed a Explore Page.md ├── Javascript Events.md ├── Report Selector.md ├── Embed a Dashboard.md └── Field Picker.md ├── actions-for-sfdc.md └── actions-for-github.md /JS Events.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llooker/powered_by_modules/HEAD/JS Events.gif -------------------------------------------------------------------------------- /JS_Query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llooker/powered_by_modules/HEAD/JS_Query.png -------------------------------------------------------------------------------- /JS_Post_filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llooker/powered_by_modules/HEAD/JS_Post_filters.png -------------------------------------------------------------------------------- /JS_iframe_height.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llooker/powered_by_modules/HEAD/JS_iframe_height.png -------------------------------------------------------------------------------- /Modules/Javascript/capture_events.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("message", function (event) { 2 | if (event.source === document.getElementById("looker_iframe").contentWindow){ 3 | MyApp.blob = JSON.parse(event.data); 4 | console.log(MyApp.blob); 5 | } 6 | }); -------------------------------------------------------------------------------- /Modules/Embed Parameters/look.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | class DashboardController < ApplicationController 4 | 5 | def look 6 | # Pass in the Look ID. ID can be found by hovering over a look in the Looker Instance 7 | @options = { 8 | embed_url: "/embed/looks/1023" 9 | } 10 | @embed_url = Auth::embed_url(@options) 11 | end 12 | 13 | 14 | end -------------------------------------------------------------------------------- /Modules/Embed Parameters/explore.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | class DashboardController < ApplicationController 4 | ## Include information on how to adjust this for clients with Access Filter Fields 5 | def explore 6 | @options = { 7 | embed_url: "/embed/explore/powered_by/order_items_simple", 8 | height: '750', 9 | } 10 | @embed_url = Auth::embed_url(@options) 11 | end 12 | end 13 | 14 | 15 | -------------------------------------------------------------------------------- /Modules/Embed Parameters/query.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | class DashboardController < ApplicationController 4 | ## Include information on how to adjust this for clients with Access Filter Fields 5 | def query 6 | # Pull up a full query by passing in the Query Slug. 7 | # The query slug can be found in the URL or through the API 8 | @options = { 9 | embed_url: "/embed/query/powered_by/order_items?query=5Yf4DK4" 10 | } 11 | @embed_url = Auth::embed_url(@options) 12 | end 13 | end 14 | 15 | 16 | -------------------------------------------------------------------------------- /Modules/Embed Parameters/lookML_dashboard.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | class DashboardController < ApplicationController 4 | ## Include information on how to adjust this for clients with Access Filter Fields 5 | def lookML_dashboard 6 | # Pass in the name of the lookML Dashboard 7 | @options = { 8 | embed_url: "/embed/dashboards/powered_by/supplier_dashboard" 9 | } 10 | 11 | @embed_url = Auth::embed_url(@options) 12 | # @embed_url = @embed_url + '&embed_domain=http://localhost:3000' 13 | end 14 | end 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Modules/API Calls/get_look_data.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | module ApplicationHelper 4 | 5 | def self.api_auth 6 | sdk = LookerSDK::Client.new( 7 | # Looker/API Credentials 8 | :client_id => ENV['API_CLIENT_ID'], 9 | :client_secret => ENV['API_SECRET'], 10 | :api_endpoint => ENV['API_ENDPOINT'], 11 | :connection_options => {:ssl => {:verify => false}} 12 | ) 13 | return sdk 14 | end 15 | 16 | def self.get_look_data(look_id) 17 | sdk = api_auth() 18 | return sdk.run_look(look_id, "jpg") 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /Modules/API Calls/get_sql_for_query.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | module ApplicationHelper 4 | 5 | def self.api_auth 6 | sdk = LookerSDK::Client.new( 7 | # Looker/API Credentials 8 | :client_id => ENV['API_CLIENT_ID'], 9 | :client_secret => ENV['API_SECRET'], 10 | :api_endpoint => ENV['API_ENDPOINT'], 11 | :connection_options => {:ssl => {:verify => false}} 12 | ) 13 | return sdk 14 | end 15 | 16 | def self.get_sql_for_query(query_slug) 17 | sdk = self.api_auth() 18 | 19 | query_info = sdk.query_for_slug(query_slug) 20 | query_id = query_info[:id] 21 | 22 | return sdk.run_query(query_id, "sql") 23 | end 24 | 25 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Looker Data Sciences, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Modules/Javascript/client_to_server.js: -------------------------------------------------------------------------------- 1 | var MyApp = {}; 2 | 3 | window.addEventListener("message", function (event) { 4 | if (event.source === document.getElementById("looker_iframe").contentWindow) 5 | { 6 | MyApp.blob = JSON.parse(event.data); 7 | console.log(MyApp.blob); 8 | } 9 | }); 10 | 11 | 12 | // callback handler for form submit 13 | $(function(){ 14 | $("#create_look_explore").submit(function(event){ 15 | event.preventDefault(); 16 | 17 | // var action = $(this).attr('action'); 18 | // var method = $(this).attr('method'); 19 | 20 | MyApp.event_URL = MyApp.blob.explore.url; 21 | 22 | console.log(MyApp.event_URL); 23 | var title = $(this).find('#title_of_look').val(); 24 | 25 | $.ajax({ 26 | method: "post", 27 | url: "/create_look", 28 | data: { title: title, event_URL: MyApp.event_URL }, 29 | dataType: 'json', 30 | 31 | success: function(data,status,xhr){ 32 | console.log(data.message); 33 | alert(data.message); 34 | }, 35 | error: function(xhr,status,error){ 36 | console.log(xhr); 37 | alert('Enter a Valid Query and a Unique Title \n' + error); 38 | } 39 | }); 40 | 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /Modules/Embed Parameters/create_query.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | class DashboardController < ApplicationController 4 | ## Include information on how to adjust this for clients with Access Filter Fields 5 | 6 | def create_query 7 | 8 | @fields = params[:fields] 9 | @fields = @fields << "order_items.created_month" 10 | @gender = params[:gender] 11 | 12 | query = { 13 | :model=>"powered_by", 14 | :view=>"order_items", 15 | #:fields=>["order_items.created_month", "users.count","inventory_items.total_cost"], 16 | :fields => @fields, 17 | :filters=>{:"products.brand"=> "Allegra K", :"order_items.created_month"=> "after 2015/01/01", :"users.gender"=>@gender}, 18 | :sorts=>["inventory_items.created_month desc"], 19 | :limit=>"100", 20 | :query_timezone=>"America/Los_Angeles" 21 | } 22 | 23 | query_slug = ApplicationHelper.get_query_slug(query) 24 | 25 | @options = { 26 | ##Using the Query Slug --> You can get the Query Slug by grabbing a URL 27 | embed_url: "/embed/query/powered_by/order_items?query=#{query_slug}" 28 | } 29 | 30 | @embed_url = Auth::embed_url(@options) 31 | 32 | end 33 | end -------------------------------------------------------------------------------- /Modules/API Calls/get_query_slug.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'looker-sdk' 3 | 4 | module ApplicationHelper 5 | 6 | def self.api_auth 7 | sdk = LookerSDK::Client.new( 8 | # Looker/API Credentials 9 | :client_id => ENV['API_CLIENT_ID'], 10 | :client_secret => ENV['API_SECRET'], 11 | :api_endpoint => ENV['API_ENDPOINT'], 12 | :connection_options => {:ssl => {:verify => false}} 13 | ) 14 | return sdk 15 | end 16 | 17 | def self.get_query_slug(query) 18 | sdk = api_auth() 19 | 20 | # Constructing a Query: 21 | # Pass in the necessary model name and field names in order to construct the query. 22 | # If Query slug will be used to display an Iframe - Authentication will automatically incorporate in the appropriate access filter fields. 23 | 24 | # query = { 25 | # :model=>"powered_by", 26 | # :view=>"order_items", 27 | # :fields=> 28 | # ["order_items.id", "orders.created_date", "products.item_name", "products.category", "order_items.sale_price"], 29 | # :filters=>{:"products.brand"=> "Allegra K"}, 30 | # :sorts=>["orders.created_date desc 0"], 31 | # :limit=>"10", 32 | # :query_timezone=>"America/Los_Angeles" 33 | # } 34 | 35 | query_detail = sdk.create_query(query) 36 | query_id = query_detail[:id] 37 | query_slug = query_detail[:slug] 38 | 39 | return query_slug 40 | end 41 | 42 | end 43 | 44 | 45 | -------------------------------------------------------------------------------- /Modules/Embed Parameters/user_defined_dashboard.rb: -------------------------------------------------------------------------------- 1 | 2 | ## Controller 3 | class DashboardController < ApplicationController 4 | def user_defined_dashboard 5 | # Pass in the Dashboard ID for a User Defined Dashboard 6 | @options = { 7 | embed_url: "/embed/dashboards/234", 8 | height: '2247', 9 | } 10 | 11 | @embed_url = Auth::embed_url(@options) 12 | 13 | # embed_url calls the Authentication Class that generates the full URL with all the parameters that looker requires to authenticate 14 | # @embed_url = @embed_url + '&embed_domain=http://localhost:3000' 15 | end 16 | 17 | def user_defined_dashboard_with_filters 18 | # Pass in the Dashboard ID and Filter Values for a User Defined Dashboard 19 | # Filters can be customized and defined outside of the Iframe. On Filter submit, pass in filter values to the IFrame to load the new dashboard 20 | state = params[:state] 21 | date_range = params[:date_filter] 22 | 23 | @options = { 24 | embed_url: "/embed/dashboards/234?Date=#{date_range}&State=#{state}", 25 | height: '2247', 26 | } 27 | 28 | @embed_url = Auth::embed_url(@options) 29 | 30 | # embed_url calls the Authentication Class that generates the full URL with all the parameters that looker requires to authenticate 31 | # @embed_url = @embed_url + '&embed_domain=http://localhost:3000' 32 | end 33 | 34 | 35 | 36 | end 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Modules/API Calls/create_look.md: -------------------------------------------------------------------------------- 1 | **Input Parameters**: 2 | * `user_id` for permissioning the user 3 | * `query_id` for query selection 4 | * `title` for the title of the Look (i.e. the saved query) 5 | * `space` for the directory or Space where the Look will be saved 6 | 7 | **Resulting Action**: A Look is saved in the Looker Instance based on the specified parameters. 8 | 9 | 10 | ``` 11 | require 'looker-sdk' 12 | 13 | module ApplicationHelper 14 | 15 | def self.api_auth 16 | sdk = LookerSDK::Client.new( 17 | # Looker/API Credentials 18 | :client_id => ENV['API_CLIENT_ID'], 19 | :client_secret => ENV['API_SECRET'], 20 | :api_endpoint => ENV['API_ENDPOINT'], 21 | :connection_options => {:ssl => {:verify => false}} 22 | ) 23 | return sdk 24 | end 25 | 26 | def self.create_look(query_slug, title) 27 | sdk = self.api_auth() 28 | 29 | query_info = sdk.query_for_slug(query_slug) 30 | query_id = query_info[:id] 31 | 32 | #The Required parameters to create a Look are listed below. Additional parameters, such as the User ID, description, etc... can also be specified. 33 | body = { 34 | :user_id => current_user, 35 | :query_id => query_id, 36 | :title => title, 37 | :description => "My Description", 38 | :space_id => 327, 39 | } 40 | return sdk.create_look(body) 41 | end 42 | 43 | end 44 | ``` 45 | 46 | Please reach out to a Looker Analyst for any questions and / or assistance implementing. 47 | -------------------------------------------------------------------------------- /Modules/API Calls/data_dictionary.rb: -------------------------------------------------------------------------------- 1 | # Get Data Dictionary 2 | 3 | require 'looker-sdk' 4 | 5 | module ApplicationHelper 6 | 7 | def self.api_auth 8 | sdk = LookerSDK::Client.new( 9 | # Looker/API Credentials 10 | :client_id => ENV['API_CLIENT_ID'], 11 | :client_secret => ENV['API_SECRET'], 12 | :api_endpoint => ENV['API_ENDPOINT'], 13 | :connection_options => {:ssl => {:verify => false}} 14 | ) 15 | return sdk 16 | end 17 | 18 | def self.get_field_values(model_name, explore_name) 19 | 20 | sdk = self.api_auth() 21 | fields = {:fields => 'id, name, description, fields'} 22 | 23 | #API Call to pull in metadata about fields in a particular explore 24 | fields = sdk.lookml_model_explore(model_name, explore_name, fields) 25 | 26 | 27 | my_fields = [] 28 | 29 | #Iterate through the field definitions and pull in the description, sql, and other looker tags you might want to include in your data dictionary. 30 | fields[:fields][:dimensions].to_a.each do |x| 31 | dimension = { 32 | :field_type => 'Dimension', 33 | :view_name => x[:view_label].to_s, 34 | :field_name => x[:label_short].to_s, 35 | :type => x[:type].to_s, 36 | :description => x[:description].to_s, 37 | :sql => x[:sql].to_s 38 | } 39 | my_fields << dimension 40 | end 41 | 42 | fields[:fields][:measures].to_a.each do |x| 43 | measure = { 44 | :field_type => 'Measure', 45 | :view_name => x[:view_label].to_s, 46 | :field_name => x[:label_short].to_s, 47 | :type => x[:type].to_s, 48 | :description => x[:description].to_s, 49 | :sql => x[:sql].to_s 50 | } 51 | 52 | my_fields << measure 53 | end 54 | 55 | return my_fields 56 | end 57 | 58 | end 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Use Cases/Data Dictionary.md: -------------------------------------------------------------------------------- 1 | 2 | # Creating a Data Dictionary 3 | 4 | ### Step 1: Build out LookML Model 5 | Within your LookML Model, define Fields with descriptions, labels, and other parameters that you might to pull into your data dictionary. 6 | 7 | ### Step 2: Use API call to pull in LookML Metadata 8 | Connect to our API and call the following Method (lookML_model_explore) to pull in the appropriate fields that you would want to identify within your Data Dictionary. 9 | 10 | Sample Ruby code can be found below. 11 | 12 | ``` 13 | # Get Data Dictionary 14 | 15 | require 'looker-sdk' 16 | 17 | module ApplicationHelper 18 | 19 | def self.api_auth 20 | sdk = LookerSDK::Client.new( 21 | # Looker/API Credentials 22 | :client_id => ENV['API_CLIENT_ID'], 23 | :client_secret => ENV['API_SECRET'], 24 | :api_endpoint => ENV['API_ENDPOINT'], 25 | :connection_options => {:ssl => {:verify => false}} 26 | ) 27 | return sdk 28 | end 29 | 30 | def self.get_field_values(model_name, explore_name) 31 | 32 | sdk = self.api_auth() 33 | fields = {:fields => 'id, name, description, fields'} 34 | 35 | #API Call to pull in metadata about fields in a particular explore 36 | fields = sdk.lookml_model_explore(model_name, explore_name, fields) 37 | 38 | 39 | my_fields = [] 40 | 41 | #Iterate through the field definitions and pull in the description, sql, and other looker tags you might want to include in your data dictionary. 42 | fields[:fields][:dimensions].to_a.each do |x| 43 | dimension = { 44 | :field_type => 'Dimension', 45 | :view_name => x[:view_label].to_s, 46 | :field_name => x[:label_short].to_s, 47 | :type => x[:type].to_s, 48 | :description => x[:description].to_s, 49 | :sql => x[:sql].to_s 50 | } 51 | my_fields << dimension 52 | end 53 | 54 | fields[:fields][:measures].to_a.each do |x| 55 | measure = { 56 | :field_type => 'Measure', 57 | :view_name => x[:view_label].to_s, 58 | :field_name => x[:label_short].to_s, 59 | :type => x[:type].to_s, 60 | :description => x[:description].to_s, 61 | :sql => x[:sql].to_s 62 | } 63 | 64 | my_fields << measure 65 | end 66 | 67 | return my_fields 68 | end 69 | 70 | end 71 | ``` 72 | 73 | ### Step 3: 74 | 75 | Format the results from the lookML_model_explore call using custom CSS and HTML. Consider using JS Plugins to allow for Search and Sort functionality to make the dictionary more accessible. In this Block, we use the [DataTable JS Plugin](https://datatables.net/). 76 | -------------------------------------------------------------------------------- /Modules/API Calls/list_of_looks_created_by_user.md: -------------------------------------------------------------------------------- 1 | **Input Parameters**: 2 | * `user_id` specifies the user whose Looks will be retrieved 3 | 4 | 5 | **Resulting Action**: Returns an array of Looks created by a User with additional Metadata about each Look. 6 | 7 | 8 | 9 | ``` 10 | require 'looker-sdk' 11 | 12 | module ApplicationHelper 13 | 14 | def self.api_auth 15 | sdk = LookerSDK::Client.new( 16 | # Looker/API Credentials 17 | :client_id => ENV['API_CLIENT_ID'], 18 | :client_secret => ENV['API_SECRET'], 19 | :api_endpoint => ENV['API_ENDPOINT'], 20 | :connection_options => {:ssl => {:verify => false}} 21 | ) 22 | return sdk 23 | end 24 | 25 | def self.get_looks_by_user(user_id) 26 | sdk = self.api_auth() 27 | 28 | all_looks = sdk.all_looks(:fields => 'id, user').to_a 29 | 30 | looks = [] 31 | all_looks.each do |x| 32 | if((x[:user][:id]) == user_id) 33 | looks << self.get_look_details(x[:id].to_s) 34 | end 35 | end 36 | 37 | return looks 38 | end 39 | 40 | def self.get_look_details(look_id) 41 | 42 | sdk = api_auth() 43 | look = sdk.look(look_id) 44 | 45 | puts "\n" + "Look Information - " + "\n" 46 | puts "ID: " + look[:id].to_s + "\n" 47 | puts "Title: " + look[:title].to_s + "\n" 48 | puts "Description: " + look[:description].to_s + "\n" 49 | 50 | 51 | #To get the Full Query URL for a look, make a call to the Look API method based on the Look ID 52 | look_url = look['url'].split('?', 2).last 53 | query_url ="/embed/query/powered_by/order_items?#{look_url}" 54 | 55 | #To get user information, make a call to the USER API method based on the user ID of the Look 56 | user = sdk.user(look[:user][:id]) 57 | created_user_name = user[:first_name] + " " + user[:last_name] 58 | puts "Created User: " + created_user_name + "\n" 59 | 60 | #Return all valid information about the Look 61 | return Look.new(look[:id].to_s, look[:title].to_s, look[:description].to_s, query_url, created_user_name) 62 | end 63 | 64 | end 65 | 66 | 67 | 68 | class Look 69 | attr_accessor :look_id, :title, :description, :query_url, :created_user_name 70 | 71 | def initialize(look_id, title, description, query_url, created_user_name) 72 | @look_id = look_id 73 | @title = title 74 | @description = description 75 | @query_url = query_url 76 | @created_user_name = created_user_name 77 | end 78 | end 79 | ``` 80 | 81 | Please reach out to a Looker Analyst for any questions and / or assistance implementing. 82 | -------------------------------------------------------------------------------- /Modules/API Calls/get_data_from_look_or_query.md: -------------------------------------------------------------------------------- 1 | ***Get Data from a Query (for new queries)*** 2 | 3 | **Input Parameters**: 4 | * all query variables should be included as input parameters. See inline comments in the code snippet below for an example 5 | 6 | **Resulting Action**: A query is executed against the database based on the input parameters, and the results of that query are returned. 7 | 8 | ``` 9 | module ApplicationHelper 10 | 11 | def self.api_auth 12 | sdk = LookerSDK::Client.new( 13 | # Looker/API Credentials 14 | :client_id => ENV['API_CLIENT_ID'], 15 | :client_secret => ENV['API_SECRET'], 16 | :api_endpoint => ENV['API_ENDPOINT'], 17 | :connection_options => {:ssl => {:verify => false}} 18 | ) 19 | return sdk 20 | end 21 | 22 | 23 | def self.get_query_data(query) 24 | 25 | # Constructing a Query: 26 | # Pass in the necessary model name and field names in order to construct the query. 27 | # If results from the Query will be displayed in a custom format (non-iframe), Access Filter Field values will need to be added to the Query that is executed. (IF YOU ARE USING ACCESS FILTER FIELDS, you will need to specify the current user's session in the filtered params) 28 | 29 | sdk = api_auth() 30 | 31 | @query = { 32 | :model=>"powered_by", 33 | :view=>"order_items", 34 | :fields=> 35 | ["order_items.id", "orders.created_date", "products.item_name", "products.category", "order_items.sale_price"], 36 | :filters=>{:"products.brand"=> "Allegra K"}, 37 | :sorts=>["orders.created_date desc 0"], 38 | :limit=>"10", 39 | :query_timezone=>"America/Los_Angeles" 40 | } 41 | return sdk.run_inline_query("json", query) 42 | 43 | end 44 | 45 | end 46 | ``` 47 | 48 | ***Get Data from a Look (for queries that have already been saved)*** 49 | 50 | **Input Parameters**: 51 | * `look_id` specifies the Look that results are retrieved from after the query is executed 52 | * `result_format` specifies the format in which the results are returned. See API documentation for the full list of format types. 53 | 54 | **Resulting Action**: A Look query is executed against the database, and the results of that query are returned. 55 | 56 | ``` 57 | require 'looker-sdk' 58 | 59 | module ApplicationHelper 60 | 61 | def self.api_auth 62 | sdk = LookerSDK::Client.new( 63 | # Looker/API Credentials 64 | :client_id => ENV['API_CLIENT_ID'], 65 | :client_secret => ENV['API_SECRET'], 66 | :api_endpoint => ENV['API_ENDPOINT'], 67 | :connection_options => {:ssl => {:verify => false}} 68 | ) 69 | return sdk 70 | end 71 | 72 | def self.get_look_data(look_id) 73 | sdk = api_auth() 74 | return sdk.run_look(look_id, "jpg") 75 | end 76 | 77 | end 78 | ``` 79 | 80 | Please reach out to a Looker Analyst for any questions and / or assistance implementing. 81 | -------------------------------------------------------------------------------- /Modules/API Calls/list_of_looks_in_a_space.md: -------------------------------------------------------------------------------- 1 | **Input Parameters**: 2 | * `space_id` to specify the Space (user or shared) where the list of Looks will be retrieved 3 | 4 | 5 | **Resulting Action**: Returns an array of Looks created in a Space with additional Metadata about each Look. 6 | 7 | 8 | 9 | ``` 10 | require 'looker-sdk' 11 | 12 | module ApplicationHelper 13 | 14 | def self.api_auth 15 | sdk = LookerSDK::Client.new( 16 | # Looker/API Credentials 17 | :client_id => ENV['API_CLIENT_ID'], 18 | :client_secret => ENV['API_SECRET'], 19 | :api_endpoint => ENV['API_ENDPOINT'], 20 | :connection_options => {:ssl => {:verify => false}} 21 | ) 22 | return sdk 23 | end 24 | 25 | def self.get_looks_in_space(space_id) 26 | sdk = self.api_auth() 27 | 28 | # Specify Additional Searches/Filters within the Space Call (i.e. Only return the looks associated with a space) 29 | fields ={:fields=> 'looks'} 30 | looks = sdk.space(space_id, fields) 31 | 32 | # Convert String to Array in order to manipulate String values 33 | looks_in_space = looks[:looks].to_a 34 | 35 | looks = [] 36 | 37 | # For every Look in the Space grab certain details about the Look -- 38 | looks_in_space.each do |x| 39 | looks << get_look_details(x[:id]) 40 | end 41 | 42 | return looks 43 | end 44 | 45 | def self.get_look_details(look_id) 46 | 47 | sdk = api_auth() 48 | look = sdk.look(look_id) 49 | 50 | puts "\n" + "Look Information - " + "\n" 51 | puts "ID: " + look[:id].to_s + "\n" 52 | puts "Title: " + look[:title].to_s + "\n" 53 | puts "Description: " + look[:description].to_s + "\n" 54 | 55 | 56 | #To get the Full Query URL for a look, make a call to the Look API method based on the Look ID 57 | look_url = look['url'].split('?', 2).last 58 | query_url ="/embed/query/powered_by/order_items?#{look_url}" 59 | 60 | #To get user information, make a call to the USER API method based on the user ID of the Look 61 | user = sdk.user(look[:user][:id]) 62 | created_user_name = user[:first_name] + " " + user[:last_name] 63 | puts "Created User: " + created_user_name + "\n" 64 | 65 | #Return all valid information about the Look 66 | return Look.new(look[:id].to_s, look[:title].to_s, look[:description].to_s, query_url, created_user_name) 67 | end 68 | 69 | end 70 | 71 | 72 | 73 | 74 | class Look 75 | attr_accessor :look_id, :title, :description, :query_url, :created_user_name 76 | 77 | def initialize(look_id, title, description, query_url, created_user_name) 78 | @look_id = look_id 79 | @title = title 80 | @description = description 81 | @query_url = query_url 82 | @created_user_name = created_user_name 83 | end 84 | end 85 | ``` 86 | 87 | Please reach out to a Looker Analyst for any questions and / or assistance implementing. 88 | -------------------------------------------------------------------------------- /Modules/API Calls/get_looks_in_user_space.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | module ApplicationHelper 4 | 5 | def self.api_auth 6 | sdk = LookerSDK::Client.new( 7 | # Looker/API Credentials 8 | :client_id => ENV['API_CLIENT_ID'], 9 | :client_secret => ENV['API_SECRET'], 10 | :api_endpoint => ENV['API_ENDPOINT'], 11 | :connection_options => {:ssl => {:verify => false}} 12 | ) 13 | return sdk 14 | end 15 | 16 | ## Main Method 17 | def self.get_looks_in_user_space(user_id) 18 | user_space_id = get_user_space_id(user_id) 19 | looks = get_looks_in_space(user_space_id) 20 | return looks 21 | end 22 | 23 | # Based on a User ID, Return a User's Personal Space 24 | def self.get_user_space_id(user_id) 25 | sdk = self.api_auth() 26 | all_spaces = sdk.all_spaces(:fields=> 'id, creator_id, is_personal') 27 | 28 | user_space_id = 0 29 | 30 | #Iterate through all spaces and determine the User's personal space 31 | all_spaces.each do |x| 32 | user_space_id = x[:id] if (x[:creator_id] == user_id && x[:is_personal] = true) 33 | end 34 | 35 | return user_space_id 36 | end 37 | 38 | def self.get_looks_in_space(space_id) 39 | sdk = self.api_auth() 40 | 41 | # Specify Additional Searches/Filters within the Space Call (i.e. Only return the looks associated with a space) 42 | fields ={:fields=> 'looks'} 43 | looks = sdk.space(space_id, fields) 44 | 45 | # Convert String to Array in order to manipulate String values 46 | looks_in_space = looks[:looks].to_a 47 | 48 | looks = [] 49 | 50 | # For every Look in the Space grab certain details about the Look -- 51 | looks_in_space.each do |x| 52 | looks << get_look_details(x[:id]) 53 | end 54 | 55 | return looks 56 | end 57 | 58 | 59 | def self.get_look_details(look_id) 60 | 61 | sdk = api_auth() 62 | look = sdk.look(look_id) 63 | 64 | puts "\n" + "Look Information - " + "\n" 65 | puts "ID: " + look[:id].to_s + "\n" 66 | puts "Title: " + look[:title].to_s + "\n" 67 | puts "Description: " + look[:description].to_s + "\n" 68 | 69 | 70 | #To get the Full Query URL for a look, make a call to the Look API method based on the Look ID 71 | look_url = look['url'].split('?', 2).last 72 | query_url ="/embed/query/powered_by/order_items?#{look_url}" 73 | 74 | #To get user information, make a call to the USER API method based on the user ID of the Look 75 | user = sdk.user(look[:user][:id]) 76 | created_user_name = user[:first_name] + " " + user[:last_name] 77 | puts "Created User: " + created_user_name + "\n" 78 | 79 | #Return all valid information about the Look 80 | return Look.new(look[:id].to_s, look[:title].to_s, look[:description].to_s, query_url, created_user_name) 81 | end 82 | 83 | end 84 | 85 | 86 | 87 | 88 | class Look 89 | attr_accessor :look_id, :title, :description, :query_url, :created_user_name 90 | 91 | def initialize(look_id, title, description, query_url, created_user_name) 92 | @look_id = look_id 93 | @title = title 94 | @description = description 95 | @query_url = query_url 96 | @created_user_name = created_user_name 97 | end 98 | end -------------------------------------------------------------------------------- /Use Cases/Custom Pages.md: -------------------------------------------------------------------------------- 1 | 2 | # Creating a Custom Page w/ Looker Data 3 | 4 | ### Step 1: Create a Query or Look 5 | Within your Looker instance grab the QueryID, LookID, or Query_Slug. 6 | 7 | ### Step 2: Use API call to get data 8 | Connect to our API and call one of the following methods to pull in the appropriate data points that you would want to identify within your Data Dictionary. 9 | 10 | 11 | ***Construct and get data from a Query*** 12 | 13 | **Input Parameters**: 14 | * all query variables should be included as input parameters. See inline comments in the code snippet below for an example 15 | 16 | **Resulting Action**: A query is executed against the database based on the input parameters, and the results of that query are returned. 17 | 18 | ``` 19 | module ApplicationHelper 20 | 21 | def self.api_auth 22 | sdk = LookerSDK::Client.new( 23 | # Looker/API Credentials 24 | :client_id => ENV['API_CLIENT_ID'], 25 | :client_secret => ENV['API_SECRET'], 26 | :api_endpoint => ENV['API_ENDPOINT'], 27 | :connection_options => {:ssl => {:verify => false}} 28 | ) 29 | return sdk 30 | end 31 | 32 | 33 | def self.get_query_data(query) 34 | 35 | # Constructing a Query: 36 | # Pass in the necessary model name and field names in order to construct the query. 37 | # If results from the Query will be displayed in a custom format (non-iframe), Access Filter Field values will need to be added to the Query that is executed. (IF YOU ARE USING ACCESS FILTER FIELDS, you will need to specify the current user's session in the filtered params) 38 | 39 | sdk = api_auth() 40 | 41 | @query = { 42 | :model=>"powered_by", 43 | :view=>"order_items", 44 | :fields=> 45 | ["order_items.id", "orders.created_date", "products.item_name", "products.category", "order_items.sale_price"], 46 | :filters=>{:"products.brand"=> "Allegra K"}, 47 | :sorts=>["orders.created_date desc 0"], 48 | :limit=>"10", 49 | :query_timezone=>"America/Los_Angeles" 50 | } 51 | return sdk.run_inline_query("json", query) 52 | 53 | end 54 | 55 | end 56 | ``` 57 | 58 | ***Get Data from a Look (for queries that have already been saved)*** 59 | 60 | **Input Parameters**: 61 | * `look_id` specifies the Look that results are retrieved from after the query is executed 62 | * `result_format` specifies the format in which the results are returned. See API documentation for the full list of format types. 63 | 64 | **Resulting Action**: A Look query is executed against the database, and the results of that query are returned. 65 | 66 | ``` 67 | require 'looker-sdk' 68 | 69 | module ApplicationHelper 70 | 71 | def self.api_auth 72 | sdk = LookerSDK::Client.new( 73 | # Looker/API Credentials 74 | :client_id => ENV['API_CLIENT_ID'], 75 | :client_secret => ENV['API_SECRET'], 76 | :api_endpoint => ENV['API_ENDPOINT'], 77 | :connection_options => {:ssl => {:verify => false}} 78 | ) 79 | return sdk 80 | end 81 | 82 | def self.get_look_data(look_id) 83 | sdk = api_auth() 84 | return sdk.run_look(look_id, "jpg") 85 | end 86 | 87 | end 88 | ``` 89 | 90 | ### Step 3: 91 | 92 | Format the results from the your API call using custom CSS and HTML. 93 | 94 | 95 | 96 | ## Extend this example by creating a Custom Visualization 97 | 98 | Pass in the results of your API call into Visualization frameworks like D3 to create custom charts and graphs. 99 | 100 | 101 | [Visualization Frameworks](https://github.com/llooker/powered_by_modules/blob/master/Use%20Cases/Embed%20a%20Explore%20Page.md "Metrics Selector") 102 | -------------------------------------------------------------------------------- /Modules/Embed Authentication/auth.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | require 'securerandom' 3 | require 'uri' 4 | require 'base64' 5 | require 'json' 6 | require 'openssl' 7 | 8 | 9 | class Auth < ActiveRecord::Base 10 | 11 | def self.embed_url(additional_data = {}) 12 | 13 | url_data = { 14 | # System Level Permissions 15 | host: ENV['LOOKER_HOST'], # ex: [mylooker_instance.looker.com] (include port # if self hosted) 16 | secret: ENV['EMBED_SECRET'], # Secret Key 17 | session_length: 30.minutes, 18 | force_logout_login: true, 19 | 20 | # User Specific Permissions 21 | external_user_id: 102, #The External ID must be a Number and must be unique for every embedded User (Primary Key). 22 | first_name: "Dr", 23 | last_name: "Strange", 24 | permissions: ['see_user_dashboards', 'see_lookml_dashboards', 'access_data', 'see_looks', 'download_with_limit', 'explore'], 25 | models: ['powered_by'], 26 | access_filters: {:powered_by => {:'products.brand' => "Allegra K"}}, 27 | }.merge(additional_data) 28 | 29 | url = Auth::created_signed_embed_url(url_data) 30 | 31 | "https://#{url}" 32 | 33 | end 34 | 35 | 36 | def self.created_signed_embed_url(options) 37 | # looker options 38 | secret = options[:secret] 39 | host = options[:host] 40 | 41 | # user options 42 | json_external_user_id = options[:external_user_id].to_json 43 | json_first_name = options[:first_name].to_json 44 | json_last_name = options[:last_name].to_json 45 | json_permissions = options[:permissions].to_json 46 | json_models = options[:models].to_json 47 | json_access_filters = options[:access_filters].to_json 48 | 49 | # url/session specific options 50 | embed_path = '/login/embed/' + CGI.escape(options[:embed_url]) 51 | json_session_length = options[:session_length].to_json 52 | json_force_logout_login = options[:force_logout_login].to_json 53 | 54 | # computed options 55 | json_time = Time.now.to_i.to_json 56 | json_nonce = SecureRandom.hex(16).to_json 57 | 58 | # compute signature 59 | string_to_sign = "" 60 | string_to_sign += host + "\n" 61 | string_to_sign += embed_path + "\n" 62 | string_to_sign += json_nonce + "\n" 63 | string_to_sign += json_time + "\n" 64 | string_to_sign += json_session_length + "\n" 65 | string_to_sign += json_external_user_id + "\n" 66 | string_to_sign += json_permissions + "\n" 67 | string_to_sign += json_models + "\n" 68 | string_to_sign += json_access_filters 69 | 70 | signature = Base64.encode64( 71 | OpenSSL::HMAC.digest( 72 | OpenSSL::Digest::Digest.new('sha1'), 73 | secret, 74 | string_to_sign.force_encoding("utf-8"))).strip 75 | 76 | # construct query string 77 | query_params = { 78 | nonce: json_nonce, 79 | time: json_time, 80 | session_length: json_session_length, 81 | external_user_id: json_external_user_id, 82 | permissions: json_permissions, 83 | models: json_models, 84 | access_filters: json_access_filters, 85 | first_name: json_first_name, 86 | last_name: json_last_name, 87 | force_logout_login: json_force_logout_login, 88 | signature: signature 89 | } 90 | query_string = query_params.to_a.map { |key, val| "#{key}=#{CGI.escape(val)}" }.join('&') 91 | 92 | "#{host}#{embed_path}?#{query_string}" 93 | end 94 | 95 | end -------------------------------------------------------------------------------- /Use Cases/Embed a Explore Page.md: -------------------------------------------------------------------------------- 1 | 2 | # Embedding: Embed a Looker Explore Page 3 | 4 | You can embed a looker explore page, much like you would embed a dashboard. 5 | 6 | To embed a looker explore page follow the [Embed a Dashboard](https://discourse.looker.com/t/javascript-embedded-iframe-events/2298 "Embedding") Use Case and add in the appropriate embed_url. 7 | 8 | **Default Explore Page -** embed_url: "/embed/explore//?qid=""
9 | **Explore Page with pre-built Query -** embed_url: "/embed/explore//?qid="" 10 | 11 | 12 | ### Enabling iFrame to Parent Page Communication 13 | Capture Events from an Embedded Explore Page by adding Javascript. 14 | 15 | Our Javascript API allows you to keep track of interactions with the iframe. [Javascript Events - Looker Documentation](https://discourse.looker.com/t/javascript-embedded-iframe-events/2298 "JS Events") 16 | 17 | ``` 18 | window.addEventListener("message", function (event) { 19 | if (event.source === document.getElementById("looker_iframe").contentWindow){ 20 | MyApp.blob = JSON.parse(event.data); 21 | console.log(MyApp.blob); 22 | } 23 | }); 24 | ``` 25 | 26 | A sample response might look like the following -- 27 | 28 | ``` 29 | Object {type: "explore:state:changed", explore: Object} 30 | explore:Object 31 | absoluteUrl:"https://demoembed.looker.com/embed/explore/powered_by/order_items? embed_domain=http:%2F%2F0.0.0.0:3000&qid=oFW25SaMKjTw4SWImC1sAH&toggle=vis" 32 | url:"/embed/explore/powered_by/order_items?embed_domain=http:%2F%2F0.0.0.0:3000&qid=oFW25SaMKjTw4SWImC1sAH&toggle=vis" 33 | ``` 34 | 35 | 36 | ### Add State to an Embedded Explore Page 37 | 38 | Once you capture JS events from an embedded explore page, you can allow for navigation between those actions by passing in the qid (query_id) from every event. By adding "history.pushState", your URL can record different events in the iframe and allow users to click the back button on your browser to toggle to the previous state of the iframe. 39 | 40 | ``` 41 | var MyApp = {}; 42 | window.addEventListener("message", function (event) { 43 | if (event.source === document.getElementById("looker_iframe").contentWindow) 44 | { 45 | MyApp.blob = JSON.parse(event.data); 46 | console.log(MyApp.blob); 47 | 48 | if (typeof MyApp.blob.explore != "undefined") { 49 | MyApp.event_URL = MyApp.blob.explore.url; 50 | const [start, qid] = MyApp.event_URL.split('qid='); 51 | history.pushState(qid, "explore_page", "?qid="+qid); 52 | } 53 | } 54 | }); 55 | ``` 56 | 57 | On every action within the iFrame, the URL will now get updated with the qid, allowing users to navigate between actions on the browser. 58 | 59 | 60 | ### Retrieve SQL from Explore iFrame 61 | 62 | With Javascript events enabled, you can capture the state of the iFrame and make additional requests to Looker through the API to get access to metadata and additional information. One such request could be to get the Looker generated SQL of queries run in the Explore. 63 | 64 | 65 | Step 1: Create a Submit button that users can click on to get input 66 | ``` 67 |
68 |
69 | 70 |
71 |
72 | ``` 73 | 74 | Step 2: 75 | Pass Client information (JS events/Form Fills) to the server via Javascript/Ajax Calls 76 | ``` 77 | var MyApp = {}; 78 | 79 | window.addEventListener("message", function (event) { 80 | if (event.source === document.getElementById("looker_iframe").contentWindow) 81 | { 82 | MyApp.blob = JSON.parse(event.data); 83 | console.log(MyApp.blob); 84 | } 85 | }); 86 | 87 | 88 | // callback handler for form submit 89 | $(function(){ 90 | $("#create_look_explore").submit(function(event){ 91 | event.preventDefault(); 92 | 93 | MyApp.event_URL = MyApp.blob.explore.url; 94 | 95 | console.log(MyApp.event_URL); 96 | var title = $(this).find('#title_of_look').val(); 97 | 98 | $.ajax({ 99 | method: "post", 100 | url: "/create_look", 101 | data: { title: title, event_URL: MyApp.event_URL }, 102 | dataType: 'json', 103 | 104 | success: function(data,status,xhr){ 105 | console.log(data.message); 106 | alert(data.message); 107 | }, 108 | error: function(xhr,status,error){ 109 | console.log(xhr); 110 | alert('Enter a Valid Query and a Unique Title \n' + error); 111 | } 112 | }); 113 | 114 | }); 115 | }); 116 | 117 | ``` 118 | 119 | 120 | Step 3: Parse Event URL from Client to get the QID 121 | ``` 122 | def get_sql 123 | event_URL = params[:event_URL].to_s 124 | 125 | query_slug = event_URL.match('qid.*?(?=&)').to_s.split('=')[1] 126 | puts "QID: #{query_slug}" 127 | 128 | response = ApplicationHelper.get_sql_for_query(query_slug) 129 | data = {:message => "Looker Generated SQL Query for Slug #{query_slug}: \n #{response}"} 130 | render :json => data, :status => :ok 131 | end 132 | ``` 133 | 134 | 135 | Step 4: Run API Call to Get SQL based on the Query
136 | API call: [Get SQL for Query](https://github.com/llooker/powered_by_modules/blob/master/Modules/API%20Calls/get_sql_for_query.rb) 137 | -------------------------------------------------------------------------------- /Use Cases/Javascript Events.md: -------------------------------------------------------------------------------- 1 | # Javascript Event Broadcasting 2 | 3 | ## Initial Steps 4 | To enable event broadcasting follow the steps below. [Detailed instructions](https://docs.looker.com/reference/embedding/embed-javascript-events) can be found on 5 | our official docs page. 6 | 1. Whitelist your website domain (https://mywebsite.com) on your Looker Admin panel to enable communication 7 | 2. Add your website domain to the iFrame HTML source tag
8 | Ex: 117 | ``` 118 | 119 | 120 | ## Parent to iFrame Communication 121 | 122 | ### Link Custom Filters to Looker iFrames 123 | Pass in user specified custom filter values from the parent page into the iFrame through a client side JS push event (without refreshing the iFrame). 124 | First, create a custom HTML form. 125 | ``` 126 |
127 |
128 | 129 | 130 | 131 | 132 |
133 |
134 | 135 | 136 |
137 |
138 | 139 | 140 |
141 | 142 |
143 | ``` 144 | On a filter click or action, the following post event transmits filter information from the parent website into the Looker iFrame to re-update the iFrame with the latest information. 145 | ``` 146 | $('.filter_element').click(function() { 147 | let Gender = `${checkboxList('gender')}`; 148 | let Category = `${checkboxList('category')}`; 149 | let startDate = new Date($('.start_range').val()).toISOString().slice(0,10); 150 | let endDate = new Date($('.end_range').val()).toISOString().slice(0,10); 151 | 152 | iframe.contentWindow.postMessage(JSON.stringify({ 153 | type:"dashboard:filters:update", 154 | filters:{ 155 | Gender: Gender, 156 | Date: startDate + " to " + endDate, 157 | Category: Category 158 | } 159 | }),"https://my.looker.com"); 160 | 161 | iframe.contentWindow.postMessage(JSON.stringify({ 162 | type: "dashboard:run" 163 | }),"https://my.looker.com"); 164 | 165 | }); 166 | ``` 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /Use Cases/Report Selector.md: -------------------------------------------------------------------------------- 1 | # Custom Use Case: Custom Report Selector 2 | 3 | 4 | Similar to embedding a dashboard, you can embed a look as an Iframe within any web application. 5 | 6 | You can also dynamically select looks from a particular project or space by using the API. 7 | 8 | 9 | ### Step 1: API CALL 10 | Use the API to get a list of all looks and attributes about that look in a particular space or project. 11 | 12 | #### API calls (Modules -> API Calls): 13 | 14 | * [Get All Looks In a Space](https://github.com/llooker/powered_by_modules/blob/master/Modules/API%20Calls/list_of_looks_in_a_space.md) 15 | * [Get All Looks Created By a User](https://github.com/llooker/powered_by_modules/blob/master/Modules/API%20Calls/list_of_looks_created_by_user.md) 16 | * [Get All Looks in a User Space](https://github.com/llooker/powered_by_modules/blob/master/Modules/API%20Calls/get_looks_in_user_space.rb) 17 | 18 | (Note: The API Calls above can be adjusted to grab dashboards instead of Looks) 19 | 20 | These Calls return an array of Looks with the following metadata about each of the looks -- Look ID, Title, Description, Modified Date, Created User 21 | 22 | 23 | ### Step 2: Generate Embed URL 24 | 25 | We could display all the Looks from the API call as selections and allow the User to visualize this look as a Data Element (Table) or as a Visualization. 26 | 27 | On a User Selection (of Look/Type of Look) we grab the form parameters and append the Look ID and type (Data = “&show=data”, Viz = “&show=viz”) to generate the embedded URL. 28 | 29 | Example Embed URL: 30 | embed_url = "https://instancename.looker.com/looks/?<&show=data>" 31 | 32 | 33 | 34 | ### Step 3: Authenticate Embed URL 35 | 36 | ``` 37 | require 'cgi' 38 | require 'securerandom' 39 | require 'uri' 40 | require 'base64' 41 | require 'json' 42 | require 'openssl' 43 | 44 | 45 | class Auth < ActiveRecord::Base 46 | 47 | def self.embed_url(additional_data = {}) 48 | 49 | url_data = { 50 | # System Level Permissions 51 | host: ENV['LOOKER_HOST'], # ex: [mylooker_instance.looker.com] (include port # if self hosted) 52 | secret: ENV['EMBED_SECRET'], # Secret Key 53 | session_length: 30.minutes, 54 | force_logout_login: true, 55 | 56 | # User Specific Permissions 57 | external_user_id: 102, #The External ID must be a Number and must be unique for every embedded User (Primary Key). 58 | first_name: "Dr", 59 | last_name: "Strange", 60 | 61 | # User Permissions 62 | permissions: ['see_user_dashboards', 'see_lookml_dashboards', 'access_data', 'see_looks', 'download_with_limit', 'explore'], 63 | models: ['powered_by'], #replace with name of your model 64 | user_attributes: {"company" => "Calvin Klein", "user_time_zone" => "America/New York"}, #Row level data permisisons per user and other user specific attributes. 65 | group_ids: [9], 66 | external_group_id: "Calvin Klein", #Used to create a group space for all users of this brand. 67 | }.merge(additional_data) 68 | 69 | 70 | url = Auth::created_signed_embed_url(url_data) 71 | 72 | embed_url = "https://#{url}" 73 | 74 | end 75 | 76 | 77 | def self.created_signed_embed_url(options) 78 | puts options.to_json 79 | # looker options 80 | secret = options[:secret] 81 | host = options[:host] 82 | 83 | # user options 84 | json_external_user_id = options[:external_user_id].to_json 85 | json_first_name = options[:first_name].to_json 86 | json_last_name = options[:last_name].to_json 87 | json_permissions = options[:permissions].to_json 88 | json_models = options[:models].to_json 89 | json_group_ids = options[:group_ids].to_json 90 | json_external_group_id = options[:external_group_id].to_json 91 | json_user_attributes = options[:user_attributes].to_json 92 | json_access_filters = options[:access_filters].to_json 93 | 94 | # url/session specific options 95 | embed_path = '/login/embed/' + CGI.escape(options[:embed_url]) 96 | json_session_length = options[:session_length].to_json 97 | json_force_logout_login = options[:force_logout_login].to_json 98 | 99 | # computed options 100 | json_time = Time.now.to_i.to_json 101 | json_nonce = SecureRandom.hex(16).to_json 102 | 103 | # compute signature 104 | string_to_sign = "" 105 | string_to_sign += host + "\n" 106 | string_to_sign += embed_path + "\n" 107 | string_to_sign += json_nonce + "\n" 108 | string_to_sign += json_time + "\n" 109 | string_to_sign += json_session_length + "\n" 110 | string_to_sign += json_external_user_id + "\n" 111 | string_to_sign += json_permissions + "\n" 112 | string_to_sign += json_models + "\n" 113 | 114 | # optionally add settings not supported in older Looker versions 115 | string_to_sign += json_group_ids + "\n" if options[:group_ids] 116 | string_to_sign += json_external_group_id+ "\n" if options[:external_group_id] 117 | string_to_sign += json_user_attributes + "\n" if options[:user_attributes] 118 | 119 | string_to_sign += json_access_filters 120 | 121 | signature = Base64.encode64( 122 | OpenSSL::HMAC.digest( 123 | OpenSSL::Digest.new('sha1'), 124 | secret, 125 | string_to_sign.force_encoding("utf-8"))).strip 126 | 127 | # construct query string 128 | query_params = { 129 | nonce: json_nonce, 130 | time: json_time, 131 | session_length: json_session_length, 132 | external_user_id: json_external_user_id, 133 | permissions: json_permissions, 134 | models: json_models, 135 | access_filters: json_access_filters, 136 | first_name: json_first_name, 137 | last_name: json_last_name, 138 | force_logout_login: json_force_logout_login, 139 | signature: signature 140 | } 141 | # add optional parts as appropriate 142 | query_params[:group_ids] = json_group_ids if options[:group_ids] 143 | query_params[:external_group_id] = json_external_group_id if options[:external_group_id] 144 | query_params[:user_attributes] = json_user_attributes if options[:user_attributes] 145 | query_params[:user_timezone] = options[:user_timezone].to_json if options.has_key?(:user_timezone) 146 | 147 | query_string = URI.encode_www_form(query_params) 148 | 149 | "#{host}#{embed_path}?#{query_string}" 150 | end 151 | end 152 | ``` 153 | 154 | 155 | ### Extend this Example 156 | 157 | 1. Handle User permissions on Customer Side
158 | Ex: You could have two sets of Users - Basic and Premium. Basic Users can be configured to only show looks from certain Models (Make a change to the API call to only render Looks from a particular Space and a particular Model). Premium Users get access to all looks in a Space. 159 | 2. Scale in complex ways - you could use this pattern to point the API at different spaces based on the permissions of the logged in user. So you could easily design customer tier levels where higher paying customers get access to different levels of reporting. 160 | 161 | 162 | -------------------------------------------------------------------------------- /Use Cases/Embed a Dashboard.md: -------------------------------------------------------------------------------- 1 | 2 | # Embedding: Embed a Dashboard 3 | 4 | 5 | You can embed a looker visualization (whether its a dashboard, look, query, or explore page) on any webpage as an Iframe. 6 | 7 | In this example we walk through embedding an dashboard. 8 | 9 | 10 | ### Step 1: Create a Dashboard 11 | Within Looker, create a dashboard that you would like to make publicly available on an external webpage. 12 | 13 | 14 | ### Step 2: Generate Embed URL 15 | 16 | Note the ID of the dashboard from the URL 17 | 18 | Example Embed URL: 19 | embed_url = "https://instancename.looker.com/dashboard/" 20 | 21 | 22 | 23 | ### Step 3: Authenticate Embed URL 24 | 25 | Gather all the parameters that are required to authenticate a dashboard. 26 | 1. User Specific Parameters: First Name, Last Name, External User ID, Permissions, Models, User Attributes 27 | 2. System Wide Parameters: Host, Secret, Session Length, Force Login Logout 28 | 29 | Using the following snippet of code, pass in the parameters above and create a signed embedded URL that can be accessed via iFrame. 30 | 31 | ``` 32 | require 'cgi' 33 | require 'securerandom' 34 | require 'uri' 35 | require 'base64' 36 | require 'json' 37 | require 'openssl' 38 | 39 | 40 | class Auth < ActiveRecord::Base 41 | 42 | def self.embed_url(additional_data = {}) 43 | 44 | url_data = { 45 | # System Level Permissions 46 | host: ENV['LOOKER_HOST'], # ex: [mylooker_instance.looker.com] (include port # if self hosted) 47 | secret: ENV['EMBED_SECRET'], # Secret Key 48 | session_length: 30.minutes, 49 | force_logout_login: true, 50 | 51 | # User Specific Permissions 52 | external_user_id: 102, #The External ID must be a Number and must be unique for every embedded User (Primary Key). 53 | first_name: "Dr", 54 | last_name: "Strange", 55 | 56 | # User Permissions 57 | permissions: ['see_user_dashboards', 'see_lookml_dashboards', 'access_data', 'see_looks', 'download_with_limit', 'explore'], 58 | models: ['powered_by'], #replace with name of your model 59 | user_attributes: {"company" => "Calvin Klein", "user_time_zone" => "America/New York"}, #Row level data permisisons per user and other user specific attributes. 60 | group_ids: [9], 61 | external_group_id: "Calvin Klein", #Used to create a group space for all users of this brand. 62 | 63 | embed_url: "/embed/dashboards/" 64 | }.merge(additional_data) 65 | 66 | 67 | url = Auth::created_signed_embed_url(url_data) 68 | 69 | embed_url = "https://#{url}" 70 | 71 | end 72 | 73 | 74 | def self.created_signed_embed_url(options) 75 | puts options.to_json 76 | # looker options 77 | secret = options[:secret] 78 | host = options[:host] 79 | 80 | # user options 81 | json_external_user_id = options[:external_user_id].to_json 82 | json_first_name = options[:first_name].to_json 83 | json_last_name = options[:last_name].to_json 84 | json_permissions = options[:permissions].to_json 85 | json_models = options[:models].to_json 86 | json_group_ids = options[:group_ids].to_json 87 | json_external_group_id = options[:external_group_id].to_json 88 | json_user_attributes = options[:user_attributes].to_json 89 | json_access_filters = options[:access_filters].to_json 90 | 91 | # url/session specific options 92 | embed_path = '/login/embed/' + CGI.escape(options[:embed_url]) 93 | json_session_length = options[:session_length].to_json 94 | json_force_logout_login = options[:force_logout_login].to_json 95 | 96 | # computed options 97 | json_time = Time.now.to_i.to_json 98 | json_nonce = SecureRandom.hex(16).to_json 99 | 100 | # compute signature 101 | string_to_sign = "" 102 | string_to_sign += host + "\n" 103 | string_to_sign += embed_path + "\n" 104 | string_to_sign += json_nonce + "\n" 105 | string_to_sign += json_time + "\n" 106 | string_to_sign += json_session_length + "\n" 107 | string_to_sign += json_external_user_id + "\n" 108 | string_to_sign += json_permissions + "\n" 109 | string_to_sign += json_models + "\n" 110 | 111 | # optionally add settings not supported in older Looker versions 112 | string_to_sign += json_group_ids + "\n" if options[:group_ids] 113 | string_to_sign += json_external_group_id+ "\n" if options[:external_group_id] 114 | string_to_sign += json_user_attributes + "\n" if options[:user_attributes] 115 | 116 | string_to_sign += json_access_filters 117 | 118 | signature = Base64.encode64( 119 | OpenSSL::HMAC.digest( 120 | OpenSSL::Digest.new('sha1'), 121 | secret, 122 | string_to_sign.force_encoding("utf-8"))).strip 123 | 124 | # construct query string 125 | query_params = { 126 | nonce: json_nonce, 127 | time: json_time, 128 | session_length: json_session_length, 129 | external_user_id: json_external_user_id, 130 | permissions: json_permissions, 131 | models: json_models, 132 | access_filters: json_access_filters, 133 | first_name: json_first_name, 134 | last_name: json_last_name, 135 | force_logout_login: json_force_logout_login, 136 | signature: signature 137 | } 138 | # add optional parts as appropriate 139 | query_params[:group_ids] = json_group_ids if options[:group_ids] 140 | query_params[:external_group_id] = json_external_group_id if options[:external_group_id] 141 | query_params[:user_attributes] = json_user_attributes if options[:user_attributes] 142 | query_params[:user_timezone] = options[:user_timezone].to_json if options.has_key?(:user_timezone) 143 | 144 | query_string = URI.encode_www_form(query_params) 145 | 146 | "#{host}#{embed_path}?#{query_string}" 147 | end 148 | 149 | end 150 | ``` 151 | 152 | ### Step 4: Display Embed URL 153 | Display the embed URL on a webpage as an Iframe 154 | 155 | My Embedded URL: <%= embed_url %> 156 |
157 | 158 | ``` 159 | <%= tag(:iframe, src: embed_url, 160 | id: 'looker_iframe' 161 | height: 700, 162 | width: "100%", 163 | allowtransparency: 'true') 164 | %> 165 | ``` 166 | 167 | ### Capture Events from your Embedded Iframes by adding Javascript 168 | 169 | ``` 170 | window.addEventListener("message", function (event) { 171 | if (event.source === document.getElementById("looker_iframe").contentWindow){ 172 | MyApp.blob = JSON.parse(event.data); 173 | console.log(MyApp.blob); 174 | } 175 | }); 176 | ``` 177 | [Javascript Events - Looker Documentation](https://docs.looker.com/reference/embedding/embed-javascript-events "JS Events") 178 | 179 | ### Create Custom Filters outside the Looker UI 180 | 181 | Create custom filters (checkboxes, radio buttons, maps) outside of the Looker UI in a HTML form element. Pass in filter actions (clicks, etc...) on Submit. Grab filter elements from the form and append to the end of the Dashboard URL. 182 | 183 | ``` 184 | @filters = "&Filter1=" + params[:filter1_value].to_s + "&Filter2=" + params[:filter2_value].to_s + "&Filter3=" + params[:filter3_value].to_s 185 | @iframe_url = current_user.embed_url( 186 | embed_url: "/embed/dashboards/#{dashboard_id}?embed_domain=#{embed_domain}&#{@filters}", 187 | ) 188 | ``` 189 | 190 | ### Extend this Example by 191 | 192 | 1. Embedding a LookML Dashboard 193 | embed_url = "/embed/dashboards/::" 194 | 195 | 2. Embedding a Look 196 | embed_url = "/embed/looks/" 197 | 198 | 3. Embedding a Query 199 | embed_url: "/embed/query//?qid=" 200 | [Example Use Case with Embedded Query](https://github.com/llooker/powered_by_modules/blob/master/Use%20Cases/Field%20Picker.md "Metrics Selector") 201 | 202 | 4. Embedding a Explore Page 203 | embed_url = "/embed/explore//" 204 | Add additional permissions for an embedded explore page such as: ['save_content', 'embed_browse_spaces'] to allow users to save and view content within an embed_browse_space. 205 | [Example Use Case with Embedded Explore](https://github.com/llooker/powered_by_modules/blob/master/Use%20Cases/Embed%20a%20Explore%20Page.md "Metrics Selector") 206 | 207 | -------------------------------------------------------------------------------- /Use Cases/Field Picker.md: -------------------------------------------------------------------------------- 1 | # Custom Use Case: Custom Embed Field Picker 2 | 3 | ### Step 1: Pick a Use Case 4 | End Users want to visualize a certain number of Metrics (New Users, Total Sale Price) over time for a particular Date Range and for a particular time period and with other filters specified. 5 | 6 | ### Step 2: Widgetize Use Case (Toggle Buttons, Drop down selectors, etc) 7 | 8 | Here is an example of a Simple Form (with checkboxes, radio buttons) in ruby. 9 | ``` 10 |
11 | 21 |
22 | 23 | 24 |
25 | 29 | 30 |
31 | ``` 32 | 33 | ### Step 3: Grab Input from form, Generate query, Grab the Query Slug 34 | 35 | On a form submit (post or get request), grab the values from the form and start to generate an embedded Query. 36 | Once a query has been generated, make an API call to get the shortened version of the Query - the Query SLUG. 37 | 38 | Sample Default URL - 39 | embed_url: "embed/query/powered_by/order_items?qid=" 40 | 41 | 42 | ``` 43 | class QueryController < ApplicationController 44 | 45 | def create_query 46 | @fields = params[:fields] #input from form 47 | @gender = params[:gender] #input from form 48 | 49 | #Looker API Call 50 | query = { 51 | :model=>"powered_by", 52 | :view=>"order_items", 53 | #:fields=>["order_items.created_month", "users.count","inventory_items.total_cost"], 54 | :fields => @fields, 55 | :filters=>{ 56 | :"products.brand"=> "Calvin Klein", 57 | :"order_items.created_month"=> "after 2015/01/01", 58 | :"users.gender"=>@gender}, 59 | :sorts=>["inventory_items.created_month desc"], 60 | :limit=>"100", 61 | :query_timezone=>"America/Los_Angeles" 62 | } 63 | query_slug = ApplicationHelper.get_query_slug(query) 64 | 65 | @options = { 66 | ##Using the Query Slug --> You can get the Query Slug by grabbing a URL 67 | embed_url: "/embed/query/powered_by/order_items?query=#{query_slug}" 68 | } 69 | @embed_url = Auth::embed_url(@options) 70 | end 71 | 72 | end 73 | ``` 74 | 75 | 76 | ### Authenticate with the query slug 77 | 78 | ``` 79 | require 'cgi' 80 | require 'securerandom' 81 | require 'uri' 82 | require 'base64' 83 | require 'json' 84 | require 'openssl' 85 | 86 | 87 | class Auth < ActiveRecord::Base 88 | 89 | def self.embed_url(additional_data = {}) 90 | 91 | url_data = { 92 | # System Level Permissions 93 | host: ENV['LOOKER_HOST'], # ex: [mylooker_instance.looker.com] (include port # if self hosted) 94 | secret: ENV['EMBED_SECRET'], # Secret Key 95 | session_length: 30.minutes, 96 | force_logout_login: true, 97 | 98 | # User Specific Permissions 99 | external_user_id: 102, #The External ID must be a Number and must be unique for every embedded User (Primary Key). 100 | first_name: "Dr", 101 | last_name: "Strange", 102 | 103 | # User Permissions 104 | permissions: ['see_user_dashboards', 'see_lookml_dashboards', 'access_data', 'see_looks', 'download_with_limit', 'explore'], 105 | models: ['powered_by'], #replace with name of your model 106 | user_attributes: {"company" => "Calvin Klein", "user_time_zone" => "America/New York"}, #Row level data permisisons per user and other user specific attributes. 107 | group_ids: [9], 108 | external_group_id: "Calvin Klein", #Used to create a group space for all users of this brand. 109 | 110 | }.merge(additional_data) 111 | 112 | 113 | url = Auth::created_signed_embed_url(url_data) 114 | 115 | embed_url = "https://#{url}" 116 | 117 | end 118 | 119 | 120 | def self.created_signed_embed_url(options) 121 | puts options.to_json 122 | # looker options 123 | secret = options[:secret] 124 | host = options[:host] 125 | 126 | # user options 127 | json_external_user_id = options[:external_user_id].to_json 128 | json_first_name = options[:first_name].to_json 129 | json_last_name = options[:last_name].to_json 130 | json_permissions = options[:permissions].to_json 131 | json_models = options[:models].to_json 132 | json_group_ids = options[:group_ids].to_json 133 | json_external_group_id = options[:external_group_id].to_json 134 | json_user_attributes = options[:user_attributes].to_json 135 | json_access_filters = options[:access_filters].to_json 136 | 137 | # url/session specific options 138 | embed_path = '/login/embed/' + CGI.escape(options[:embed_url]) 139 | json_session_length = options[:session_length].to_json 140 | json_force_logout_login = options[:force_logout_login].to_json 141 | 142 | # computed options 143 | json_time = Time.now.to_i.to_json 144 | json_nonce = SecureRandom.hex(16).to_json 145 | 146 | # compute signature 147 | string_to_sign = "" 148 | string_to_sign += host + "\n" 149 | string_to_sign += embed_path + "\n" 150 | string_to_sign += json_nonce + "\n" 151 | string_to_sign += json_time + "\n" 152 | string_to_sign += json_session_length + "\n" 153 | string_to_sign += json_external_user_id + "\n" 154 | string_to_sign += json_permissions + "\n" 155 | string_to_sign += json_models + "\n" 156 | 157 | # optionally add settings not supported in older Looker versions 158 | string_to_sign += json_group_ids + "\n" if options[:group_ids] 159 | string_to_sign += json_external_group_id+ "\n" if options[:external_group_id] 160 | string_to_sign += json_user_attributes + "\n" if options[:user_attributes] 161 | 162 | string_to_sign += json_access_filters 163 | 164 | signature = Base64.encode64( 165 | OpenSSL::HMAC.digest( 166 | OpenSSL::Digest.new('sha1'), 167 | secret, 168 | string_to_sign.force_encoding("utf-8"))).strip 169 | 170 | # construct query string 171 | query_params = { 172 | nonce: json_nonce, 173 | time: json_time, 174 | session_length: json_session_length, 175 | external_user_id: json_external_user_id, 176 | permissions: json_permissions, 177 | models: json_models, 178 | access_filters: json_access_filters, 179 | first_name: json_first_name, 180 | last_name: json_last_name, 181 | force_logout_login: json_force_logout_login, 182 | signature: signature 183 | } 184 | # add optional parts as appropriate 185 | query_params[:group_ids] = json_group_ids if options[:group_ids] 186 | query_params[:external_group_id] = json_external_group_id if options[:external_group_id] 187 | query_params[:user_attributes] = json_user_attributes if options[:user_attributes] 188 | query_params[:user_timezone] = options[:user_timezone].to_json if options.has_key?(:user_timezone) 189 | 190 | query_string = URI.encode_www_form(query_params) 191 | 192 | "#{host}#{embed_path}?#{query_string}" 193 | end 194 | 195 | end 196 | ``` 197 | 198 | 199 | ### Display Authenticated URL 200 | 201 | ``` 202 | My Embedded URL: <%= @embed_url %>
203 | 204 | <%= tag(:iframe, 205 | src: @embed_url, 206 | height: 700, 207 | width: "100%", 208 | allowtransparency: 'true') 209 | %> 210 | ``` 211 | 212 | -------------------------------------------------------------------------------- /actions-for-sfdc.md: -------------------------------------------------------------------------------- 1 | ## Welcome! 2 | This document will walk you through implementing Looker Actions and Salesforce. We've outlined two use cases here, but the code below can be adapted and expanded to any number of additional use cases. 3 | 4 | Briefly, the Looker Actions system works by sending row-level query results out of Looker in a way that can be processed by other systems. Because those results leave Looker in a structured format, and most tools that you'd want to integrate those results into expect information to be in any number of specific formats, an intermediary server is often required to accept the results from Looker and transform them for the application that will ultimately receive them. 5 | 6 | Once you've implemented the server and the integrated Looker and Salesforce to it, adding additional use cases to suit your company's needs is simple incremental work. Read on to see how we've set up this integration. 7 | 8 | ### Use Cases Covered Here 9 | - Updating the Annual Contract Value (ACV) on an open Opportunity 10 | - Changing the Stage of an Opportunity 11 | 12 | ### Implementation Notes 13 | - **This is example code!** While lots of the pieces will work right out of the box, this code snippet requires you to input your own pieces of information specific to your Salesforce instance and your Looker instance. An engineer who knows their way around this sort of code should implement it. 14 | - **We're relying on [jsforce](https://jsforce.github.io/), a Salesforce API Library for Javascript, in concert with [node.js](https://nodejs.org/en/).** We actually strongly recommend this setup. Ruby has a different package that we tried, and had trouble with authentication for sandbox hosts. Writing the calls from scratch doesn't work well because it only lets you query their RESTful API. This implementation requires being able to write to Salesforce's SOAP API. 15 | - Following this implementation, **actions will always occur as a single, hard-coded Salesforce user**. With the addition of User Attributes (a Looker feature that will be released in Looker 4.4), you will be able to allow users’ actions to show up as themselves within Salesforce. This is relevant because this implementation only allows the owner of an Opportunity to be able to update it. Therefore, you need each user's credentials to be set to allow them to update their own Oppportunities. 16 | - We chose certain fields for this implementation, but **you can change the fields to whatever makes the most sense for your workflow**. Actions can pass information from other fields that don’t carry the actual action (and don’t necessarily have to be present in the query in Looker). For example, if your table results include `opportunity.name` but not `opportunity.id`, you can pull and send `opportunity.id` in your action anyway. 17 | - Finally, **all of the code snippets below came from one entire code block**. That code block has been broken up to be able to annotate sections effectively, but if you string all pieces together, you will have a complete piece of code that implements a server and all of the actions listed above. 18 | - Seeing updated results in Looker depend on your ETL for Salesforce data. The information should be updated in Salesforce as soon as the Action is complete. 19 | 20 | ## Implementing Actions for Salesforce 21 | 22 | ### Get Ready 23 | 1. Get a certificate to use SSL for this server. ([We suggest the AWS certificate manager](https://aws.amazon.com/certificate-manager/)) 24 | 2. Create a [Connected App](https://developer.salesforce.com/page/Connected_Apps) in SFDC - you'll need a consumer key from that setup process. 25 | 3. Download Salesforce's node package, [jsforce](https://jsforce.github.io/). 26 | 4. Ensure you're using [node.js](https://nodejs.org/en/). 27 | 28 | ### Provide all Basic Information and Set up a Server 29 | This is where you'll input the information specific to your Salesforce instance, the Connected App you created, and your Looker instance. 30 | 31 | ```javascript 32 | // Constants 33 | var SANDBOX_HOST = "url to your salesforce host" 34 | // Get these from creating a connected app in Salesforce 35 | var CONSUMER_KEY = "your consumer key" 36 | var CONSUMER_SECRET = "your consumer secret" 37 | var SECURITY_TOKEN = "your security token" 38 | // Your username and password for Salesforce 39 | var USERNAME = "username" 40 | var PASSWORD = "password" 41 | // used as certs for local https development 42 | var CERT_PATH = 'path/to/your/ssl/cert' 43 | 44 | var fs = require('fs') 45 | var privateKey = fs.readFileSync(CERT_PATH + "/localhost.key", 'utf8') 46 | var certificate = fs.readFileSync(CERT_PATH + "/localhost.crt", 'utf8') 47 | var http = require('http') 48 | var https = require('https') 49 | var express = require('express') 50 | // Important, the Salesforce node package 51 | var jsforce = require('jsforce') 52 | var bodyParser = require('body-parser') 53 | 54 | var app = express() 55 | var port = 3000 56 | 57 | // Setup json form encoding (used to parse form data from Looker) 58 | app.use(bodyParser.json()); 59 | app.use(bodyParser.urlencoded({ 60 | extended: true 61 | })); 62 | // Setup HTTPS server 63 | var credentials = {key: privateKey, cert: certificate}; 64 | var httpsServer = https.createServer(credentials, app); 65 | httpsServer.listen(8443); 66 | 67 | var userId = ''; 68 | // Connect to Salesforce using JSForce... 69 | var sf_conn = new jsforce.Connection({ 70 | loginUrl: SANDBOX_HOST 71 | }); 72 | sf_conn.login(USERNAME, PASSWORD, (err, res) => { 73 | if (err) { 74 | return console.log("Error logging in", err) 75 | } 76 | console.log("Token: " + sf_conn.accessToken); 77 | console.log(res); 78 | userId = res.id; 79 | }); 80 | ``` 81 | ### Define your Actions 82 | These are two examples of available actions, including error and permission checking. 83 | 84 | *Update ACV* 85 | ```javascript 86 | app.post('/update_acv', (req, res) => { 87 | // Parse form fields 88 | var opportunity_id = req.body.data.id; 89 | var update_value = req.body.form_params.update_value; 90 | // Check if value is valid number 91 | if (isNaN(parseInt(update_value))) { 92 | console.log("Update value is not a number."); 93 | res.send(formResponseJson(false, "Please enter a valid number", false)); 94 | } else { 95 | // Check if user making the update is the owner of the opportunity, 96 | // otherwise, we can't update the field in Salesforce. 97 | sf_conn.sobject("Opportunity").retrieve(opportunity_id, (err, opportunity) => { 98 | if (err) { 99 | return console.error(err); 100 | } 101 | if (opportunity.OwnerId !== userId) { 102 | res.send(formResponseJson(false, "You don't have permission to update this field.", false)); 103 | } else { 104 | sf_conn.sobject("Opportunity").update({ 105 | Id : opportunity_id, 106 | Amount : update_value 107 | }, (err, query) => { 108 | if (err || !query.success) { 109 | console.log("Query failed: " + err); 110 | res.send(formResponseJson(query.success, "Error.", false)); 111 | } else { 112 | res.send(formResponseJson(true, "", true)); 113 | } 114 | }); 115 | } 116 | }); 117 | } 118 | }); 119 | ``` 120 | *Update Status* 121 | 122 | ```javascript 123 | app.post('/update_status', (req, res) => { 124 | var opportunity_id = req.body.data.id; 125 | var update_value = req.body.form_params.status; 126 | sf_conn.sobject("Opportunity").retrieve(opportunity_id, (err, opportunity) => { 127 | if (err) { 128 | return console.error(err); 129 | } 130 | if (opportunity.OwnerId !== userId) { 131 | console.log("OwnerId !== UserId"); 132 | res.send(formResponseJson(false, "You don't have permission to update this field.", false)); 133 | } else { 134 | sf_conn.sobject("Opportunity").update({ 135 | Id : opportunity_id, 136 | StageName : update_value 137 | }, (err, query) => { 138 | if (err || !query.success) { 139 | console.log("Query failed: " + err); 140 | res.send(formResponseJson(query.success, "Error.", false)); 141 | } else { 142 | res.send(formResponseJson(true, "", true)); 143 | } 144 | }); 145 | } 146 | }); 147 | }); 148 | 149 | app.listen(port, (err) => { 150 | if (err) { 151 | return console.log('Error starting server', err) 152 | } 153 | console.log(`Server listening on port ${port}`); 154 | }); 155 | 156 | var formResponseJson = function(isSuccess, validationError, refreshQuery) { 157 | var response = { 158 | "looker": { 159 | "success": isSuccess, 160 | ``` 161 | ### Define the Actions in LookML 162 | 163 | *Update ACV* 164 | ```lookml 165 | dimension: amount { 166 | required_fields: [id] 167 | action: { 168 | label: "Update ACV" 169 | url: "https://localhost:8443/update_acv" 170 | param: { 171 | name: "id" 172 | value: "{{ row['opportunities.id'] }}" 173 | } 174 | param: { 175 | name: "Content-Type" 176 | value: "application/x-www-form-urlencoded" 177 | } 178 | form_param: { 179 | name: "update_value" 180 | type: string 181 | } 182 | } 183 | type: number 184 | sql: ${TABLE}.amount ;; 185 | } 186 | ``` 187 | *Update Status* 188 | Notice that form parameters are defined in the LookML in this example. You also have the option to set form parameters on the server, but setting them in LookML makes them easier to manipulate by other Looker developers. This is particularly useful if form parameters are subject to change. 189 | 190 | ```lookml 191 | dimension: status { 192 | required_fields: [id] 193 | action: { 194 | label: "Update Status" 195 | url: "https://localhost:8443/update_status" 196 | param: { 197 | name: "id" 198 | value: "{{ row['opportunities.id'] }}" 199 | } 200 | param: { 201 | name: "Content-Type" 202 | value: "application/x-www-form-urlencoded" 203 | } 204 | form_param: { 205 | name: "status" 206 | type: select 207 | default: "{{ row['opportunities.status'] }}" 208 | option: { 209 | name: "Active Lead" 210 | label: "Active Lead" 211 | } 212 | option: { 213 | name: "Qualified Prospect" 214 | label: "Qualified Prospect" 215 | } 216 | option: { 217 | name: "Trial Requested" 218 | label: "Trial Requested" 219 | } 220 | option: { 221 | name: "Trial" 222 | label: "Trial" 223 | } 224 | option: { 225 | name: "Trial - In progress, positive" 226 | label: "Trial - In progress, positive" 227 | } 228 | option: { 229 | name: "Proposal" 230 | label: "Proposal" 231 | } 232 | option: { 233 | name: "Negotiation" 234 | label: "Negotiation" 235 | } 236 | option: { 237 | name: "Commit- Not Won" 238 | label: "Commit- Not Won" 239 | } 240 | option: { 241 | name: "Closed Won" 242 | label: "Closed Won" 243 | } 244 | option: { 245 | name: "Closed Lost" 246 | label: "Closed Lost" 247 | } 248 | } 249 | } 250 | type: string 251 | sql: ${TABLE}.status ;; 252 | } 253 | ``` 254 | -------------------------------------------------------------------------------- /actions-for-github.md: -------------------------------------------------------------------------------- 1 | ## Welcome! 2 | This document will walk you through implementing Looker Actions and GitHub. We've outlined 5 use cases here, but the code below can be adapted and expanded to any number of additional use cases. Briefly, the Looker Actions system works by sending row-level query results out of Looker in a way that can be processed by other systems. Because those results leave Looker in a structured format, and most tools that you'd want to integrate those results into expect information to be in any number of specific formats, an intermediary server is often required to accept the results from Looker and transform them for the application that will ultimately receive them. Once you've implemented the server and the integrated Looker and Github to it, adding additional use cases to suit your company's needs is simple incremental work. Read on to see how we've set up this integration. 3 | 4 | ### Use Cases Covered Here 5 | - Creating a new Issue 6 | - Commenting on an Issue 7 | - Labeling an Issue 8 | - Closing or re-opening an existing Issue 9 | - Assigning an Issue 10 | 11 | ### Implementation Notes 12 | - This is example code! While lots of the pieces will work right out of the box, this code snippet requires you to input your own pieces of information specific to your GitHub repository and your Looker instance. An engineer who knows their way around this sort of code should implement it. 13 | - We're using Sinatra to set up a server here, but you can adapt the implementation to use the tools of your own choosing. 14 | - We're relying on Oktokit ([GitHub's Ruby Wrapper on top of their API](http://octokit.github.io/octokit.rb/)), so you'll need that for this implementation to function. 15 | - Following this implementation, actions will always occur as a single GitHub user. With the addition of User Attributes (a Looker feature that will be released in Looker 4.4), you would be able to allow users’ actions to show up as themselves within GitHub. To extend the existing implementation, any user who wants to be able to use the GitHub actions would need to get their own authentication token and set it on their account in Looker so User Attributes can be aware of it. 16 | - We chose certain fields for this implementation, but you can change the fields to whatever makes the most sense for your workflow. Actions can pass information from other fields that don’t carry the actual action (and don’t necessarily have to be present in the query in Looker). For example, if your table results include `issue_title` but not `issue_id`, you can pull and send `issue_id` in your action anyway. 17 | - Depending on the speed of your ETL, it may take a while to see the results of the action that you’ve performed in Looker. You can circumvent this by 'kicking' the ETL every time you make a change via actions. That option is embedded in this code, but you can also choose to omit it entirely by setting the ETL option to false. At that point, your data will update at its regularly scheduled ETL. 18 | - Finally, all of the code snippets below came from one entire code block. That code block has been broken up to be able to annotate sections effectively, but if you string all pieces together, you will have a complete piece of code that implements a server and all of the actions listed above. 19 | 20 | ## Implementing Actions for GitHub 21 | 22 | ### Get Ready 23 | 1. Get a certificate to use SSL for this server. ([We suggest the AWS certificate manager](https://aws.amazon.com/certificate-manager/)) 24 | 2. Get your authorization token from GitHub (github.com/settings/token). 25 | 3. Install [Oktokit](http://octokit.github.io/octokit.rb/) 26 | 27 | ### Configure your Server 28 | The next three sections should run as one code block. They're broken up here for ease of understanding and reading. 29 | 30 | ```ruby 31 | require 'sinatra' 32 | 33 | require 'octokit' 34 | require 'sinatra/base' 35 | require 'webrick' 36 | require 'webrick/https' 37 | require 'openssl' 38 | require 'json' 39 | 40 | 41 | REPO_NAME = '[filepath to your repo goes here]' 42 | CERT_PATH = '[file path to your certificate from Basic Step #1]' 43 | 44 | # personal access token from github from Basic Step #2 45 | GITHUB_TOKEN = ENV['GITHUB_AUTH_TOKEN'] || 'TOKEN GOES HERE' 46 | 47 | # whether or not to perform the ETL / refresh query when action is completed 48 | # input either true or false 49 | PERFORM_ETL = true | false 50 | 51 | webrick_options = { 52 | :Port => 8443, 53 | :Logger => WEBrick::Log::new($stderr, WEBrick::Log::DEBUG), 54 | :DocumentRoot => "/ruby/htdocs", 55 | :SSLEnable => true, 56 | :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, 57 | :SSLCertificate => OpenSSL::X509::Certificate.new( File.open(File.join(CERT_PATH, "looker.self.signed.pem")).read), 58 | :SSLPrivateKey => OpenSSL::PKey::RSA.new( File.open(File.join(CERT_PATH, "looker.self.signed.key")).read), 59 | :SSLCertName => [ [ "CN",WEBrick::Utils::getservername ] ] 60 | } 61 | 62 | class MyServer < Sinatra::Base 63 | set :show_exceptions, false 64 | 65 | def default_client 66 | Octokit::Client.new(:access_token => GITHUB_TOKEN) 67 | end 68 | 69 | before '/*' do 70 | data = JSON.parse(request.body.read, symbolize_names: true) 71 | @form_params = data[:form_params] 72 | @data = data[:data] 73 | 74 | @client = default_client 75 | end 76 | 77 | # if you selected 'true' for PERFORM_ETL, input your ETL logic in this code block 78 | def execute_etl 79 | `[your ETL logic goes here]` 80 | end 81 | 82 | def reply_success 83 | execute_etl if PERFORM_ETL 84 | 85 | content_type 'application/json' 86 | 87 | {'looker' => {'success' => true, 'refresh_query' => PERFORM_ETL}}.to_json 88 | end 89 | ``` 90 | ### Define the Forms you Want Constructed on the Server 91 | Some information is better to pull on the server itself (rather than asking for it in your LookML code). In these examples, we're pulling the unique list of labels available in your GitHub repo to populate a dropdown in the form, and then passing the entire form through to Looker. If you'd prefer, you can also create forms in the Looker Actions code and not on the server at all. 92 | 93 | ```ruby 94 | # these are the more complex forms that contain extra information from the 95 | # github API (such as assignee candidates / possible labels) 96 | 97 | post '/assignee_form.json' do 98 | assignees = @client.repository_assignees(REPO_NAME) 99 | 100 | data = assignees.map do |user| 101 | {'name' => user.login, 'label' => user.login} 102 | end 103 | 104 | [{'name' => 'user', 'required' => 'true', 'type' => 'select'}.merge('options' => data)].to_json 105 | end 106 | 107 | post '/create_issue_form.json' do 108 | assignees = @client.repository_assignees(REPO_NAME) 109 | 110 | label_names = default_client.labels(REPO_NAME).map(&:name) 111 | 112 | labels = label_names.map do |label| 113 | {'name' => label, 'label' => label} 114 | end 115 | 116 | data = assignees.map do |user| 117 | {'name' => user.login, 'label' => user.login} 118 | end 119 | 120 | [ 121 | {'name' => 'title', 'required' => true}, 122 | {'name' => 'body', 'type' => 'textarea'}, 123 | {'name' => 'Assignee', 'type' => 'select'}.merge('options' => data), 124 | {'name' => 'Label 1', 'type' => 'select'}.merge('options' => labels), 125 | {'name' => 'Label 2', 'type' => 'select'}.merge('options' => labels), 126 | ].to_json 127 | end 128 | 129 | post '/label_form.json' do 130 | label_names = default_client.labels(REPO_NAME).map(&:name) 131 | 132 | labels = label_names.map do |label| 133 | {'name' => label, 'label' => label} 134 | end 135 | 136 | [{'name' => 'label', 'required' => 'true', 'type' => 'select'}.merge('options' => labels)].to_json 137 | end 138 | ``` 139 | 140 | 141 | ### Define the Actions you Want to Perform 142 | You can implement any or all of the following actions. They should also be considered blueprints for other actions you might want to implement. 143 | 144 | *Create an Issue* 145 | ```ruby 146 | post '/create_issue' do 147 | title = @form_params[:title] 148 | body = @form_params[:body] 149 | assignee = @form_params[:"Assignee"] 150 | label1 = @form_params[:'Label 1'] 151 | label2 = @form_params[:'Label 2'] 152 | 153 | labels = [] 154 | 155 | labels.push(label1) if label1 156 | labels.push(label2) if label2 157 | 158 | @client.create_issue(REPO_NAME, title, body, { 159 | :assignee => assignee, :labels => labels 160 | }) 161 | 162 | reply_success 163 | end 164 | ``` 165 | *Add Assignee* 166 | ```ruby 167 | post '/issue/:issue_number/add_assignee' do |issue_number| 168 | login = @form_params[:user] 169 | @client.update_issue(REPO_NAME, issue_number, {:assignee => login}) 170 | 171 | reply_success 172 | end 173 | ``` 174 | *Toggle Issue State from Opened to Closed* 175 | 176 | ```ruby 177 | post '/issue/:issue_number/state/:state' do |issue_number, state| 178 | if state == 'reopen' 179 | @client.reopen_issue(REPO_NAME, issue_number) 180 | else 181 | @client.close_issue(REPO_NAME, issue_number) 182 | end 183 | 184 | reply_success 185 | end 186 | ``` 187 | *Add a Comment to an Issue* 188 | 189 | ```ruby 190 | post '/issue/:issue_number/add_comment' do |issue_number| 191 | body = @form_params[:comment_body] 192 | 193 | @client.add_comment(REPO_NAME, issue_number, body) 194 | 195 | reply_success 196 | end 197 | ``` 198 | *Add a Label to an Issue* 199 | 200 | ```ruby 201 | # looker action will request the label form (above) to allow user to pick amongst valid labels 202 | post '/issue/:issue_number/add_label' do |issue_number| 203 | label_name = @form_params[:label] 204 | @client.add_labels_to_an_issue(REPO_NAME, issue_number, [label_name]) 205 | 206 | reply_success 207 | end 208 | ``` 209 | *Remove a Label from an Issue* 210 | ```ruby 211 | post '/issue/:issue_number/remove_label/:label_name' do |issue_number, label_name| 212 | @client.remove_label(REPO_NAME, issue_number, label_name) 213 | 214 | reply_success 215 | end 216 | ``` 217 | ### Defining Actions in LookML 218 | The following sections of code will be done in the Looker IDE on the fields that you want to update to include actions. The field that you select will offer the action when you click any value of that field on the Explore page, in a Look, or in a tile on a Dashboard. 219 | 220 | *Create an Issue* 221 | ```lookml 222 | dimension: name { 223 | sql: ${TABLE}.name ;; 224 | 225 | action: { 226 | label: "Create Issue" 227 | url: "https://localhost:8443/create_issue" 228 | form_url: "https://localhost:8443/create_issue_form.json" 229 | } 230 | } 231 | ``` 232 | 233 | *Add an Assignee to an Issue* 234 | ```lookml 235 | dimension: assignee { 236 | # note this here is bad below 237 | sql: COALESCE(${TABLE}.assignee, 'NONE') ;; 238 | 239 | action: { 240 | label: "Add/Update Assignee" 241 | url: "https://localhost:8443/issue/{{number._value}}/add_assignee" 242 | form_url: "https://localhost:8443/assignee_form.json" 243 | } 244 | } 245 | ``` 246 | *Toggle Issue Open or Closed* 247 | ```lookml 248 | dimension: open { 249 | type: yesno 250 | sql: ${TABLE}.state = 'open' ;; 251 | 252 | action: { 253 | label: "Toggle Open/Closed" 254 | url: "https://localhost:8443/issue/{{ number._value}}/state/{% if value == 'Yes' %}close{% else %}reopen{% endif %}" 255 | } 256 | } 257 | ``` 258 | *Add Comment to an Issue and Add a Label to an Issue* 259 | ```lookml 260 | dimension: title { 261 | description: "This is the name of the issue." 262 | required_fields: [repo.name, number] 263 | html: {{ value }} 264 | ;; 265 | sql: ${TABLE}.title ;; 266 | 267 | 268 | action: { 269 | label: "Add Comment" 270 | url: "https://localhost:8443/issue/{{number._value}}/add_comment" 271 | 272 | form_param: { 273 | name: "comment_body" 274 | required: yes 275 | } 276 | } 277 | 278 | action: { 279 | label: "Add Label" 280 | url: "https://localhost:8443/issue/{{number._value}}/add_label" 281 | form_url: "https://localhost:8443/label_form.json" 282 | } 283 | ``` 284 | --------------------------------------------------------------------------------