Exemplos de código JavaScript e React da framework de extensões

Esta página oferece exemplos de código escritos em React e JavaScript para funções comuns que pode querer usar nas suas extensões.

Usar o SDK de extensões do Looker

As extensões têm de estabelecer uma ligação com o anfitrião do Looker. No React, isto é feito envolvendo a extensão num componente ExtensionProvider40. Este componente estabelece uma ligação com o anfitrião do Looker e disponibiliza o SDK de extensões do Looker e o SDK do Looker à extensão.

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


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

Contexto sobre fornecedores de extensões

Os fornecedores de extensões expõem o SDK de extensões do Looker e a API do SDK às extensões. Foram criadas diferentes versões do fornecedor de extensões desde que a estrutura de extensões foi criada. Esta secção explica o histórico dos fornecedores de extensões e por que motivo o ExtensionProvider40 é o fornecedor recomendado.

O primeiro fornecedor de extensões foi ExtensionProvider, que expôs os dois SDKs do Looker, as versões 3.1 e 4.0. A desvantagem era que a inclusão de ambos os SDKs aumentava o tamanho do pacote de produção final.

Em seguida, foi criado o grupo ExtensionProvider2. Esta foi criada porque não fazia sentido uma extensão usar ambos os SDKs e obrigar o programador a escolher um ou outro. Infelizmente, isto resultou na inclusão de ambos os SDKs no tamanho do pacote de produção final.

Quando o SDK 4.0 passou para a versão GA, foi criado o ExtensionProvider40. A vantagem do ExtensionProvider40 é que o programador não tem de escolher que SDK usar, porque o SDK 4.0 é a única versão disponível. Como o SDK 3.1 não está incluído no pacote final, isto tem a vantagem de reduzir o tamanho do pacote.

Para adicionar funções do SDK de extensões do Looker, primeiro, tem de obter uma referência ao SDK, o que pode ser feito a partir do fornecedor ou globalmente. Em seguida, pode chamar funções do SDK como faria em qualquer aplicação JavaScript.

  • Para aceder ao SDK a partir do fornecedor, siga estes passos:
  import { ExtensionContext40 } from '@looker/extension-sdk-react'

  export const Comp1 = () => {
    const extensionContext = useContext(
      ExtensionContext40
    )
    const { extensionSDK, coreSDK } = extensionContext
  • Para aceder ao SDK globalmente (a extensão tem de ser inicializada antes de ser chamada), siga estes passos:
    const coreSDK = getCoreSDK()

Agora, pode usar o SDK como faria em qualquer aplicação JavaScript:

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

Uma vez que a extensão é executada numa iFrame em sandbox, não pode navegar para outro local na instância do Looker atualizando o objeto window.location do elemento principal. É possível navegar através do SDK de extensões do Looker.

Esta função requer a autorização navigation.

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

. . .

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

. . .

  extensionSDK.updateLocation('/browse')

Abrir uma nova janela do navegador

Uma vez que a extensão é executada num iFrame em sandbox, não pode usar a janela principal para abrir uma nova janela do navegador. É possível abrir uma janela do navegador através do SDK de extensões do Looker.

Esta função requer a autorização new_window para abrir uma nova janela para uma localização na instância atual do Looker ou a autorização new_window_external_urls para abrir uma nova janela que é executada num anfitrião diferente.

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

Encaminhamento e links diretos

O seguinte aplica-se a extensões baseadas em React.

Os componentes ExtensionProvider, ExtensionProvider2 e ExtensionProvider40 criam automaticamente um React Router denominado MemoryRouter para sua utilização. Não tente criar um BrowserRouter, uma vez que não funciona em iFrames em sandbox. Não tente criar um HashRouter, uma vez que não funciona em iFrames no sandbox para a versão do navegador Microsoft Edge não baseada no Chromium.

Se o MemoryRouter for usado e usar o react-router na extensão, a framework de extensões sincroniza automaticamente o router da extensão com o router do anfitrião do Looker. Isto significa que a extensão vai receber uma notificação dos cliques nos botões anterior e seguinte do navegador, bem como da rota atual, quando a página for recarregada. Isto também significa que a extensão deve suportar automaticamente links diretos. Consulte os exemplos de extensões para saber como usar react-router.

Dados de contexto das extensões

Os dados de contexto da framework de extensões não devem ser confundidos com os contextos do React.

As extensões têm a capacidade de partilhar dados de contexto entre todos os utilizadores de uma extensão. Os dados de contexto podem ser usados para dados que não mudam com frequência e que não têm requisitos de segurança especiais. Deve ter cuidado ao escrever os dados, uma vez que não existe bloqueio de dados e a última escrita é a que prevalece. Os dados de contexto estão disponíveis para a extensão imediatamente após o arranque. O SDK de extensões do Looker fornece funções para permitir que os dados de contexto sejam atualizados e renovados.

O tamanho máximo dos dados de contexto é de aproximadamente 16 MB. Os dados de contexto são serializados numa string JSON, pelo que também tem de ter isso em conta se usar dados de contexto para a sua extensão.

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

Atributos do utilizador

O SDK de extensões do Looker fornece uma API para aceder aos atributos do utilizador do Looker. Existem dois tipos de acesso a atributos do utilizador:

  • Com âmbito: associado à extensão. Um atributo do utilizador com âmbito é associado ao espaço de nomes da extensão, e o atributo do utilizador tem de ser definido na instância do Looker antes de poder ser usado. Para definir um espaço de nomes para um atributo do utilizador, adicione o nome da extensão como prefixo do nome do atributo. Todos os traços e os carateres "::" no nome da extensão têm de ser substituídos por um sublinhado, uma vez que não é possível usar traços nem dois pontos nos nomes dos atributos do utilizador.

    Por exemplo: um atributo do utilizador no âmbito do utilizador denominado my_value usado com um ID de extensão de my-extension::my-extension tem de ter um nome de atributo do utilizador de my_extension_my_extension_my_value definido. Depois de definido, o atributo do utilizador pode ser lido e atualizado pela extensão.

  • Global: estes são atributos de utilizador globais e são só de leitura. Um exemplo é o atributo do utilizador locale.

Segue-se uma lista de chamadas da API de atributos do utilizador:

  • userAttributeGetItem — Lê um atributo do utilizador. Pode ser definido um valor predefinido, que é usado se não existir um valor de atributo do utilizador para o utilizador.
  • userAttributeSetItem: guarda um atributo do utilizador para o utilizador atual. Falha para atributos de utilizador globais. O valor guardado só é visível para o utilizador atual.
  • userAttributeResetItem: repõe um atributo do utilizador para o utilizador atual com o valor predefinido. Falha para atributos de utilizador globais.

Para aceder aos atributos do utilizador, tem de especificar os nomes dos atributos nas autorizações global_user_attributes e/ou scoped_user_attributes. Por exemplo, no ficheiro de manifesto do projeto do LookML, adicionaria:

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

Armazenamento local

Os iFrames em sandbox não permitem o acesso ao armazenamento local do navegador. O SDK de extensões do Looker permite que uma extensão leia e escreva no armazenamento local da janela principal. O armazenamento local tem um espaço de nomes para a extensão, o que significa que não pode ler o armazenamento local criado pela janela principal ou outras extensões.

A utilização do armazenamento local requer a autorização local_storage.

A API localhost da extensão é assíncrona, ao contrário da API de armazenamento local do navegador síncrona.

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

Atualizar o título da página

As extensões podem atualizar o título da página atual. Não são necessários direitos para realizar esta ação.

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

. . .

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

  extensionSDK.updateTitle('My Extension Title')

Escrever na área de transferência do sistema

Os iFrames colocados em sandbox não permitem o acesso à área de transferência do sistema. O SDK de extensões do Looker permite que uma extensão escreva texto na área de transferência do sistema. Por motivos de segurança, a extensão não tem autorização para ler a partir da área de transferência do sistema.

Para escrever na área de transferência do sistema, precisa da autorização 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) {
      . . .
    }

