Exemples de code React et JavaScript du framework d'extension

Le framework de l'extension Looker utilisera bientôt un nouveau mécanisme de chargement. Le nouveau chargeur peut générer des erreurs lors du chargement des extensions existantes. Pour savoir comment tester vos extensions avec le nouveau chargeur avant de l'activer officiellement dans les environnements Looker, consultez l'article Tester le nouveau chargeur de framework d'extensions dans le centre d'aide Looker.

Cette page fournit des exemples de code écrits dans React et JavaScript pour les fonctions courantes que vous pouvez utiliser dans vos extensions.

Utiliser le SDK d'extension Looker

Pour ajouter des fonctions à partir du SDK Extension Looker, vous devez d'abord obtenir une référence au SDK, qui peut être effectuée par le fournisseur ou à l'échelle mondiale. Vous pouvez ensuite appeler les fonctions du SDK comme vous le feriez dans n'importe quelle application JavaScript.

  • Pour accéder au SDK à partir du fournisseur:
  import { ExtensionContext2 } from '@looker/extension-sdk-react'

  export const Comp1 = () => {
    const extensionContext = useContext(
      ExtensionContext2
    )
    const { extensionSDK, coreSDK } = extensionContext
  • Pour accéder au SDK à l'échelle mondiale (l'extension doit être initialisée avant d'être appelée):
    const coreSDK = getCoreSDK2()

Vous pouvez maintenant utiliser le SDK comme dans n'importe quelle application JavaScript:

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

Étant donné que l'extension s'exécute dans un iFrame en bac à sable, vous ne pouvez pas naviguer ailleurs dans l'instance Looker en mettant à jour l'objet window.location du parent. Vous pouvez naviguer à l'aide du SDK Extension Looker.

Cette fonction nécessite le droit d'accès navigation.

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

. . .

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

. . .

  extensionSDK.updateLocation('/browse')

Ouverture d'une nouvelle fenêtre de navigateur

Étant donné que l'extension s'exécute dans un iFrame en bac à sable, vous ne pouvez pas utiliser la fenêtre parente pour ouvrir une nouvelle fenêtre de navigateur. Il est possible d'ouvrir une fenêtre de navigateur à l'aide du SDK Looker Extension.

Cette fonction nécessite le droit d'accès new_window pour ouvrir une nouvelle fenêtre vers un emplacement dans l'instance Looker actuelle, ou le droit d'accès new_window_external_urls pour ouvrir une nouvelle fenêtre qui s'exécute sur un autre hôte.

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

Routage et liens profonds

Les informations suivantes s'appliquent aux extensions basées sur React.

Les composants ExtensionProvider et ExtensionProvider2 créent automatiquement un routeur React appelé MemoryRouter que vous pouvez utiliser. N'essayez pas de créer un BrowserRouter, car il ne fonctionne pas dans des cadres iFrame en bac à sable. N'essayez pas de créer un HashRouter, car il ne fonctionne pas dans des cadres iFrame en bac à sable pour la version non-Chromium du navigateur Microsoft Edge.

Si vous utilisez MemoryRouter et que vous utilisez react-router dans votre extension, le framework de l'extension synchronise automatiquement le routeur de votre extension avec le routeur hôte Looker. Cela signifie que l'extension sera informée des clics sur le bouton "Précédent" et "Suivant" du navigateur lorsque la page sera actualisée. Cela signifie également que l'extension doit automatiquement accepter les liens profonds. Consultez les exemples d'extensions pour savoir comment utiliser react-router.

Données de contexte de l'extension

Les données de contexte du framework d'extension ne doivent pas être confondues avec les contextes React.

Les extensions peuvent partager des données de contexte entre tous les utilisateurs d'une extension. Les données de contexte peuvent être utilisées pour les données qui ne changent pas fréquemment et qui n'ont pas d'exigences de sécurité particulières. Il convient d'être prudent lors de l'écriture des données, car il n'y a pas de verrouillage des données et la dernière écriture l'emporte. Les données de contexte sont disponibles pour l'extension immédiatement au démarrage. Le SDK d'extension Looker fournit des fonctions permettant de mettre à jour et d'actualiser les données de contexte.

La taille maximale des données de contexte est d'environ 16 Mo. Les données de contexte seront sérialisées dans une chaîne JSON. Vous devez donc également en tenir compte si vous utilisez les données de contexte pour votre 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()

Attributs utilisateur

Le SDK d'extension Looker fournit une API pour accéder aux attributs utilisateur de Looker. Il existe deux types d'accès aux attributs utilisateur:

  • Portée : associée à l'extension. Un attribut utilisateur restreint est associé à un espace de noms dans l'extension, et il doit être défini dans l'instance Looker pour pouvoir être utilisé. Pour ajouter un attribut utilisateur à un espace de noms, ajoutez le nom de l'extension en tant que préfixe. Tous les tirets et les caractères "'::&#39" du nom de l'extension doivent être remplacés par un trait de soulignement, car les tirets et les deux-points ne peuvent pas être utilisés dans les noms d'attributs utilisateur.

    Par exemple, un attribut utilisateur restreint nommé my_value et associé à l'ID d'extension my-extension::my-extension doit être associé au nom d'attribut utilisateur my_extension_my_extension_my_value. Une fois défini, l'extension utilisateur peut lire et mettre à jour l'attribut utilisateur.

  • Global : ces attributs globaux sont en lecture seule. Prenons l'exemple de l'attribut utilisateur locale.

Voici la liste des appels d'API des attributs utilisateur:

  • userAttributeGetItem : lit un attribut utilisateur. Une valeur par défaut peut être définie et sera utilisée si aucune valeur d'attribut n'existe pour l'utilisateur.
  • userAttributeSetItem : enregistre un attribut utilisateur pour l'utilisateur actuel. Échec pour les attributs utilisateur généraux. Seul l'utilisateur actuel peut voir la valeur enregistrée.
  • userAttributeResetItem : rétablit la valeur par défaut d'un attribut utilisateur pour l'utilisateur actuel. Échec pour les attributs utilisateur généraux.

Pour accéder aux attributs utilisateur, vous devez spécifier les noms d'attributs dans les droits d'accès global_user_attributes et/ou scoped_user_attributes. Par exemple, dans le fichier manifeste de projet LookML, vous devez ajouter:

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

Stockage en local

Les cadres iFrame en bac à sable ne permettent pas l'accès au stockage local du navigateur. Le SDK d'extension Looker permet à une extension de lire et d'écrire dans l'espace de stockage local de la fenêtre parente. Le stockage local est associé à un espace de noms pour l'extension, ce qui signifie qu'il ne peut pas lire le stockage local créé par la fenêtre parente ou d'autres extensions.

Pour utiliser le stockage local, vous devez disposer du droit d'accès local_storage.

L'API localhost (extension) est asynchrone par opposition à l'API synchrone de stockage en local du navigateur.

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

Mettre à jour le titre d'une page

Les extensions peuvent modifier le titre de la page actuelle. Aucun droit d'accès n'est requis pour effectuer cette action.

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

. . .

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

  extensionSDK.updateTitle('My Extension Title')

Écriture dans le presse-papiers du système

Les iFrame en bac à sable ne permettent pas d'accéder au presse-papiers du système. Le SDK d'extension Looker permet à une extension d'écrire du texte dans le presse-papiers du système. Pour des raisons de sécurité, l'extension n'est pas autorisée à lire depuis le presse-papiers du système.

Pour écrire dans le presse-papiers du système, vous devez disposer du droit d'accès 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) {
      . . .
    }

