拡張フレームワークの React と JavaScript のコード例

Looker 拡張フレームワークは、まもなく新しい読み込みメカニズムを使用します。新しい拡張機能では、既存の拡張機能が読み込まれたときにエラーが発生することがあります。Looker 環境で正式に有効化される前に、新しいローダで拡張機能をテストする方法については、Looker ヘルプセンターの新しい拡張機能フレームワーク ローダーのテストに関する記事をご覧ください。

このページでは、拡張機能で使用する一般的な関数の React と JavaScript で記述されたコードサンプルについて説明します。

Looker Extension SDK の使用

Looker の拡張機能 SDK から関数を追加するには、まず、SDK への参照を取得する必要があります。SDK への参照は、プロバイダまたはグローバルで実行できます。これで、他の JavaScript アプリケーションの場合と同様に、SDK 関数を呼び出すことができます。

  • プロバイダから SDK にアクセスするには:
  import { ExtensionContext2 } from '@looker/extension-sdk-react'

  export const Comp1 = () => {
    const extensionContext = useContext(
      ExtensionContext2
    )
    const { extensionSDK, coreSDK } = extensionContext
  • SDK にグローバルにアクセスするには(呼び出す前に拡張機能を初期化する必要があります)。
    const coreSDK = getCoreSDK2()

これで、任意の JavaScript アプリケーションと同じように SDK を使用できるようになりました。

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

拡張機能はサンドボックス化された iframe で実行されるため、親の window.location オブジェクトを更新して Looker インスタンス内の他の場所に移動することはできません。Looker 拡張機能 SDK を使用してナビゲートできます。

この関数には navigation の利用資格が必要です。

import { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  const { extensionSDK } = extensionContext

. . .

  extensionSDK.updateLocation('/browse')

新しいブラウザ ウィンドウを開く

この拡張機能はサンドボックス化された iframe 内で実行されるため、親ウィンドウを使用して新しいブラウザ ウィンドウを開くことはできません。Looker 拡張機能 SDK を使用してブラウザ ウィンドウを開くことができます。

この関数では、new_window 利用資格で現在の Looker インスタンス内の場所を新しいウィンドウで開くか、new_window_external_urls 利用資格で別のホストで実行されている新しいウィンドウを開く必要があります。

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')

ルーティングとディープリンク

以下は、React ベースの拡張機能に適用されます。

ExtensionProviderExtensionProvider2 のコンポーネントでは、MemoryRouter という React ルーターが自動的に作成されます。BrowserRouter はサンドボックス化された iframe では機能しないため、作成しないでください。HashRouter は作成しないでください。Microsoft Edge ブラウザの Chromium ベース バージョン以外のサンドボックス化された iframe では機能しません。

MemoryRouter を利用し、拡張機能で react-router を使用する場合、拡張機能フレームワークは拡張機能のルーターを Looker ホストルーターに自動的に同期します。つまり、拡張機能は、ページが再読み込みされると、ブラウザの [戻る] ボタンと [進む] ボタンのクリック、現在のルートが通知されます。また、拡張機能が自動的にディープリンクをサポートするようになります。react-router の使用方法については、拡張機能の例をご覧ください。

拡張機能のコンテキスト データ

拡張機能フレームワークのコンテキスト データを React のコンテキストと混同しないでください。

拡張機能は、拡張機能のすべてのユーザー間でコンテキスト データを共有できます。コンテキスト データは、頻繁に変更されないデータや特別なセキュリティ要件がないデータに使用できます。データロックは発生せず、最後の書き込みが有効になるため、データの書き込みには注意が必要です。コンテキスト データは、起動時にすぐに拡張機能で利用できます。Looker の拡張機能 SDK には、コンテキスト データを更新および更新する関数が用意されています。

コンテキスト データの最大サイズは約 16 MB です。コンテキスト データは JSON 文字列にシリアル化されるため、拡張機能にコンテキスト データを使用する場合は、これを考慮する必要があります。

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()

ユーザー属性

Looker の拡張機能 SDK は、Looker のユーザー属性にアクセスするための API を提供します。ユーザー属性へのアクセスには次の 2 種類があります。

  • スコープ - 拡張機能に関連付けられます。スコープのユーザー属性は拡張された名前空間であり、使用する前にユーザー属性を Looker インスタンスで定義する必要があります。ユーザー属性に名前空間を設定するには、属性名の前に拡張機能名を付けます。ダッシュやコロンはユーザー属性名に使用できないため、ダッシュや '::' 拡張機能名に含まれる文字はアンダースコアに置き換える必要があります

    たとえば、拡張機能 ID が my-extension::my-extension の、スコープが my_value のユーザー属性は、ユーザー属性名 my_extension_my_extension_my_value を定義する必要があります。ユーザー属性は一度定義されると、拡張機能によって読み取られ、更新されます。

  • グローバル - グローバル ユーザー属性であり、読み取り専用です。例として、locale ユーザー属性があります。

API 呼び出しのユーザー属性のリストは次のとおりです。

  • userAttributeGetItem - ユーザー属性を読み取ります。デフォルト値を定義し、ユーザーにユーザー属性値が存在しない場合に使用されます。
  • userAttributeSetItem - 現在のユーザーのユーザー属性を保存します。グローバルなユーザー属性の場合に失敗します。保存された値は、現在のユーザーだけに表示されます。
  • userAttributeResetItem - 現在のユーザーのユーザー属性をデフォルト値にリセットします。グローバルなユーザー属性の場合に失敗します。

ユーザー属性にアクセスするには、global_user_attributesscoped_user_attributes の一方または両方の利用資格の属性名を指定する必要があります。たとえば、LookML のプロジェクト マニフェスト ファイルに以下を追加します。

  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')

ローカル ストレージ

サンドボックス化された iframe では、ブラウザのローカル ストレージにアクセスできません。Looker 拡張機能 SDK を使用すると、拡張機能から親ウィンドウのローカル ストレージへの読み取りと書き込みを行うことができます。ローカル ストレージは拡張機能の名前空間になります。つまり、親ウィンドウまたは他の拡張機能によって作成されたローカル ストレージを読み取ることはできません。

ローカル ストレージを使用するには、local_storage の利用資格が必要です。

拡張機能の localhost API は、同期ブラウザのローカル ストレージ 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')

ページタイトルを更新する

拡張機能によって現在のページタイトルが更新される場合があります。この操作を行うのに資格は必要ありません。

import { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  const { extensionSDK } = extensionContext

  extensionSDK.updateTitle('My Extension Title')

システム クリップボードへの書き込み

サンドボックス化された iframe では、システム クリップボードにアクセスできません。Looker 拡張機能 SDK を使用すると、システム クリップボードにテキストを書き込むことができます。セキュリティ上の理由から、この拡張機能はシステム クリップボードから読み取ることができません。

システム クリップボードに書き込むには、use_clipboard の利用資格が必要です。

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) {
      . . .
    }

ダッシュボード、Look、Explore を埋め込む

拡張フレームワークは、ダッシュボード、Look、Explore の埋め込みをサポートしています。通常のダッシュボードと以前のダッシュボードの両方を埋め込むことができます。

use_embeds の利用資格が必要です。Looker JavaScript Embed SDK を使用してコンテンツを埋め込むことをおすすめします。詳しくは、Embed SDK のドキュメントをご覧ください。

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} />)