Incorporar painéis de controlo, Looks e Explores

A estrutura de extensões suporta a incorporação de painéis de controlo, Looks e explorações.

A autorização use_embeds é obrigatória. Recomendamos que use o SDK de incorporação JavaScript do Looker para incorporar conteúdo. Consulte a documentação do SDK de incorporação para mais informações.

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

Os exemplos de extensões usam componentes com estilos para fornecer estilos simples ao iFrame gerado. Por exemplo:

import styled from "styled-components"

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

Aceder a pontos finais de APIs externas

A framework de extensões oferece dois métodos para aceder a pontos finais de APIs externas:

  • O proxy do servidor: acede ao ponto final através do servidor do Looker. Este mecanismo permite que o servidor do Looker defina de forma segura os IDs de cliente e as chaves secretas.
  • O proxy de obtenção: acede ao ponto final a partir do navegador do utilizador. O proxy é a IU do Looker.

Em ambos os casos, tem de especificar o ponto final da API externa na autorização external_api_urls da extensão.

Proxy do servidor

O exemplo seguinte demonstra a utilização do proxy do servidor para obter um token de acesso para utilização pelo proxy de obtenção. O ID de cliente e o segredo têm de ser definidos como atributos do utilizador para a extensão. Normalmente, quando o atributo do utilizador está configurado, o valor predefinido é definido como o ID do cliente ou o segredo.

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

O nome do atributo do utilizador tem de ser mapeado para a extensão. Os travessões têm de ser substituídos por sublinhados e os carateres :: têm de ser substituídos por um único sublinhado.

Por exemplo, se o nome da sua extensão for my-extension::my-extension, os atributos do utilizador que têm de ser definidos para o exemplo anterior seriam os seguintes:

my_extension_my_extension_my_client_id
my_extension_my_extension_'my_client_secret'

Proxy de obtenção

O exemplo seguinte demonstra a utilização do proxy de obtenção. Usa o token de acesso do exemplo de proxy do servidor anterior.

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

. . .

  }

Integração do OAuth

