拡張機能フレームワークの React と JavaScript のコードサンプル

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

Looker Extension SDK を使用する

拡張機能は Looker ホストとの接続を確立する必要があります。React では、拡張機能を ExtensionProvider40 コンポーネントでラップすることで実現します。このコンポーネントは、Looker ホストとの接続を確立し、Looker Extension SDKLooker SDK を拡張機能で使用できるようにします。

import React from 'react'
import { ExtensionProvider40 } from '@looker/extension-sdk-react'
import { DemoCoreSDK } from './DemoCoreSDK'


export const App = () => {
 return (
   <ExtensionProvider40 chattyTimeout={-1}>
     <DemoCoreSDK />
   </ExtensionProvider40>
 )
}

拡張機能プロバイダの背景

拡張機能プロバイダは、Looker Extension SDK と SDK API を拡張機能に公開します。拡張機能フレームワークの作成以降、拡張機能プロバイダのさまざまなバージョンが作成されています。このセクションでは、拡張機能プロバイダの歴史と、ExtensionProvider40 が推奨されるプロバイダである理由について説明します。

最初の拡張機能プロバイダは ExtensionProvider で、Looker SDK のバージョン 3.1 と 4.0 の両方を公開していました。デメリットは、両方の SDK を含めると、最終的な製品バンドルのサイズが大きくなることです。

ExtensionProvider2 が作成されました。これは、拡張機能で両方の SDK を使用し、デベロッパーにどちらかを選択するよう強制することは意味がないためです。残念ながら、この方法では、最終的な製品バンドルのサイズに両方の SDK が含まれてしまいます。

SDK 4.0 が一般提供に移行されたときに、ExtensionProvider40 が作成されました。ExtensionProvider40 の利点は、SDK 4.0 が唯一の利用可能なバージョンであるため、デベロッパーが使用する SDK を選択する必要がないことです。SDK 3.1 は最終的なバンドルに含まれないため、バンドルのサイズを削減できます。

