拡張フレームワークの 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 拡張機能 SDK と SDK API を拡張機能に公開します。拡張機能フレームワークの作成後に、さまざまなバージョンの拡張機能プロバイダが作成されています。このセクションでは、拡張機能プロバイダの歴史と、ExtensionProvider40 が推奨されるプロバイダについて説明します。

最初の拡張プロバイダは、Looker SDK バージョン 3.1 と 4.0 の両方を公開した ExtensionProvider でした。欠点は、両方の SDK を含めると最終的な本番環境バンドルのサイズが大きくなることです。

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

SDK 4.0 が GA に移行したとき、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 を自動的に作成します。サンドボックス化された iframe では機能しないため、BrowserRouter は作成しないでください。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 Embedded SDK を使用することをおすすめします。詳しくは、埋め込み 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 によってクライアント 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
    . . .
  }

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

たとえば、拡張子の名前が 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 の変更を絶えず行っています。一部の変更では、コードを利用するためにリファクタリングする必要がありますが、必要な場合はリリースノートに記載されています。

ツリー シェイキングを利用するには、使用するモジュールをエスモジュールとしてエクスポートする必要があります。また、インポートする関数には副作用がないようにしてください。TypeScript/JavaScript 用 Looker SDKLooker SDK ランタイム ライブラリLooker UI コンポーネントLooker Extension SDK Extension 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 to JavaScript などです。