A framework de extensões suporta a integração com fornecedores de OAuth. O OAuth pode ser usado para obter um token de acesso para aceder a um recurso específico, por exemplo, um documento do Google Sheets.

Tem de especificar o ponto final do servidor OAuth na autorização extension oauth2_urls. Também pode ter de especificar URLs adicionais na autorização external_api_urls.

As estruturas de extensões suportam os seguintes fluxos:

  • Fluxo implícito
  • Tipo de concessão com código de autorização com chave secreta
  • Desafio e verificador de código PKCE

O fluxo geral é que é aberta uma janela secundária que carrega uma página do servidor OAuth. O servidor OAuth autentica o utilizador e redireciona-o de volta para o servidor do Looker com detalhes adicionais que podem ser usados para obter um token de acesso.

Fluxo implícito:

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

Tipo de concessão com código de autorização com chave secreta:

  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

Desafio e verificador do código 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

Espartano

O Spartan refere-se a um método de utilização da instância do Looker como um ambiente para expor extensões, e apenas extensões, a um conjunto designado de utilizadores. Um utilizador do Spartan que navega para uma instância do Looker vê o fluxo de início de sessão que o administrador do Looker configurou. Assim que o utilizador for autenticado, é apresentada uma extensão ao utilizador de acordo com o respetivo landing_page atributo de utilizador, conforme mostrado em seguida. O utilizador só pode aceder a extensões e não pode aceder a nenhuma outra parte do Looker. Se o utilizador tiver acesso a várias extensões, as extensões controlam a capacidade do utilizador de navegar para as outras extensões através de extensionSDK.updateLocation. Existe um método específico do SDK de extensões do Looker para permitir que o utilizador termine sessão na instância do Looker.

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

Definir utilizadores espartanos

Para definir um utilizador espartano, tem de criar um grupo denominado "Apenas extensões".

Depois de criar o grupo "Apenas extensões", navegue para a página Atributos do utilizador na secção Administração do Looker e edite o landing_pageatributo do utilizador. Selecione o separador Valores de grupo e adicione o grupo "Apenas extensões". O valor deve ser definido como /spartan/my_extension::my_extension/, em que my_extension::my_extension é o ID da extensão. Agora, quando esse utilizador iniciar sessão, é encaminhado para a extensão designada.

Divisão de código

A divisão de código é a técnica em que o código só é pedido quando é necessário. Normalmente, os fragmentos de código estão associados a rotas do React, em que cada rota tem o seu próprio fragmento de código. No React, isto é feito com os componentes Suspense e React.lazy. O componente Suspense apresenta um componente alternativo enquanto o fragmento de código é carregado. React.lazy é responsável por carregar o fragmento de código.

Configuração para dividir o código:

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>

O componente carregado em diferido é implementado da seguinte forma:

import { lazy } from 'react'

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

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

O componente é implementado da seguinte forma. O componente tem de ser exportado como um componente predefinido:

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

export default Comp1

Agitar árvores

Embora os SDKs do Looker suportem atualmente a eliminação de código não utilizado, esta função ainda precisa de melhorias. Estamos continuamente a modificar os nossos SDKs para melhorar o suporte da eliminação de código não utilizado. Algumas destas alterações podem exigir que refatore o seu código para tirar partido das mesmas, mas, quando tal for necessário, será documentado nas notas de lançamento.

Para usar o tree-shaking, o módulo que usa tem de ser exportado como um esmodule e as funções que importa têm de estar livres de efeitos secundários. O SDK Looker para TypeScript/Javascript, a biblioteca de tempo de execução do SDK Looker, os componentes da IU do Looker, o SDK de extensão do Looker e o SDK de extensão para React cumprem todos estes requisitos.

Numa extensão, use o SDK Looker 4.0 e o componente ExtensionProvider2 ou ExtensionProvider40 do SDK de extensão para React.

O seguinte código configura o fornecedor de extensões. Tem de indicar ao fornecedor o SDK que quer:

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

Não use o seguinte estilo de importação na sua extensão:

import * as lookerComponents from `@looker/components`

O exemplo anterior importa tudo do módulo. Em vez disso, importe apenas os componentes de que realmente precisa. Por exemplo:

import { Paragraph }  from `@looker/components`

Glossário

  • Divisão de código: uma técnica para o carregamento lento de JavaScript até ser realmente necessário. Idealmente, deve manter o pacote JavaScript carregado inicialmente o mais pequeno possível. Isto pode ser conseguido através da divisão de código. Qualquer funcionalidade que não seja imediatamente necessária não é carregada até ser realmente necessária.
  • IDE: ambiente de programação integrado. Um editor usado para criar e modificar uma extensão. Alguns exemplos são o Visual Studio Code, o Intellij e o WebStorm.
  • Cena: geralmente, uma visualização de página no Looker. As cenas são mapeadas para trajetos principais. Por vezes, uma cena tem cenas secundárias que são mapeadas para subrotas dentro da rota principal.
  • Transpilação: o processo de usar código fonte escrito num idioma e transformá-lo noutro idioma com um nível de abstração semelhante. Um exemplo é a conversão de TypeScript para JavaScript.