Exemplos de código JavaScript e React do framework de extensão

Esta página mostra exemplos de código escritos em React e JavaScript para funções comuns que podem ser usadas nas suas extensões.

Como usar o SDK da extensão do Looker

As extensões precisam estabelecer uma conexão com o host do Looker. No React, isso é feito unindo a extensão a um componente ExtensionProvider40. Esse componente estabelece uma conexão com o host do Looker e disponibiliza o SDK da extensão do Looker e o SDK do Looker para a 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>
 )
}

Informações gerais sobre provedores de extensão

Os provedores de extensões expõem o SDK da extensão do Looker e a API SDK às extensões. Versões diferentes do provedor de extensão foram criadas desde que o framework de extensão foi criado. Esta seção explica o histórico dos provedores de extensão e por que o ExtensionProvider40 é o provedor recomendado.

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

ExtensionProvider2 foi criado. Isso foi criado porque não faz sentido para uma extensão usar os dois SDKs e forçar o desenvolvedor a escolher um ou outro. Infelizmente, isso ainda fez com que os dois SDKs fossem incluídos no tamanho do pacote de produção final.

Quando o SDK 4.0 foi movido para a GA, a ExtensionProvider40 foi criada. A vantagem da ExtensionProvider40 é que o desenvolvedor não precisa escolher qual 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, a vantagem dessa opção é reduzir o tamanho do pacote.