この拡張機能の例では、スタイル付きコンポーネントを使用して、生成される iframe にシンプルなスタイルを提供しています。例:

import styled from "styled-components"

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

外部 API エンドポイントへのアクセス

拡張フレームワークには、外部 API エンドポイントにアクセスするための 2 つのメソッドが用意されています。

  • サーバー プロキシ - Looker サーバー経由でエンドポイントにアクセスします。この仕組みにより、Looker サーバーによってクライアント ID と秘密鍵を安全に設定できます。
  • 取得プロキシ - ユーザーのブラウザからエンドポイントにアクセスします。プロキシは Looker UI です。

どちらの場合も、拡張機能の external_api_urls 利用資格に外部 API エンドポイントを指定する必要があります。

サーバー プロキシ

次の例は、サーバー プロキシを使用して、フェッチ プロキシで使用するアクセス トークンを取得する方法を示しています。クライアント ID とシークレットは、拡張機能のユーザー属性として定義する必要があります。通常、ユーザー属性を設定する場合、デフォルト値はクライアント ID またはシークレットに設定されます。

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
    . . .
  }

ユーザー属性名を拡張機能にマッピングする必要があります。ダッシュはアンダースコアに置き換える必要があり、:: 文字は単一のアンダースコアに置き換える必要があります。

たとえば、拡張機能の名前が my-extension::my-extension の場合、上記の例で定義する必要があるユーザー属性は次のようになります。

my_extension_my_extension_my_client_id
my_extension_my_extension_'my_client_secret'

プロキシの取得

次の例は、取得プロキシの使用方法を示しています。前述のサーバー プロキシの例のアクセス トークンを使用します。

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 の統合

拡張フレームワークは、OAuth プロバイダとの統合をサポートしています。OAuth は、特定のリソース(Goorgle スプレッドシート ドキュメントなど)にアクセスするためのアクセス トークンを取得するために使用できます。

extension oauth2_urls の利用資格で OAuth サーバー エンドポイントを指定する必要があります。external_api_urls の利用資格で追加の URL の指定が必要になる場合もあります。

拡張フレームワークは、次のフローをサポートします。

  • 暗黙的フロー
  • 秘密鍵を使用した認証コード権限付与タイプ
  • PKCE コード確認および確認者

一般的なフローは、OAuth サーバーページを読み込む子ウィンドウが開くことです。OAuth サーバーはユーザーを認証し、アクセス トークンの取得に使用できる追加情報を使用して Looker サーバーにリダイレクトします。