Intégration de tableaux de bord, de styles et d'explorations

Le framework des extensions permet d'intégrer des tableaux de bord, des looks et des explorations. Les tableaux de bord standards et les anciens tableaux de bord peuvent être intégrés.

Le droit d'accès use_embeds est requis. Nous vous recommandons d'utiliser le SDK Embed JavaScript de Looker pour intégrer du contenu. Pour en savoir plus, consultez la documentation du SDK Embed.

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

Les exemples d'extensions utilisent des composants stylisés pour fournir un style simple à l'iFrame généré. Exemple :

import styled from "styled-components"

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

Accéder aux points de terminaison de l'API externe

Le framework d'extension fournit deux méthodes pour accéder aux points de terminaison d'API externes:

  • Proxy de serveur : accède au point de terminaison via le serveur Looker. Ce mécanisme permet de définir des ID client et des clés secrètes de manière sécurisée par le serveur Looker.
  • Proxy de récupération : accède au point de terminaison depuis le navigateur de l'utilisateur. Le proxy est l'interface utilisateur de Looker.

Dans les deux cas, vous devez spécifier le point de terminaison de l'API externe dans le droit d'accès external_api_urls de l'extension.

Proxy de serveur

L'exemple suivant illustre l'utilisation du proxy du serveur pour obtenir un jeton d'accès qui sera utilisé par le proxy d'extraction. L'ID client et le code secret doivent être définis en tant qu'attributs utilisateur pour l'extension. En règle générale, lorsque l'attribut utilisateur est configuré, la valeur par défaut correspond à l'ID ou au code secret du client.

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

Le nom de l'attribut utilisateur doit être mappé à l'extension. Les tirets doivent être remplacés par des traits de soulignement et les caractères :: par un seul trait de soulignement.

Par exemple, si le nom de votre extension est my-extension::my-extension, les attributs utilisateur à définir pour l'exemple ci-dessus sont les suivants:

my_extension_my_extension_my_client_id
my_extension_my_extension_'my_client_secret'

Récupérer le proxy

L'exemple suivant illustre l'utilisation du proxy d'exploration. Il utilise le jeton d'accès de l'exemple de proxy de serveur précédent.

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

. . .

  }

Intégration OAuth

Le framework d'extension est compatible avec les fournisseurs OAuth. OAuth peut être utilisé pour obtenir un jeton d'accès afin d'accéder à une ressource particulière, par exemple un document Google Sheets.

Vous devez spécifier le point de terminaison du serveur OAuth dans le droit d'accès extension oauth2_urls. Vous devrez peut-être également indiquer des URL supplémentaires dans le droit d'accès external_api_urls.