Looker Extension SDK の関数を追加するには、まず SDK への参照を取得する必要があります。これは、プロバイダから取得することも、グローバルに取得することもできます。その後、通常の JavaScript アプリケーションと同様に SDK 関数を呼び出すことができます。

  • プロバイダから SDK にアクセスする手順は次のとおりです。
  import { ExtensionContext40 } from '@looker/extension-sdk-react'

  export const Comp1 = () => {
    const extensionContext = useContext(
      ExtensionContext40
    )
    const { extensionSDK, coreSDK } = extensionContext
  • SDK にグローバルにアクセスするには(この呼び出しの前に拡張機能を初期化する必要があります)、次の手順を行います。
    const coreSDK = getCoreSDK()

これで、任意の 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 Extension SDK を使用して移動できます。

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

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

. . .

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

. . .

  extensionSDK.updateLocation('/browse')

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

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

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

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

. . .

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

. . .
  extensionSDK.openBrowserWindow('/browse', '_blank')
. . .
  extensionSDK.openBrowserWindow('https://docs.looker.com/reference/manifest-params/application#entitlements', '_blank')

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

React ベースの拡張機能には次のものが対象となります。

ExtensionProviderExtensionProvider2ExtensionProvider40 コンポーネントは、使用できる MemoryRouter という React Router を自動的に作成します。BrowserRouter はサンドボックス化された iframe では動作しないため、作成しないでください。HashRouter は作成しないでください。Microsoft Edge ブラウザの非 Chromium ベースのバージョン用サンドボックス化された iframe では動作しないためです。

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

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

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

拡張機能は、拡張機能のすべてのユーザー間でコンテキスト データを共有できます。コンテキスト データは、頻繁に変更されず、特別なセキュリティ要件がないデータに使用できます。データのロックは行われず、最後の書き込みが優先されるため、データの書き込みには注意が必要です。コンテキスト データは、拡張機能の起動直後に使用できるようになります。Looker Extension SDK は、コンテキスト データのアップデートと更新を可能にする関数を提供します。

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

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

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  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 Extension SDK には、Looker のユーザー属性にアクセスするための API が用意されています。ユーザー属性のアクセスには次の 2 種類があります。

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

    たとえば、対象範囲のユーザー属性名 my_valuemy-extension::my-extension という拡張機能 ID と一緒に使用するには、ユーザー属性名に my_extension_my_extension_my_value を定義する必要があります。定義されたユーザー属性は、拡張機能によって読み取られ、更新される場合があります。

  • グローバル - グローバル ユーザー属性で、読み取り専用です。サンプルは、locale ユーザー属性です。

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

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

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

  entitlements: {
    scoped_user_attributes: ["my_value"]
    global_user_attributes: ["locale"]
  }
import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  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 Extension SDK を使用すると、拡張機能で親ウィンドウのローカル ストレージへの読み書きが可能になります。ローカル ストレージは拡張機能に名前空間が割り当てられます。つまり、親ウィンドウや他の拡張機能によって作成されたローカル ストレージを読み取ることはできません。

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

拡張機能の localhost API は、同期ブラウザのローカル ストレージ API とは対照的に非同期です。

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

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  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 { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

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

  extensionSDK.updateTitle('My Extension Title')

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

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

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

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

. . .

const extensionContext = useContext(
    ExtensionContext40
  )
  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 { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  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 (&#60;EmbedContainer ref={embedCtrRef} /&#62;)

拡張機能の例では、スタイル設定されたコンポーネントを使用して、生成された 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 と Secret は、拡張機能のユーザー属性として定義する必要があります。通常、ユーザー属性が設定されている場合、デフォルト値はクライアント ID または Secret に設定されます。

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

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  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
    . . .
  }

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

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

my_extension_my_extension_my_client_id
my_extension_my_extension_'my_client_secret'

取得プロキシ

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

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

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  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 を使用すると、Google スプレッドシートのドキュメントなど、特定のリソースにアクセスするためのアクセス トークンを取得できます。

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

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

  • 暗黙的フロー
  • シークレット キーを使用した認証コード権限付与タイプ
  • PKCE コードの課題と検証ツール

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

暗黙的フロー:

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

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  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&#60;string, string&#62; = {
    client_id: GITHUB_CLIENT_ID!,
    response_type: 'code',
  }
  const response = await extensionSDK.oauth2Authenticate(
    'https://github.com/login/oauth/authorize',
    authenticateParameters,
   'GET'
  )
  const exchangeParameters: Record&#60;string, string&#62; = {
    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 { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

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

. . .

  const authRequest: Record&#60;string, string&#62; = {
    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&#60;string, string&#62; = {
    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 は、指定されたユーザーのセットに拡張機能(拡張機能のみ)を公開する環境として Looker インスタンスを使用する方法を指します。Looker インスタンスに移動する Spartan ユーザーは、Looker 管理者が構成したログインフローと一緒に表示されます。ユーザーが認証されると、次に示すように、landing_page ユーザー属性に応じて拡張機能がユーザーに表示されます。ユーザーは拡張機能のみにアクセスできます。Looker の他の部分にはアクセスできません。ユーザーが複数の拡張機能にアクセスできる場合、これらの拡張機能は extensionSDK.updateLocation を使用してユーザーが他の拡張機能に移動できるかどうかを制御します。ユーザーが Looker インスタンスからログアウトできるようにする Looker Extension SDK メソッドが 1 つあります。

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

. . .

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

. . .
  // Navigate to another extension
  extensionSDK.updateLocation('/spartan/another::extension')

. . .
  // Logout
  extensionSDK.spartanLogout()

Spartan ユーザーの定義

簡潔ユーザーを定義するには、「Extensions Only」というグループを作成する必要があります

「Extensions Only」グループが作成されたら、Looker の [Admin] セクションの [User Attributes] ページに移動して、landing_page ユーザー属性を編集します。[Group Values] タブを選択し、「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 としてエクスポートする必要があります。また、インポートする関数に副作用がないようにしてください。Looker SDK for TypeScript/JavaScriptLooker SDK ランタイム ライブラリLooker UI コンポーネントLooker Extension SDKExtension SDK for React はすべて、これらの要件を満たしています。

拡張機能では、Looker SDK 4.0 を使用し、Extension SDK for ReactExtensionProvider2 コンポーネントまたは ExtensionProvider40 コンポーネントを使用します。

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

import { MyExtension } from './MyExtension'
import { ExtensionProvider40 } 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 のページビューです。シーンは主要なルートにマッピングされます。シーンに、主要ルート内のサブルートにマッピングされる子シーンが含まれている場合があります。
  • トランスパイル - 1 つの言語で書かれたソースコードを、類似した抽象化レベルの別の言語に変換するプロセス。たとえば、TypeScript から JavaScript への変換などです。