home User Guide Getting Started Help Center Documentation Community Training Certification
menu
close
settings
Looker keyboard_arrow_down
language keyboard_arrow_down
English
Français
Deutsch
日本語
search
print
Extension framework React and JavaScript code examples

This page provides code examples written in React and JavaScript for common functions you may want to utilize in your extensions.

Using the Looker Extension SDK

To add functions from the Looker Extension SDK, first you need to get a reference to the SDK, which can be done either from the provider or globally. Then you can call SDK functions as you would in any JavaScript application.

Now you can use the SDK as you would in any JavaScript application:

const GetLooks = async () => { try { const looks = await sdk.ok(sdk.all_looks('id')) // process looks … } catch (error) { // do error handling … } }

Navigating elsewhere in the Looker instance

Since the extension runs in a sandboxed iframe, you cannot navigate elsewhere within the Looker instance by updating the parent’s window.location object. It is possible to navigate using the Looker Extension SDK.

This function requires the navigation entitlement.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext … extensionSDK.updateLocation('/browse')

Opening a new browser window

Since the extension runs in a sandboxed iframe, you cannot use the parent window to open a new browser window. It is possible to open up a browser window using the Looker Extension SDK.

This function requires either the new_window entitlement to open a new window to a location in the current Looker instance, or the new_window_external_urls entitlement to open a new window that runs on a different host.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext … extensionSDK.openBrowserWindow('/browse', '_blank') … extensionSDK.openBrowserWindow('https://docs.looker.com/reference/manifest-params/application#entitlements', '_blank')

Routing and deep linking

The following applies to React-based extensions.

The ExtensionProvider and ExtensionProvider2 components automatically create a React Router called MemoryRouter for you to use. Do not attempt to create a BrowserRouter, as it does not work in sandboxed iframes. Do not attempt to create a HashRouter, as it it does not work in sandboxed iframes for the non-Chromium-based version of the Microsoft Edge browser.

If the MemoryRouter is utilized and you use react-router in your extension, the extension framework will automatically sync your extension’s router to the Looker host router. This means that the extension will be notified of browser backward and forward button clicks and of the current route when the page is reloaded. This also means that the extension should automatically support deep linking. See the extension examples for how to utilize react-router.

Extension context data

Extension framework context data should not be confused with React contexts.

Extensions have the ability to share context data between all users of an extension. The context data can be used for data that does not change frequently and that does not have special security requirements. 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 upon startup. The Looker Extension SDK provides functions to allow the context data to be updated and refreshed.

The maximum size of the context data is approximately 16 MB. Context data will be serialized to a JSON string, so that also needs to be taken into account if you use context data for your extension.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext … // Get loaded context data. This will reflect any updates that have // been made by saveContextData. let context = await extensionSDK.getContextData() … // Save context data to Looker server. context = await extensionSDK.saveContextData(context) … // Refresh context data from Looker server. context = await extensionSDK.refreshContextData()

User attributes

The Looker Extension SDK provides an API to access Looker user attributes. There are two types of user attribute access:

Below is a list of user attributes API calls:

To access user attributes, you must specify the attribute names in the global_user_attributes and/or scoped_user_attributes entitlements. For example, in the LookML project manifest file, you would add:

entitlements: { scoped_user_attributes: ["my_value"] global_user_attributes: ["locale"] }

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext // Read global user attribute const locale = await extensionSDK.userAttributeGetItem('locale') // Read scoped user attribute const value = await extensionSDK.userAttributeGetItem('my_value') // Update scoped user attribute const value = await extensionSDK.userAttributeSetItem('my_value', 'abcd1234') // Reset scoped user attribute const value = await extensionSDK.userAttributeResetItem('my_value')

Local storage

Sandboxed iframes do not allow access to browser local storage. The Looker Extension SDK allows an extension to read and write to the parent window’s local storage. Local storage is namespaced to the extension, meaning it cannot read local storage created by the parent window or other extensions.

Using local storage requires the local_storage entitlement.

The extension localhost API is asynchronous as opposed to the synchronous browser local storage API.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext // Read from local storage const value = await extensionSDK.localStorageGetItem('my_storage') // Write to local storage await extensionSDK.localStorageSetItem('my_storage', 'abcedefh') // Delete item from local storage await extensionSDK.localStorageRemoveItem('my_storage')

Updating the page title

Extensions may update the current page title. Entitlements are not required to perform this action.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext extensionSDK.updateTitle('My Extension Title')

Writing to the system clipboard

Sandboxed iframes do not allow access to the system clipboard. The Looker Extension SDK allows an extension to write text to the system clipboard. For security purposes, the extension is not allowed to read from the system clipboard.

To write to the system clipboard, you need the use_clipboard entitlement.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext // Write to system clipboard try { await extensionSDK.clipboardWrite( 'My interesting information' ) . . . } catch (error) { . . . }

Embedding dashboards, Looks, and Explores

The extension framework supports embedding of dashboards, Looks, and Explores. Both regular dashboards and legacy dashboards can be embedded.

The use_embeds entitlement is required. We recommend that you use the Looker JavaScript Embed SDK to embed content. See the Embed SDK documentation for more information.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext … const canceller = (event: any) => { return { cancel: !event.modal } } const updateRunButton = (running: boolean) => { setRunning(running) } const setupDashboard = (dashboard: LookerEmbedDashboard) => { setDashboard(dashboard) } const embedCtrRef = useCallback( (el) => { const hostUrl = extensionContext?.extensionSDK?.lookerHostData?.hostUrl if (el && hostUrl) { el.innerHTML = " LookerEmbedSDK.init(hostUrl) const db = LookerEmbedSDK.createDashboardWithId(id as number) .withNext() .appendTo(el) .on('dashboard:loaded', updateRunButton.bind(null, false)) .on('dashboard:run:start', updateRunButton.bind(null, true)) .on('dashboard:run:complete', updateRunButton.bind(null, false)) .on('drillmenu:click', canceller) .on('drillmodal:explore', canceller) .on('dashboard:tile:explore', canceller) .on('dashboard:tile:view', canceller) .build() .connect() .then(setupDashboard) .catch((error: Error) => { console.error('Connection error', error) }) } }, [] ) return (<EmbedContainer ref={embedCtrRef} />)

The extension examples use styled components to provide simple styling to the generated iframe. For example:

import styled from "styled-components" export const EmbedContainer = styled.div` width: 100%; height: 95vh; & > iframe { width: 100%; height: 100%; }

Accessing external API endpoints

The extension framework provides two methods for accessing external API endpoints:

In both cases you need to specify the external API endpoint in the extension external_api_urls entitlement.

Server proxy

The following example demonstrates the use of the server proxy to get an access token for use by the fetch proxy. The client id and secret must be defined as user attributes for the extension. Typically, when the user attribute is set up, the default value is set to the client id or secret.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext … const requestBody = { client_id: extensionSDK.createSecretKeyTag('my_client_id'), client_secret: extensionSDK.createSecretKeyTag('my_client_secret'), }, try { const response = await extensionSDK.serverProxy( 'https://myaccesstokenserver.com/access_token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), } ) const { access_token, expiry_date } = response.body … } catch (error) { // Error handling … }

The user attribute name must be mapped to the extension. Dashes must be replaced by underscores and the :: characters must be replaced with a single underscore.

For example, if the name of your extension is my-extension::my-extension, the user attributes that need to be defined for the above example would be:

my_extension_my_extension_my_client_id my_extension_my_extension_'my_client_secret'

Fetch proxy

The following example demonstrates the use of the fetch proxy. It uses the access token from the previous server proxy example.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext … try { const response = await extensionSDK.fetchProxy( 'https://myaccesstokenserver.com/myendpoint', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: Bearer ${accessToken}, }, body: JSON.stringify({ some_value: someValue, another_value: anotherValue, }), } ) // Handle success … } catch (error) { // Handle failure … }

OAuth integration

The extension framework supports integration with OAuth providers. OAuth can be used to get an access token to access a particular resource, for example a Goorgle sheets document.

You will need to specify the OAuth server endpoint in the extension oauth2_urls entitlement. You may also need to specify additional URLs in the external_api_urls entitlement.

The extension frameworks supports the following flows:

The general flow is that a child window is opened that loads an OAuth server page. The OAuth server authenticates the user and redirects back to the Looker server with additional details that can be used to get an access token.

Implicit flow:

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext … const response = await extensionSDK.oauth2Authenticate( 'https://accounts.google.com/o/oauth2/v2/auth', { client_id: GOOGLE_CLIENT_ID!, scope: GOOGLE_SCOPES, response_type: 'token', } ) const { access_token, expires_in } = response

Authorization code grant type with secret key:

const authenticateParameters: Record<string, string> = { client_id: GITHUB_CLIENT_ID!, response_type: 'code', } const response = await extensionSDK.oauth2Authenticate( 'https://github.com/login/oauth/authorize', authenticateParameters, 'GET' ) const exchangeParameters: Record<string, string> = { client_id: GITHUB_CLIENT_ID!, code: response.code, client_secret: extensionSDK.createSecretKeyTag('github_secret_key'), } const codeExchangeResponse = await extensionSDK.oauth2ExchangeCodeForToken( 'https://github.com/login/oauth/access_token', exchangeParameters ) const { access_token, error_description } = codeExchangeResponse

PKCE code challenge and verifier:

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext … const authRequest: Record<string, string> = { client_id: AUTH0_CLIENT_ID!, response_type: 'code', scope: AUTH0_SCOPES, code_challenge_method: 'S256', } const response = await extensionSDK.oauth2Authenticate( 'https://sampleoauthserver.com/authorize', authRequest, 'GET' ) const exchangeRequest: Record<string, string> = { grant_type: 'authorization_code', client_id: AUTH0_CLIENT_ID!, code: response.code, } const codeExchangeResponse = await extensionSDK.oauth2ExchangeCodeForToken( 'https://sampleoauthserver.com/login/oauth/token', exchangeRequest ) const { access_token, expires_in } = codeExchangeResponse

Spartan

Spartan refers to a method of using the Looker instance as an environment to expose extensions, and extensions only, to a designated set of users. A spartan user navigating to a Looker instance will be presented with whatever login flow the Looker admin has configured. Once the user is authenticated, an extension will be presented to the user according to their landing_page user attribute as shown below. The user can only access extensions; they cannot access any other part of Looker. If the user has access to multiple extensions, the extensions control the user’s ability to navigate to the other extensions using extensionSDK.updateLocation. There is one specific Looker Extension SDK method to allow the user to log out of the Looker instance.

import { ExtensionContext2 } from '@looker/extension-sdk-react' … const extensionContext = useContext( ExtensionContext2 ) const { extensionSDK } = extensionContext … // Navigate to another extension extensionSDK.updateLocation('/spartan/another::extension') … // Logout extensionSDK.spartanLogout()

Defining spartan users

In order to define a spartan user, you must create a group called “Extensions Only”.

Once the “Extensions Only” group has been created, navigate to the User Attributes page in Looker’s Admin section and edit the landing_page user attribute. Select the Group Values tab and add the “Extensions Only” group. The value should be set to /spartan/my_extension::my_extension/ where my_extension::my_extension is the id of your extension. Now when that user logs in, the user will be routed to the designated extension.

Code splitting

Code splitting is the technique where code is requested only when it is needed. Typically code chunks are associated with React routes where each route gets its own code chunk. In React this is done with the Suspense and React.lazy components. The Suspense component displays a fallback component while the code chunk is loaded. React.lazy is responsible for loading the code chunk.

Setting up to code split:

import { AsyncComp1 as Comp1 } from './Comp1.async' import { AsyncComp1 as Comp2 } from './Comp2.async' … <Suspense fallback={<div>Loading...</div>}> <Switch> <Route path="/comp1"> <Comp1 /> </Route> <Route path="/comp2"> <Comp2 /> </Route> </Switch> <Suspense>

The lazy loaded component is implemented as follows:

import { lazy } from 'react' const Comp1 = lazy( async () => import(/* webpackChunkName: "comp1" */ './Comp1') ) export const AsyncComp1 = () => <Home />

The component is implemented as follows. The component must be exported as a default component:

const Comp1 = () => { return ( <div>Hello World</div> ) } export default Comp1

Tree shaking

Looker SDKs do support tree shaking but they are not yet perfect. We are continually modifying our SDKs to improve tree shaking support. Some of these changes may require that you refactor your code to take advantage, but when this is required, it will be documented in the release notes.

To utilize tree shaking the module you are using must be exported as an esmodule and the functions you import must be free of side effects. The Looker SDK for Typescript/Javascript, Looker SDK Runtime Library, Looker UI Components, Looker Extension SDK, and Extension SDK for React all do this.

In an extension, you should pick one of the Looker SDKs, 3.1 or 4.0 and use the ExtensionProvider2 component from the Extension SDK for React. If you require both SDKs, continue to use the ExtensionProvider component, but you will see an increase in the final bundle size.

The following code sets up the extension provider. You will need to tell the provider which SDK you want:

import { MyExtension } from './MyExtension' import { ExtensionProvider2 } from '@looker/extension-sdk-react' import { Looker40SDK } from '@looker/sdk/lib/4.0/methods' import { hot } from 'react-hot-loader/root' export const App = hot(() => { return ( <ExtensionProvider2 type={Looker40SDK}> <MyExtension /> </ExtensionProvider2> ) })

Do not use the following import style in your extension:

import * as lookerComponents from @looker/components

The above example brings in everything from the module. Instead, only import the components you actually need. For example:

import { Paragraph } from @looker/components

Glossary

Top