Les frameworks d'extension sont compatibles avec les flux suivants:

  • Flux implicite
  • Type d'attribution du code d'autorisation avec clé secrète
  • Test et validation du code PKCE

En général, une fenêtre enfant s'ouvre et charge une page de serveur OAuth. Le serveur OAuth authentifie l'utilisateur et le redirige vers le serveur Looker. Des informations supplémentaires permettent d'obtenir un jeton d'accès.

Flux implicite:

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

Type d'attribution du code d'autorisation avec clé secrète:

  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

Test et validation du code 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

"Spartan" fait référence à une méthode qui consiste à utiliser l'instance Looker comme environnement pour exposer des extensions (et des extensions uniquement) à un ensemble d'utilisateurs désigné. Un utilisateur spartout qui accède à une instance Looker se verra présenter le flux de connexion que l'administrateur Looker a configuré. Une fois l'utilisateur authentifié, une extension lui est présentée en fonction de son attribut utilisateur landing_page, comme indiqué ci-dessous. L'utilisateur peut uniquement accéder aux extensions. Il ne peut accéder à aucune autre partie de Looker. Si l'utilisateur a accès à plusieurs extensions, celles-ci contrôlent la possibilité pour l'utilisateur d'accéder aux autres extensions à l'aide de extensionSDK.updateLocation. Il existe une méthode spécifique pour le SDK de l'extension Looker, qui permet à l'utilisateur de se déconnecter de l'instance Looker.

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

Définir des utilisateurs spartouts

Pour définir un utilisateur spartian, vous devez créer un groupe appelé "Extensions uniquement".

Une fois le groupe "Extensions uniquement créé, accédez à la page Attributs utilisateur de la section Admin de Looker, puis modifiez l'attribut utilisateur landing_page. Sélectionnez l'onglet Valeurs du groupe et ajoutez le groupe "Extensions uniquement". La valeur doit être définie sur /spartan/my_extension::my_extension/, où my_extension::my_extension correspond à l'ID de votre extension. À présent, lorsque l'utilisateur se connecte, il est redirigé vers l'extension désignée.

Diviser le code

La division du code est une technique qui demande du code uniquement lorsqu'il est nécessaire. Généralement, les fragments de code sont associés à des routes React, où chaque route obtient son propre fragment de code. Dans React, c'est possible avec les composants Suspense et React.lazy. Le composant Suspense affiche un composant de remplacement pendant le chargement du fragment de code. React.lazy est responsable du chargement du fragment de code.

Configurer la répartition du code:

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>

Le composant à chargement différé est implémenté comme suit:

import { lazy } from 'react'

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

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

Le composant est implémenté comme suit. Le composant doit être exporté en tant que composant par défaut:

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

export default Comp1

Tremblements d'arbres

Les SDK Looker permettent de secouer les arbres, mais ils ne sont pas encore parfaits. Nous modifions en permanence nos SDK pour améliorer la prise en charge des secousses d'arbres. Certaines de ces modifications peuvent nécessiter une refactorisation pour votre code, mais lorsque cela est nécessaire, elles sont documentées dans les notes de version.

Pour utiliser le secouement des arbres, vous devez exporter le module en tant qu'esmodule, et les fonctions que vous importez ne doivent pas comporter d'effets secondaires. C'est ce que font les SDK Looker pour TypeScript/Javascript, la bibliothèque d'exécution du SDK Looker, les composants de l'interface utilisateur Looker, le SDK Extension Looker et le SDK Extension pour React.

Dans une extension, vous devez choisir l'un des SDK 3.1 ou 4.0 de Looker, et utiliser le composant ExtensionProvider2 du SDK Extension pour React. Si vous avez besoin des deux SDK, continuez à utiliser le composant ExtensionProvider, mais vous constaterez une augmentation de la taille du bundle final.

Le code suivant configure le fournisseur d'extensions. Vous devez indiquer au fournisseur quel SDK vous voulez:

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

N'utilisez pas le style d'importation suivant dans votre extension:

import * as lookerComponents from `@looker/components`

L'exemple ci-dessus rassemble tout le contenu du module. Importez plutôt uniquement les composants dont vous avez réellement besoin. Exemple :

import { Paragraph }  from `@looker/components`

Glossaire

  • Répartition du code : technique de chargement différé du code JavaScript jusqu'à ce qu'elle soit réellement nécessaire. Idéalement, vous devez faire en sorte que le groupe JavaScript initialement chargé soit le plus petit possible. Pour ce faire, utilisez la division du code. Les fonctionnalités qui ne sont pas immédiatement requises ne sont pas chargées tant qu'elles ne sont pas réellement nécessaires.
  • IDE : environnement de développement intégré Éditeur utilisé pour créer et modifier une extension. Par exemple, Visual Studio Code, Intellij ou WebStorm.
  • Scène : généralement une page vue dans Looker. Les scènes correspondent à des itinéraires principaux. Parfois, une scène comporte des scènes enfants qui correspondent à des sous-itinéraires sur l'itinéraire principal.
  • Transpile : processus consistant à prendre le code source écrit dans un langage et à le transformer dans un autre langage présentant un niveau d'abstraction similaire. Exemple : TypeScript to JavaScript.