Para adicionar funções do SDK da extensão do Looker, primeiro você precisa acessar uma referência ao SDK, o que pode ser feito no provedor ou globalmente. Em seguida, você pode chamar as funções do SDK como faria em qualquer aplicativo JavaScript.

  • Para acessar o SDK do provedor, siga estas etapas:
  import { ExtensionContext40 } from '@looker/extension-sdk-react'

  export const Comp1 = () => {
    const extensionContext = useContext(
      ExtensionContext40
    )
    const { extensionSDK, coreSDK } = extensionContext
  • Para acessar o SDK globalmente (a extensão precisa ser inicializada antes de ser chamada), siga estas etapas:
    const coreSDK = getCoreSDK()

Agora você pode usar o SDK como faria em qualquer aplicativo JavaScript:

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

Como a extensão é executada em um iframe isolado, não é possível navegar para outro lugar na instância do Looker atualizando o objeto window.location do pai. É possível navegar usando o SDK da extensão Looker.

Essa função requer o direito navigation.

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

. . .

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

. . .

  extensionSDK.updateLocation('/browse')

Como abrir uma nova janela do navegador

Como a extensão é executada em um iframe em sandbox, você não pode usar a janela principal para abrir uma nova janela do navegador. É possível abrir uma janela do navegador usando o SDK da extensão do Looker.

Essa função exige que o direito de new_window abra uma nova janela em um local na instância atual do Looker ou o direito de new_window_external_urls para abrir uma nova janela executada em um host 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')

Roteamento e links diretos

As informações a seguir se aplicam a extensões baseadas em React.

Os componentes ExtensionProvider, ExtensionProvider2 e ExtensionProvider40 criam automaticamente um React Router chamado MemoryRouter para você usar. Não tente criar um BrowserRouter, porque ele não funciona em iframes em sandbox. Não tente criar um HashRouter, porque ele não funciona em iframes em sandbox para a versão não baseada no Chromium do navegador Microsoft Edge.

Se o MemoryRouter for usado e você usar react-router na extensão, o framework de extensão vai sincronizar automaticamente o roteador da extensão com o roteador do host do Looker. Isso significa que a extensão será notificada quando o navegador clicar no botão para voltar e avançar e sobre a rota atual quando a página for atualizada. Isso também significa que a extensão será compatível automaticamente com links diretos. Consulte os exemplos de extensão para saber como usar a react-router.

Dados de contexto da extensão

Os dados de contexto do framework de extensão não podem ser confundidos com contextos do React.

As extensões podem compartilhar dados de contexto entre todos os usuários de uma extensão. Os dados de contexto podem ser usados para dados que não mudam com frequência e não têm requisitos de segurança especiais. Tenha cuidado ao gravar os dados, porque não há bloqueio de dados e a última gravação prevalece. Os dados de contexto ficam disponíveis para a extensão imediatamente após a inicialização. O SDK da extensão do Looker oferece funções para permitir que os dados de contexto sejam atualizados.

O tamanho máximo dos dados de contexto é de aproximadamente 16 MB. Os dados de contexto serão serializados em uma string JSON. Isso também precisa ser levado em consideração se você usar dados de contexto para 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 usuário

O SDK da extensão do Looker fornece uma API para acessar os atributos do usuário do Looker. Há dois tipos de acesso a atributos de usuário:

  • Escopo: associado à extensão. Um atributo de usuário com escopo recebe um namespace para a extensão, e o atributo do usuário precisa ser definido na instância do Looker antes de ser usado. Para criar um namespace para um atributo de usuário, acrescente um prefixo ao nome do atributo com o nome da extensão. Qualquer traço e ":" no nome da extensão precisam ser substituídos por um sublinhado, já que traços e dois-pontos não podem ser usados em nomes de atributos do usuário.

    Por exemplo, um atributo de usuário com escopo chamado my_value usado com um ID de extensão de my-extension::my-extension precisa ter um nome de atributo de usuário definido como my_extension_my_extension_my_value. Depois de definido, o atributo do usuário pode ser lido e atualizado pela extensão.

  • Global: são atributos de usuário globais e somente leitura. Um exemplo é o atributo de usuário locale.

Esta é uma lista de chamadas de API de atributos do usuário:

  • userAttributeGetItem: lê um atributo do usuário. Um valor padrão pode ser definido e será usado se um valor de atributo do usuário não existir para o usuário.
  • userAttributeSetItem: salva um atributo de usuário para o usuário atual. Ocorrerá falha para atributos globais do usuário. O valor salvo só fica visível para o usuário atual.
  • userAttributeResetItem: redefine um atributo do usuário atual para o valor padrão. Ocorrerá falha para atributos globais do usuário.

Para acessar os atributos do usuário, especifique os nomes dos atributos nos direitos global_user_attributes e/ou scoped_user_attributes. Por exemplo, no arquivo de manifesto do projeto do LookML, você 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 acesso ao armazenamento local do navegador. O SDK de extensão do Looker permite que uma extensão leia e grave no armazenamento local da janela mãe. O armazenamento local recebe um namespace para a extensão, o que significa que ele não pode ler o armazenamento local criado pela janela pai ou por outras extensões.

O uso do armazenamento local requer o direito local_storage.

A API de extensão localhost é assíncrona em comparação com a API síncrona de armazenamento local do navegador.

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

Como atualizar o título da página

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

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

. . .

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

  extensionSDK.updateTitle('My Extension Title')

Gravar na área de transferência do sistema

Os iframes em sandbox não permitem acesso à área de transferência do sistema. O SDK da extensão do Looker permite que uma extensão grave texto no Clipboard do sistema. Por motivos de segurança, a extensão não tem permissão para ler dados da área de transferência do sistema.

Para gravar na área de transferência do sistema, você precisa do direito 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, Looks e análises

O framework de extensão oferece suporte à incorporação de dashboards, Looks e Explores.

O direito use_embeds é obrigatório. Recomendamos usar o SDK de incorporação JavaScript do Looker para incorporar conteúdo. Consulte a documentação do SDK incorporado para obter 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ão usam componentes estilizados para fornecer um estilo simples ao iframe gerado. Exemplo:

import styled from "styled-components"

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

Como acessar endpoints de API externos

O framework de extensão oferece dois métodos para acessar endpoints de API externos:

  • O proxy do servidor: acessa o endpoint pelo servidor do Looker. Esse mecanismo permite que IDs do cliente e chaves secretas sejam definidos com segurança pelo servidor do Looker.
  • O proxy de busca: acessa o ponto de extremidade no navegador do usuário. O proxy é a interface do Looker.

Em ambos os casos, é necessário especificar o endpoint de API externo no direito external_api_urls da extensão.

Proxy do servidor

O exemplo a seguir demonstra o uso do proxy do servidor para receber um token de acesso para uso pelo proxy de busca. O ID e o segredo do cliente precisam ser definidos como atributos do usuário para a extensão. Normalmente, quando o atributo do usuário é configurado, o valor padrão é definido como o ID ou a chave secreta do cliente.

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 usuário precisa ser mapeado para a extensão. Os traços precisam ser substituídos por sublinhados, e os caracteres :: precisam ser substituídos por um único sublinhado.

Por exemplo, se o nome da sua extensão for my-extension::my-extension, os atributos de usuário que precisam ser definidos para o exemplo anterior serão os seguintes:

my_extension_my_extension_my_client_id
my_extension_my_extension_'my_client_secret'

Buscar proxy

O exemplo a seguir demonstra o uso do proxy de busca. Ele usa o token de acesso do exemplo anterior de proxy do servidor.

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 com OAuth

O framework de extensão é compatível com a integração com provedores OAuth. O OAuth pode ser usado para conseguir um token de acesso e acessar um recurso específico, como um documento das Planilhas Google.

Você precisará especificar o endpoint do servidor OAuth no direito extension oauth2_urls. Talvez também seja necessário especificar outros URLs no direito external_api_urls.

Os frameworks de extensão oferecem suporte aos seguintes fluxos:

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

No fluxo geral, é aberta uma janela filha que carrega uma página do servidor OAuth. O servidor OAuth autentica o usuário e redireciona para o servidor do Looker com mais detalhes que podem ser usados para conseguir 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 de 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 de 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

Spartan

Spartan é um método de usar a instância do Looker como um ambiente para expor extensões (e apenas extensões) a um conjunto designado de usuários. Um usuário básico acessando uma instância do Looker vai receber o fluxo de login configurado pelo administrador do Looker. Depois que o usuário for autenticado, uma extensão será apresentada de acordo com o atributo do usuário landing_page, conforme mostrado a seguir. O usuário só pode acessar extensões. elas não poderão acessar nenhuma outra parte do Looker. Se o usuário tiver acesso a várias extensões, elas vão controlar a capacidade de navegar para as outras extensões usando extensionSDK.updateLocation. Há um método específico do SDK da extensão do Looker para permitir que o usuário saia da 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()

Como definir usuários espartanos

Para definir um usuário Spartan, é necessário criar um grupo chamado "Somente extensões".

Depois que a opção "Somente extensões" acesse a página Atributos do usuário na seção Administrador do Looker e edite o atributo de usuário landing_page. Selecione a guia Valores do grupo e adicione o grupo "Apenas extensões". O valor precisa ser definido como /spartan/my_extension::my_extension/, em que my_extension::my_extension é o ID da sua extensão. Agora, quando esse usuário fizer login, ele será roteado para a extensão designada.

Divisão de código

A divisão de código é a técnica em que o código é solicitado apenas quando necessário. Normalmente, os blocos de código são associados a rotas do React, em que cada rota recebe o próprio bloco. No React, isso é feito com os componentes Suspense e React.lazy. O componente Suspense exibe um componente substituto enquanto o bloco de código é carregado. O React.lazy é responsável por carregar o bloco de código.

Como configurar a divisão de 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 de maneira lenta é 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 maneira. O componente precisa ser exportado como um componente padrão:

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

export default Comp1

Árvores balançando

Embora os SDKs do Looker ofereçam suporte ao tree shaking atualmente, essa função ainda precisa ser melhorada. Estamos sempre modificando nossos SDKs para melhorar o suporte ao recurso de agitar o dispositivo. Algumas dessas alterações podem exigir que você refatore o código para ter vantagem, mas quando isso for necessário, elas serão documentadas nas notas da versão.

Para usar o tree-shaking, o módulo usado precisa ser exportado como um esmodule, e as funções importadas não podem ter efeitos colaterais. O SDK do Looker para TypeScript/Javascript, a biblioteca de execução do SDK do Looker, os componentes da interface do Looker, o SDK da extensão do Looker e o SDK da extensão para React atendem a esses requisitos.

Em uma extensão, use o SDK do Looker 4.0 e o componente ExtensionProvider2 ou ExtensionProvider40 do SDK da extensão para React.

O código a seguir configura o provedor de extensão. Será necessário informar ao provedor qual SDK você 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 traz tudo que está no módulo. Em vez disso, importe apenas os componentes necessários. Exemplo:

import { Paragraph }  from `@looker/components`

Glossário

  • Divisão de código: uma técnica para o carregamento lento de JavaScript até que ele seja realmente necessário. O ideal é manter o pacote JavaScript carregado inicialmente o menor possível. Isso pode ser feito com a divisão de código. Qualquer funcionalidade que não seja imediatamente necessária não é carregada até que seja realmente necessária.
  • IDE: ambiente de desenvolvimento integrado. Um editor usado para criar e modificar uma extensão. Por exemplo, o Visual Studio Code, o IntelliJ e o WebStorm.
  • Cena: geralmente uma visualização de página no Looker. As cenas são associadas às rotas principais. Às vezes, uma cena inclui cenas infantis mapeadas para subtrajetos dentro do trajeto principal.
  • Transpilação: o processo de pegar o código-fonte escrito em uma linguagem e transformá-lo em outra com um nível de abstração semelhante. Um exemplo é TypeScript para JavaScript.