暗黙的フロー:

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

秘密鍵を使用した認証コード権限付与タイプ:

  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 コードによる確認と検証:

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 とは、Looker インスタンスを環境として使用し、指定されたユーザーのみに拡張機能と拡張機能のみを公開する方法です。Looker インスタンスに移動するシンプルなユーザーは、Looker 管理者が構成したログインフローが表示されます。ユーザーが認証されると、以下のように、landing_page ユーザー属性に従って拡張機能が表示されます。ユーザーは、拡張機能にのみアクセスできます。Looker の他の部分にはアクセスできません。ユーザーが複数の拡張機能にアクセスできる場合、その拡張機能は extensionSDK.updateLocation を使用して、ユーザーが他の拡張機能に移動できるかどうかを制御します。ユーザーが Looker インスタンスからログアウトできるようにするための Looker の拡張機能 SDK のメソッドが 1 つあります。

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()

単純なユーザーの定義

簡潔なユーザーを定義するには、「広告表示オプションのみ」という名前のグループを作成する必要があります

「拡張機能のみ」グループを作成したら、Looker の [Admin] セクションの [User Attributes] ページに移動し、landing_page ユーザー属性を編集します。[グループの値] タブを選択し、「Extensions Only」グループを追加します。値は /spartan/my_extension::my_extension/ に設定します。my_extension::my_extension は拡張機能の ID です。これで、ユーザーがログインすると、指定した拡張機能にルーティングされるようになりました。

コード分割

コード分割は、必要な場合にのみコードがリクエストされる手法です。通常、コードチャンクは React ルートに関連付けられ、各ルートは独自のコードチャンクを取得します。React では、Suspense コンポーネントと React.lazy コンポーネントを使用します。Suspense コンポーネントは、コードチャンクの読み込み中にフォールバック コンポーネントを表示します。React.lazy は、コード チャンクを読み込みます。

コード分割の設定:

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>

遅延読み込みコンポーネントは次のように実装されます。

import { lazy } from 'react'

const Comp1 = lazy(
 async () => import(/* webpackChunkName: "comp1" */ './Comp1')
)

export const AsyncComp1 = () => &#60;Home />

このコンポーネントは、次のように実装されます。このコンポーネントは、デフォルトのコンポーネントとしてエクスポートする必要があります

const Comp1 = () => {
  return (
    &#60;div&#62;Hello World&#60;/div&#62;
  )
}

export default Comp1

木の揺れ

Looker SDK はツリー シェイキングをサポートしますが、まだ完全ではありません。Google は、ツリー シェイキングのサポートを改善するために SDK の変更に継続的に取り組んでいます。こうした変更の中には、コードを活用するためにリファクタリングが必要になる場合がありますが、必要な場合はリリースノートに記載されます。

ツリー シェイキングを利用するには、使用しているモジュールを esmodule としてエクスポートし、インポートする関数に副作用がないようにしてください。TypeScript/JavaScript 用の Looker SDKLooker SDK ランタイム ライブラリLooker UI コンポーネントLooker 拡張機能 SDKExtension SDK for React はすべてこれらに対応しています。

拡張機能では、Looker SDK(3.1 または 4.0)のいずれかを選択し、Extension SDK for ReactExtensionProvider2 コンポーネントを使用する必要があります。両方の SDK が必要な場合は、引き続き ExtensionProvider コンポーネントを使用しますが、最終的なバンドルサイズが増加します。

次のコードは、拡張機能プロバイダを設定します。使用する SDK をプロバイダに伝える必要があります。

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 (
    &#60;ExtensionProvider2 type={Looker40SDK}&#62;
      &#60;MyExtension /&#62;
    &#60;/ExtensionProvider2&#62;
  )
})

拡張機能で次のインポート スタイルを使用しないでください。

import * as lookerComponents from `@looker/components`

上記の例では、モジュールからすべてのものを取り入れています。代わりに、実際に必要なコンポーネントのみをインポートしてください。例:

import { Paragraph }  from `@looker/components`

用語集

  • コード分割 - 実際に必要になるまで JavaScript の遅延読み込みを行う手法です。最初に読み込んだ JavaScript バンドルのサイズをできる限り小さくすることをおすすめします。これは、コード分割を利用することで実現できます。すぐに必要のない機能は、実際に必要になるまでは読み込まれません。
  • IDE - 統合開発環境。拡張機能を作成および変更するために使用されるエディタ。たとえば、Visual Studio Code、Intellij、WebStorm などがあります。
  • シーン — 通常は Looker のページビューです。シーンは主要ルートに対応。シーンに、主要なルートのサブルートにマッピングする子シーンが含まれることもあります。
  • トランスパイル - ある言語で記述されたソースコードを取得し、同様の抽象化レベルを持つ別の言語に変換するプロセス。たとえば、TypeScript は JavaScript になります。