The extension-examples repository is the new home of Looker's Extension examples.
This version and versions going forward demonstrate the use of code splitting. You must be on Looker version 21.0 or above for code splitting to work correctly.
To remove code splitting you will need to modify KitchenSink.tsx
. Instructions are provided in this file to show how to fall back to building a single monolithic JavaScript bundle.
This repository demonstrates functionality that is available to the Extension SDK. It can be used as a starting point for developing your own extensions.
It uses React and TypeScript for writing your extension, the React Extension SDK for interacting with Looker, and Webpack for building your code.
This version of the Kitchen sink requires Looker 7.9 or above.
-
Clone or download a copy of this template to your development machine
-
Navigate (
cd
) to the template directory on your system -
Install the dependencies with Yarn.
yarn install
You may need to update your Node version or use a Node version manager to change your Node version.
-
Start the development server
yarn develop
Great! Your extension is now running and serving the JavaScript at http://localhost:8080/bundle.js.
Note well: The webpack development server also supports https. To use, add the parameter --https to the start command
"start": "webpack serve --disable-host-check --https"
Should you decide to use https, you should visit the bundle URL you are running as there will likely be a certificate warning. The development server runs with a self-signed SSL certificate, so you will need to accept this to allow your browser to connect to it.The default yarn start command runs with hot module replacement working. Some changes will cause a full reload of the extension iframe. When this happens the extension framework connection will break. You will need to do a full page reload of the outer page to restart the extension.
To run without hot module replacement run
yarn develop --no-hot
-
Now log in to Looker and create a new project.
This is found under Develop => Manage LookML Projects => New LookML Project.
You'll want to select "Blank Project" as your "Starting Point". You'll now have a new project with no files.
- In your copy of the extension project you have
manifest.lkml
file.
You can either drag & upload this file into your Looker project, or create a
manifest.lkml
with the same content. Change theid
,label
, orurl
as needed.application: kitchensink { label: "Kitchen sink" url: "http://localhost:8080/bundle.js" entitlements: { local_storage: yes navigation: yes new_window: yes allow_forms: yes allow_same_origin: yes core_api_methods: ["all_connections","search_folders", "run_inline_query", "me", "all_looks", "run_look"] external_api_urls: ["http://127.0.0.1:3000", "http://localhost:3000", "https://*.googleapis.com", "https://*.github.com", "https://REPLACE_ME.auth0.com"] oauth2_urls: ["https://accounts.google.com/o/oauth2/v2/auth", "https://github.com/login/oauth/authorize", "https://dev-5eqts7im.auth0.com/authorize", "https://dev-5eqts7im.auth0.com/login/oauth/token", "https://github.com/login/oauth/access_token"] } }
- In your copy of the extension project you have
The manifest includes a reference to the oauth2_url https://REPLACE_ME.auth0.com
. This URL needs to be obtained from Auth0 and is explained later in this document.
-
Create a
model
LookML file in your project. The name doesn't matter. The model and connection won't be used, and in the future this step may be eliminated.- Add a connection in this model. It can be any connection, it doesn't matter which.
- Configure the model you created so that it has access to some connection.
-
Connect your new project to Git. You can do this multiple ways:
- Create a new repository on GitHub or a similar service, and follow the instructions to connect your project to Git
- A simpler but less powerful approach is to set up git with the "Bare" repository option which does not require connecting to an external Git Service.
-
Commit your changes and deploy your them to production through the Project UI.
-
Reload the page and click the
Browse
dropdown menu. You should see your extension in the list.- The extension will load the JavaScript from the
url
you provided in theapplication
definition/ - Reloading the extension page will bring in any new code changes from the extension template (webpack's hot reloading is not currently supported).
- The extension will load the JavaScript from the
Going forward, most new features added to the Extension SDK will require that an entitlement be defined for the feature in the application manifest. If you plan on adding your extension to the Looker Marketplace, entitlements MUST be defined, even for existing functionality. Eventually most existing Extension SDK features will require entitlements to be defined so it is recommended that you start defining entitlements now.
The external API feature demonstrated in the Kitchensink is a new feature and requires that entitlements are defined. This is now reflected in the sample manifest contained repository.
Extensions can share context data between users. The context data can be used for data that does not change frequently and to share amongst different users of the extension. Care should be taken when writing the data as there is no data locking and the last write wins. The context data is available to the extension immediately. Functions are provided to write and refresh the data.
getContextData
- get the context data.saveContextData
- writes context data to the Looker server.refreshContextData
- gets the lastest context data from the Looker server.
The configuation component demonstrates the context functionality. It can be used to show/hide views in the Kitchen Sink. It can also be used to change the keys used for the embed demonstrations.
API functions demonstrates the following functionality
- Update title - modifies the title of the page the extension is running in.
- Navigation - navigates to different locations in the Looker server using the current page (browse and marketplace).
- Open new window - opens a new browser window.
- Verify host connection - simple mechanism to check whether the extension and Looker host are in touch. In reality an extension will never need to use this functionality.
- Local storage access - ability to read, write and remove data from local storage. Note that localstorage is namespaced to the extension.
- Pinger action - demonstrates sending data to Lookers pinger server.
- Generate error - demonstrates that extension errors are reported. Note that in Looker server development mode these are not reported.
- Route test - demonstrates routing with query strings and push state.
Core SDK functions demonstrates various calls the Looker SDK.
- All connections (GET method)
- Search folders (GET with parameters)
- Inline query (POST method)
There are three Embed demonstrations:
- Dashboard - can be toggled between class and next dashboards.
- Explore
- Look
The fetch proxy demonstration requires that a json data server be running. To start the server run the command
yarn data-server
An error message will be displayed if the server is not running OR if the required entitlements are not defined.
Create a .env file with the following entries. The values are explained later in the document. These values should be set prior to starting the development and data servers. Do NOT store the .env file in your source code repository.
CUSTOM_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_API_KEY=
GITHUB_CLIENT_ID=
AUTH0_CLIENT_ID=
AUTH0_BASE_URL=
POSTS_SERVER_URL=http://127.0.0.1:3000
POSTS_SERVER_URL is the URL of the data server.
The custom client secret can be any value. It is NOT used in the extension but is used by the demo data server to validate whether a user is authorized the data server (note that the implemention is exceedingly simplistic and is just used for demo purposes). The client secret should be added to the .env
file so that the data server can do a simple check.
CUSTOM_CLIENT_SECRET=
The custom client secret must also be added to the User attributes in the looker server. The user attribute should be set up as follows:
- name -
kitchensink_kitchensink_custom_secret_key
- user acess - view
- hide values - yes
- domain whitelist - http://127.0.0.1:3000/*
- default value - your secret key
The extension authenticates the user by adding a secret key tag to the authentication request. The secret key tag is replaced by the Looker server with the user attribute value. The authentication endpoint then returns a JWT token that can be used in subsequent requests. The code that does this is found in the Auth.tsx
file:
const dataServerAuth = async (body: any): Promise<string | undefined> => {
try {
// The custom secret will be resolved by the Looker server.
body.client_secret = extensionSDK.createSecretKeyTag('custom_secret_key')
const response = await extensionSDK.serverProxy(
`${POSTS_SERVER_URL}/auth`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(body),
}
)
if (response.ok && response.body && response.body.jwt_token) {
return response.body.jwt_token
}
} catch (error) {
console.error(error)
}
return undefined
}
The demo requires a client id and an API key to access the Google sheets API. To obtain one, click here and follow the instructions in step 1. The following values need to be setup in the .env
file, these values can be found in the google developer console.
GOOGLE_CLIENT_ID=Application OAUTH2 client ID
GOOGLE_API_KEY=Application API key
When the user uses the Google OAUTH2 authorization mechanism the client id is used. The extension accesses the sheets API directly. Note that the OAUTH2 implicit flow is used to authorize with Google.
When the user uses the other authorization mechanisms, the extension access the sheets API using the serverProxy call. The data server uses the API key to access the sheets API. This way the API key is NOT exposed in the extension code.
The Github OAUTH2 mechanism uses the Authorization code grant type with secret key. Create a Github OAUTH App. Add the Github client id to your .env file
GITHUB_CLIENT_ID=Github OAUTH2 client ID
The Github client secret must also be added to the User attributes in the looker server. The user attribute should be set up as follows:
- name -
kitchensink_kitchensink_github_secret_key
- user acess - view
- hide values - yes
- domain whitelist - https://github.com/login/oauth/access_token
- default value - Github client secret
See Auth.tsx
for authorizing use Github OAUTH2 Authorization code grant type with secret key.
const githubSignin = async () => {
try {
const response = await extensionSDK.oauth2Authenticate(
'https://github.com/login/oauth/authorize',
{
client_id: GITHUB_CLIENT_ID,
response_type: 'code',
},
'GET'
)
const codeExchangeResponse = await extensionSDK.oauth2ExchangeCodeForToken(
'https://github.com/login/oauth/access_token',
{
client_id: GITHUB_CLIENT_ID,
client_secret: extensionSDK.createSecretKeyTag('github_secret_key'),
code: response.code,
}
)
const { access_token } = codeExchangeResponse
// Success handling
} catch (error) {
// Error handling
}
}
The Auth0 OAUTH2 mechanism uses the uses the Authorization Code grant type with either a secret key or PKCE (code challenge & code verifier). Create a Auth0 account. Add the Auth0 client id and base URL to your .env file
AUTH0_CLIENT_ID=Auth0 Client id
AUTH0_BASE_URL=https://{tenant_id}.auth0.com
The Auth0 application client secret must also be added to the User attributes in the looker server. The user attribute should be set up as follows:
- name -
kitchensink_kitchensink_auth0_secret_key
- user acess - view
- hide values - yes
- domain whitelist - https://{tenant_id}.auth0.com/login/oauth/token
- default value - Auth0 client secret
See Auth.tsx
for authorizing using the Auth0 OAUTH2 Authorization Code grant type. The following demonstrates use of the secret key.
const auth0Signin = async () => {
try {
const response = await extensionSDK.oauth2Authenticate(
`${AUTH0_BASE_URL}/authorize`,
{
client_id: AUTH0_CLIENT_ID,
response_type: 'code',
scope: AUTH0_SCOPES,
},
'GET'
)
const codeExchangeResponse = await extensionSDK.oauth2ExchangeCodeForToken(
`${AUTH0_BASE_URL}/login/oauth/token`,
{
grant_type: 'authorization_code',
client_id: AUTH0_CLIENT_ID,
client_secret: extensionSDK.createSecretKeyTag('auth0_secret_key'),
code: response.code,
}
)
const { access_token, expires_in } = codeExchangeResponse
// Success processing
} catch (error) {
// Error processing
}
}
See Auth.tsx
for authorizing using the Auth0 OAUTH2 PKCE (code challenge & code verifier). The following demonstrates use of the code challenge.
const auth0Signin = async () => {
try {
const response = await extensionSDK.oauth2Authenticate(
`${AUTH0_BASE_URL}/authorize`,
{
client_id: AUTH0_CLIENT_ID,
response_type: 'code',
scope: AUTH0_SCOPES,
authRequest.code_challenge_method = 'S256',
},
'GET'
)
const codeExchangeResponse = await extensionSDK.oauth2ExchangeCodeForToken(
`${AUTH0_BASE_URL}/login/oauth/token`,
{
grant_type: 'authorization_code',
client_id: AUTH0_CLIENT_ID,
code: response.code,
}
)
const { access_token, expires_in } = codeExchangeResponse
// Success processing
} catch (error) {
// Error processing
}
}
Code splitting relies on React.lazy
and Suspense
to render dynamic imports of a component. The name of the generated JavaScript file can be influenced by specifying a webpackChunkName comment. Note that the Suspense
component is rendered by the KitchenSink
component.
Example:
import React, { lazy, Suspense } from 'react'
const Home = lazy<any>(
async () => import(/* webpackChunkName: "home" */ './Home')
)
export const AsyncHome: React.FC = () => <Home />
```
Note that the imported component MUST be the default export for the module.
```typescript
import React from 'react'
import { Heading, Paragraph, SpaceVertical } from '@looker/components'
import { SandboxStatus } from '../SandboxStatus'
import { HomeProps } from './types'
const Home: React.FC<HomeProps> = () => {
return <>. . .</>
}
export default Home
The following packages now support tree shaking which reduces the size the bundle generated:
@looker/sdk
@looker/sdk-rtl
@looker/extension-sdk
@looker/extension-sdk-react
Note that the @looker/components
does not yet support tree shaking but when it does bundle sizes should be reduced even further.
To fully take advantage of tree shaking, the extension should use a single SDK and use ExtensionProvider2
which only pulls in dependent code for the chosen SDK.
Example setup (see App.tsx
):
return (
<ExtensionProvider2 onRouteChange={onRouteChange} type={Looker40SDK}>
<KitchenSink route={route} routeState={routeState} />
</ExtensionProvider2>
)
Example usage:
const extensionContext = useContext<ExtensionContextData2<Looker40SDK>>(
ExtensionContext2
)
const { extensionSDK, coreSDK } = extensionContext
OR
const sdk = getCoreSDK2<Looker40SDK>()
The process above requires your local development server to be running to load the extension code. To allow other people to use the extension, a production build of the extension needs to be run. As the kitchensink uses code splitting to reduce the size of the initially loaded bundle, multiple JavaScript files are generated.
- In your extension project directory on your development machine, build the extension by running the command
yarn build
. - Drag and drop ALL of the generated JavaScript files contained in the
dist
directory into the Looker project interface. - Modify your
manifest.lkml
to usefile
instead ofurl
and point it at thebundle.js
file:application: kitchensink { label: "Kitchen sink" file: "bundle.js" entitlements: { local_storage: yes navigation: yes new_window: yes allow_forms: yes allow_same_origin: yes core_api_methods: ["all_connections","search_folders", "run_inline_query", "me", "all_looks", "run_look"] external_api_urls: ["http://127.0.0.1:3000", "http://localhost:3000", "https://*.googleapis.com", "https://*.github.com", "https://REPLACE_ME.auth0.com"] oauth2_urls: ["https://accounts.google.com/o/oauth2/v2/auth", "https://github.com/login/oauth/authorize", "https://dev-5eqts7im.auth0.com/authorize", "https://dev-5eqts7im.auth0.com/login/oauth/token", "https://github.com/login/oauth/access_token"] } }
Note that the additional JavaScript files generated during the production build process do not have to be mentioned in the manifest. These files will be loaded dynamically by the extension as and when they are needed. Note that to utilize code splitting, the Looker server must be at version 7.21 or above.
- The template uses Looker's component library and styled components. Neither of these libraries are required so you may remove and replace them with a component library of your own choice,