Aracılığıyla paylaş


Dış kullanıcılar için ekleme nedir?

Önemli

Bu özellik Genel Önizleme aşamasındadır. Önizlemeler sayfasında önizleme kaydını onaylayabilirsiniz. Bkz. Azure Databricks önizlemelerini yönetme.

Bu sayfada dış kullanıcılar için eklemenin nasıl çalıştığı, azure Databricks çalışma alanınızı ekli panoların güvenli paylaşımı için yapılandırma ve başlamak için örnek uygulamaların nasıl kullanılacağı açıklanmaktadır. Dış kullanıcılar için ekleme, katıştırılmış panoların kimliğini doğrulamak ve erişimi yetkilendirmek için bir hizmet sorumlusu ve kapsamlı erişim belirteçleri kullanır. Bu yaklaşım, bu kullanıcılar için Azure Databricks hesapları sağlamadan panoları kuruluşunuzun dışındaki iş ortakları ve müşteriler gibi görüntüleyicilerle paylaşmanızı sağlar.

Kuruluşunuzdaki kullanıcılar için pano ekleme de dahil olmak üzere diğer ekleme seçenekleri hakkında bilgi edinmek için bkz. Pano ekleme.

Dış kullanıcılar için ekleme nasıl çalışır?

Aşağıdaki diyagram ve numaralandırılmış adımlar, dış kullanıcılar için bir pano eklediğinizde kullanıcıların kimliğinin nasıl doğrulandığını ve panoların kullanıcı kapsamlı sonuçlarla nasıl doldurulduğunu açıklar.

Uygulamanız ve Databricks çalışma alanı genelinde gerekli belirteç değişimlerini gösteren akış grafiği.

  1. Kullanıcı kimlik doğrulaması ve isteği: Kullanıcı uygulamanızda oturum açar. Uygulamanızın ön ucu, pano erişim belirteci için sunucunuza kimliği doğrulanmış bir istek gönderir.
  2. Hizmet sorumlusu kimlik doğrulaması: Sunucunuz, Databricks sunucusundan bir OAuth belirteci istemek ve almak için hizmet sorumlusu gizli dizisini kullanır. Bu, Azure Databricks'in hizmet sorumlusu adına erişimi olan tüm pano API'lerini çağırabilen geniş kapsamlı bir belirteçtir. Sunucunuz, ve gibi /tokeninfoexternal_viewer_idtemel kullanıcı bilgilerini geçirerek bu belirteci kullanarak uç noktayı çağırırexternal_value. Bkz. Panoları tek tek kullanıcılara güvenli bir şekilde sunma.
  3. Kullanıcı kapsamlı belirteç oluşturma: Sunucunuz, uç noktadan ve Databricks OpenID Connect (OIDC) uç noktasından gelen /tokeninfo yanıtı kullanarak, iletmiş olduğunuz kullanıcı bilgilerini kodlayan sıkı kapsamlı yeni bir belirteç oluşturur.
  4. Pano işleme ve veri filtreleme: Uygulama sayfasından örnek oluşturulur DatabricksDashboard@databricks/aibi-client ve oluşturma sırasında kullanıcı kapsamlı belirteci geçirir. Pano, kullanıcının bağlamıyla işlenir. Bu belirteç erişimi yetkiler, ile external_viewer_iddenetimi destekler ve veri filtreleme için taşır external_value . Pano veri kümelerindeki sorgular, kullanıcı başına filtre uygulamak için başvurabilir __aibi_external_value ve her görüntüleyicinin yalnızca görüntülemesine izin verilen verileri görmesini sağlar.

Panoları tek tek kullanıcılara güvenli bir şekilde sunma

Uygulama sunucunuzu, her kullanıcı için kendi external_viewer_idtabanlı benzersiz bir kullanıcı kapsamlı belirteci oluşturacak şekilde yapılandırın. Bu, denetim günlükleri aracılığıyla pano görünümlerini ve kullanımını izlemenizi sağlar. external_viewer_id, pano veri kümelerinde kullanılan SQL sorgularına eklenebilen genel bir değişken işlevi gören bir external_valueile eşleştirilir. Bu, her kullanıcı için panoda görüntülenen verileri filtrelemenizi sağlar.

external_viewer_id pano denetim günlüklerinize geçirilir ve kişisel bilgileri içermemelidir. Bu değer kullanıcı başına benzersiz olmalıdır.

external_value sorgu işlemede kullanılır ve kişisel bilgileri içerebilir .

Aşağıdaki örnekte, dış değerin veri kümesi sorgularında filtre olarak nasıl kullanılacağı gösterilmektedir:

SELECT *
FROM sales
WHERE region = __aibi_external_value

Kuruluma genel bakış

Bu bölüm, bir panoyu dış konuma eklemek üzere ayarlamak için gerçekleştirmeniz gereken adımlara yönelik üst düzey kavramsal bir genel bakış içerir.

Dış uygulamaya pano eklemek için önce Azure Databricks'te bir hizmet sorumlusu oluşturup bir gizli dizi oluşturursunuz. Hizmet sorumlusuna panoya ve temel alınan verilere okuma erişimi verilmelidir. Sunucunuz hizmet sorumlusu adına pano API'lerine erişebilen bir belirteç almak için hizmet sorumlusu gizli dizisini kullanır. Bu belirteçle sunucu, ve /tokeninfo değerleri de dahil olmak üzere external_value temel kullanıcı profili bilgilerini döndüren openID Connect (OIDC) uç noktası olan API uç noktasını çağırırexternal_viewer_id. Bu değerler istekleri tek tek kullanıcılarla ilişkilendirmenize olanak sağlar.

Sunucunuz, hizmet sorumlusundan alınan belirteci kullanarak panoya erişen belirli bir kullanıcı kapsamında yeni bir belirteç oluşturur. Bu kullanıcı kapsamlı belirteç, uygulamanın kitaplıktan DatabricksDashboard nesnenin örneğini @databricks/aibi-client oluşturduğu uygulama sayfasına geçirilir. Belirteç, denetimi destekleyen kullanıcıya özgü bilgileri taşır ve her kullanıcının yalnızca erişim yetkisine sahip olduğu verileri görmesi için filtrelemeyi zorlar. Kullanıcının bakış açısından, uygulamada oturum açmak otomatik olarak eklenmiş panoya doğru veri görünürlüğüyle erişim sağlar.

Hız sınırları ve performansla ilgili dikkat edilmesi gerekenler

Dış ekleme, saniyede 20 pano yükü hız sınırına sahiptir. Aynı anda 20'den fazla pano açabilirsiniz, ancak 20'den fazla pano aynı anda yüklenmeye başlanmaz.

Önkoşullar

Dış ekleme uygulamak için aşağıdaki önkoşulları karşıladığınızdan emin olun:

1. Adım: Hizmet sorumlusu oluşturma

Azure Databricks içindeki dış uygulamanızın kimliği olarak hareket etmek için bir hizmet sorumlusu oluşturun. Bu hizmet sorumlusu, uygulamanız adına isteklerin kimliğini doğrular.

Hizmet sorumlusu oluşturmak için:

  1. Çalışma alanı yöneticisi olarak Azure Databricks çalışma alanında oturum açın.
  2. Azure Databricks çalışma alanının üst çubuğunda kullanıcı adınıza tıklayın ve Ayarlar'ı seçin.
  3. Sol bölmede Kimlik ve erişim'e tıklayın.
  4. Hizmet sorumluları yanındaki Yönet seçeneğine tıklayın.
  5. Hizmet sorumlusu ekle'ye tıklayın.
  6. Yeni ekle'ye tıklayın.
  7. Hizmet sorumlusu için açıklayıcı bir ad girin.
  8. Ekle'yi tıklatın.
  9. Hizmet sorumluları listeleme sayfasından yeni oluşturduğunuz hizmet sorumlusunu açın. Gerekirse, adıyla aramak için Filtre metni girdisi alanını kullanın.
  10. Hizmet sorumlusu ayrıntıları sayfasında Uygulama Kimliğini kaydedin. Databricks SQL erişimi ve Çalışma alanı erişimi onay kutularının seçili olduğunu doğrulayın.

2. Adım: OAuth gizli dizisi oluşturma

Hizmet sorumlusu için bir gizli dizi oluşturun ve dış uygulamanız için ihtiyacınız olacak aşağıdaki yapılandırma değerlerini toplayın:

  • Hizmet sorumlusu (istemci) kimliği
  • İstemci gizliliği

Hizmet sorumlusu, dış uygulamanızdan erişim belirteci isteğinde bulunurken kimliğini doğrulamak için bir OAuth gizli dizisi kullanır.

Gizli dizi oluşturmak için:

  1. Hizmet sorumlusu ayrıntıları sayfasında Gizli Diziler'e tıklayın.
  2. Gizli dizi oluştur'a tıklayın.
  3. Yeni gizli dizi için gün cinsinden bir yaşam süresi değeri girin (örneğin, 1 ile 730 gün arasında).
  4. Gizli diziyi hemen kopyalayın. Bu ekrandan ayrıldıktan sonra bu gizli diziyi yeniden görüntüleyemezsiniz.

3. Adım: Hizmet sorumlunuza izin atama

Oluşturduğunuz hizmet sorumlusu, uygulamanız üzerinden pano erişimi sağlayan kimlik görevi görür. Pano, paylaşılan veri izinleriyle yayımlanmadıysa izinleri yalnızca geçerlidir. Paylaşılan veri izinleri kullanılıyorsa, yayımcının kimlik bilgileri verilere erişiyor. Diğer ayrıntılar ve öneriler için bkz . Kimlik doğrulama yaklaşımlarını ekleme.

  1. Pano listesi sayfasını açmak için çalışma alanı kenar çubuğunda Panolar'a tıklayın.
  2. Eklemek istediğiniz panonun adına tıklayın. Yayımlanan pano açılır.
  3. Paylaş’a tıklayın.
  4. Paylaşım iletişim kutusundaki metin girişi alanını kullanarak hizmet sorumlunuzu bulun ve üzerine tıklayın. İzin düzeyini CAN RUN olarak ayarlayın. Ardından Ekle'ye tıklayın.
  5. Pano kimliğini kaydedin. Pano kimliğini panonun URL'sinde bulabilirsiniz (ör. https://<your-workspace-url>/dashboards/<dashboard-id>). Bkz. Databricks çalışma alanı ayrıntıları.

Uyarı

Tek tek veri izinlerine sahip bir pano yayımlarsanız, hizmet sorumlunuza panoda kullanılan verilere erişim izni vermelisiniz. İşlem erişimi her zaman yayımcının kimlik bilgilerini kullanır, bu nedenle hizmet sorumlusuna işlem izinleri vermeniz gerekmez.

Verileri okumak ve görüntülemek için hizmet sorumlusunun panoda başvuruda bulunan tablo ve görünümlerde en az SELECT ayrıcalıklara sahip olması gerekir. Bkz . Ayrıcalıkları kimler yönetebilir?.

4. Adım: Kimlik doğrulaması yapmak ve belirteç oluşturmak için örnek uygulamayı kullanma

Panonuzu dışarıdan ekleme alıştırması yapmak için örnek bir uygulama kullanın. Uygulamalar, kapsamlı belirteçler oluşturmak için gerekli belirteç değişimini başlatan yönergeler ve kod içerir. Aşağıdaki kod bloklarının bağımlılıkları yoktur. Aşağıdaki uygulamalardan birini kopyalayın ve kaydedin.

Piton

Bunu kopyalayın ve adlı example.pybir dosyaya kaydedin.

#!/usr/bin/env python3

import os
import sys
import json
import base64
import urllib.request
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler

# -----------------------------------------------------------------------------
# Config
# -----------------------------------------------------------------------------
CONFIG = {
    "instance_url": os.environ.get("INSTANCE_URL"),
    "dashboard_id": os.environ.get("DASHBOARD_ID"),
    "service_principal_id": os.environ.get("SERVICE_PRINCIPAL_ID"),
    "service_principal_secret": os.environ.get("SERVICE_PRINCIPAL_SECRET"),
    "external_viewer_id": os.environ.get("EXTERNAL_VIEWER_ID"),
    "external_value": os.environ.get("EXTERNAL_VALUE"),
    "workspace_id": os.environ.get("WORKSPACE_ID"),
    "port": int(os.environ.get("PORT", 3000)),
}

basic_auth = base64.b64encode(
    f"{CONFIG['service_principal_id']}:{CONFIG['service_principal_secret']}".encode()
).decode()

# -----------------------------------------------------------------------------
# HTTP Request Helper
# -----------------------------------------------------------------------------
def http_request(url, method="GET", headers=None, body=None):
    headers = headers or {}
    if body is not None and not isinstance(body, (bytes, str)):
        raise ValueError("Body must be bytes or str")

    req = urllib.request.Request(url, method=method, headers=headers)
    if body is not None:
        if isinstance(body, str):
            body = body.encode()
        req.data = body

    try:
        with urllib.request.urlopen(req) as resp:
            data = resp.read().decode()
            try:
                return {"data": json.loads(data)}
            except json.JSONDecodeError:
                return {"data": data}
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"HTTP {e.code}: {e.read().decode()}") from None

# -----------------------------------------------------------------------------
# Token logic
# -----------------------------------------------------------------------------
def get_scoped_token():
    # 1. Get all-api token
    oidc_res = http_request(
        f"{CONFIG['instance_url']}/oidc/v1/token",
        method="POST",
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": f"Basic {basic_auth}",
        },
        body=urllib.parse.urlencode({
            "grant_type": "client_credentials",
            "scope": "all-apis"
        })
    )
    oidc_token = oidc_res["data"]["access_token"]

    # 2. Get token info
    token_info_url = (
        f"{CONFIG['instance_url']}/api/2.0/lakeview/dashboards/"
        f"{CONFIG['dashboard_id']}/published/tokeninfo"
        f"?external_viewer_id={urllib.parse.quote(CONFIG['external_viewer_id'])}"
        f"&external_value={urllib.parse.quote(CONFIG['external_value'])}"
    )
    token_info = http_request(
        token_info_url,
        headers={"Authorization": f"Bearer {oidc_token}"}
    )["data"]

    # 3. Generate scoped token
    params = token_info.copy()
    authorization_details = params.pop("authorization_details", None)
    params.update({
        "grant_type": "client_credentials",
        "authorization_details": json.dumps(authorization_details)
    })

    scoped_res = http_request(
        f"{CONFIG['instance_url']}/oidc/v1/token",
        method="POST",
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": f"Basic {basic_auth}",
        },
        body=urllib.parse.urlencode(params)
    )
    return scoped_res["data"]["access_token"]

# -----------------------------------------------------------------------------
# HTML generator
# -----------------------------------------------------------------------------
def generate_html(token):
    return f"""<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Demo</title>
    <style>
        body {{ font-family: system-ui; margin: 0; padding: 20px; background: #f5f5f5; }}
        .container {{ max-width: 1200px; margin: 0 auto; height:calc(100vh - 40px) }}
    </style>
</head>
<body>
    <div id="dashboard-content" class="container"></div>
    <script type="module">
        import {{ DatabricksDashboard }} from "https://cdn.jsdelivr.net/npm/@databricks/aibi-client@0.0.0-alpha.7/+esm";
        const dashboard = new DatabricksDashboard({{
            instanceUrl: "{CONFIG['instance_url']}",
            workspaceId: "{CONFIG['workspace_id']}",
            dashboardId: "{CONFIG['dashboard_id']}",
            token: "{token}",
            container: document.getElementById("dashboard-content")
        }});
        dashboard.initialize();
    </script>
</body>
</html>"""

# -----------------------------------------------------------------------------
# HTTP server
# -----------------------------------------------------------------------------
class RequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/":
            self.send_response(404)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(b"Not Found")
            return

        try:
            token = get_scoped_token()
            html = generate_html(token)
            status = 200
        except Exception as e:
            html = f"<h1>Error</h1><p>{e}</p>"
            status = 500

        self.send_response(status)
        self.send_header("Content-Type", "text/html")
        self.end_headers()
        self.wfile.write(html.encode())

def start_server():
    missing = [k for k, v in CONFIG.items() if not v]
    if missing:
        print(f"Missing: {', '.join(missing)}", file=sys.stderr)
        sys.exit(1)

    server = HTTPServer(("localhost", CONFIG["port"]), RequestHandler)
    print(f":rocket: Server running on http://localhost:{CONFIG['port']}")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        sys.exit(0)

if __name__ == "__main__":
    start_server()

JavaScript

Bunu kopyalayın ve adlı example.jsbir dosyaya kaydedin.

#!/usr/bin/env node

const http = require('http');
const https = require('https');
const { URL, URLSearchParams } = require('url');

// This constant is just a mapping of environment variables to their respective
// values.
const CONFIG = {
  instanceUrl: process.env.INSTANCE_URL,
  dashboardId: process.env.DASHBOARD_ID,
  servicePrincipalId: process.env.SERVICE_PRINCIPAL_ID,
  servicePrincipalSecret: process.env.SERVICE_PRINCIPAL_SECRET,
  externalViewerId: process.env.EXTERNAL_VIEWER_ID,
  externalValue: process.env.EXTERNAL_VALUE,
  workspaceId: process.env.WORKSPACE_ID,
  port: process.env.PORT || 3000,
};

const basicAuth = Buffer.from(`${CONFIG.servicePrincipalId}:${CONFIG.servicePrincipalSecret}`).toString('base64');

// ------------------------------------------------------------------------------------------------
// Main
// ------------------------------------------------------------------------------------------------

function startServer() {
  const missing = Object.keys(CONFIG).filter((key) => !CONFIG[key]);
  if (missing.length > 0) throw new Error(`Missing: ${missing.join(', ')}`);

  const server = http.createServer(async (req, res) => {
    // This is a demo server, we only support GET requests to the root URL.
    if (req.method !== 'GET' || req.url !== '/') {
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('Not Found');
      return;
    }

    let html = '';
    let status = 200;

    try {
      const token = await getScopedToken();
      html = generateHTML(token);
    } catch (error) {
      html = `<h1>Error</h1><p>${error.message}</p>`;
      status = 500;
    } finally {
      res.writeHead(status, { 'Content-Type': 'text/html' });
      res.end(html);
    }
  });

  server.listen(CONFIG.port, () => {
    console.log(`🚀 Server running on http://localhost:${CONFIG.port}`);
  });

  process.on('SIGINT', () => process.exit(0));
  process.on('SIGTERM', () => process.exit(0));
}

async function getScopedToken() {
  // 1. Get all-api token. This will allow you to access the /tokeninfo
  // endpoint, which contains the information required to generate a scoped token
  const {
    data: { access_token: oidcToken },
  } = await httpRequest(`${CONFIG.instanceUrl}/oidc/v1/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: `Basic ${basicAuth}`,
    },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      scope: 'all-apis',
    }),
  });

  // 2. Get token info. This information is **required** for generating a token that is correctly downscoped.
  // A correctly downscoped token will only have access to a handful of APIs, and within those APIs, only
  // a the specific resources required to render the dashboard.
  //
  // This is essential to prevent leaking a privileged token.
  //
  // At the time of writing, OAuth tokens in Databricks are valid for 1 hour.
  const tokenInfoUrl = new URL(
    `${CONFIG.instanceUrl}/api/2.0/lakeview/dashboards/${CONFIG.dashboardId}/published/tokeninfo`,
  );
  tokenInfoUrl.searchParams.set('external_viewer_id', CONFIG.externalViewerId);
  tokenInfoUrl.searchParams.set('external_value', CONFIG.externalValue);

  const { data: tokenInfo } = await httpRequest(tokenInfoUrl.toString(), {
    headers: { Authorization: `Bearer ${oidcToken}` },
  });

  // 3. Generate scoped token. This call is very similar to what was issued before, but now we are providing the scoping to make the generated token
  // safe to pass to a browser.
  const { authorization_details, ...params } = tokenInfo;
  const {
    data: { access_token },
  } = await httpRequest(`${CONFIG.instanceUrl}/oidc/v1/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: `Basic ${basicAuth}`,
    },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      ...params,
      authorization_details: JSON.stringify(authorization_details),
    }),
  });

  return access_token;
}

startServer();

// ------------------------------------------------------------------------------------------------
// Helper functions
// ------------------------------------------------------------------------------------------------

/**
 * Helper function to create HTTP requests.
 * @param {string} url - The URL to make the request to.
 * @param {Object} options - The options for the request.
 * @param {string} options.method - The HTTP method to use.
 * @param {Object} options.headers - The headers to include in the request.
 * @param {Object} options.body - The body to include in the request.
 * @returns {Promise<Object>} A promise that resolves to the response data.
 */
function httpRequest(url, { method = 'GET', headers = {}, body } = {}) {
  return new Promise((resolve, reject) => {
    const isHttps = url.startsWith('https://');
    const lib = isHttps ? https : http;
    const options = new URL(url);
    options.method = method;
    options.headers = headers;

    const req = lib.request(options, (res) => {
      let data = '';
      res.on('data', (chunk) => (data += chunk));
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          try {
            resolve({ data: JSON.parse(data) });
          } catch {
            resolve({ data });
          }
        } else {
          reject(new Error(`HTTP ${res.statusCode}: ${data}`));
        }
      });
    });

    req.on('error', reject);

    if (body) {
      if (typeof body === 'string' || Buffer.isBuffer(body)) {
        req.write(body);
      } else if (body instanceof URLSearchParams) {
        req.write(body.toString());
      } else {
        req.write(JSON.stringify(body));
      }
    }
    req.end();
  });
}

function generateHTML(token) {
  return `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Demo</title>
    <style>
        body { font-family: system-ui; margin: 0; padding: 20px; background: #f5f5f5; }
        .container { max-width: 1200px; margin: 0 auto; height:calc(100vh - 40px) }
    </style>
</head>
<body>
    <div id="dashboard-content" class="container"></div>
    <script type="module">
        /**
         * We recommend bundling the dependency instead of using a CDN. However, for demonstration purposes,
         * we are just using a CDN.
         * 
         * We do not recommend one CDN over another and encourage decoupling the dependency from third-party code.
         */
        import { DatabricksDashboard } from "https://cdn.jsdelivr.net/npm/@databricks/aibi-client@0.0.0-alpha.7/+esm";
    
        const dashboard = new DatabricksDashboard({
            instanceUrl: "${CONFIG.instanceUrl}",
            workspaceId: "${CONFIG.workspaceId}",
            dashboardId: "${CONFIG.dashboardId}",
            token: "${token}",
            container: document.getElementById("dashboard-content")
        });
        
        dashboard.initialize();
    </script>
</body>
</html>`;
}

5. Adım: Örnek uygulamayı çalıştırma

Aşağıdaki değerleri değiştirin ve ardından kod bloğunu terminalden çalıştırın. Değerleriniz açılı ayraçlarla () < >:

  • Aşağıdaki değerleri bulmak ve değiştirmek için çalışma alanı URL'sini kullanın:
    • <your-instance>
    • <workspace_id>
    • <dashboard_id>
  • Aşağıdaki değerleri hizmet sorumlusunu oluştururken oluşturduğunuz değerlerle değiştirin (2. adım):
    • <service_principal_id>
    • <service_principal_secret> (istemci gizli dizisi)
  • Aşağıdaki değerleri dış uygulamanın kullanıcılarıyla ilişkilendirilmiş tanımlayıcılarla değiştirin:
    • <some-external-viewer>
    • <some-external-value>
  • değerini önceki adımda oluşturduğunuz veya </path/to/example> dosyasının yoluyla .py değiştirin.js. Dosya uzantısını ekleyin.

Uyarı

Değere kişisel bilgileri (PII) eklemeyin EXTERNAL_VIEWER_ID .


INSTANCE_URL='https://<your-instance>.databricks.com' \
WORKSPACE_ID='<workspace_id>' \
DASHBOARD_ID='<dashboard_id>' \
SERVICE_PRINCIPAL_ID='<service-principal-id>' \
SERVICE_PRINCIPAL_SECRET='<service-principal_secret>' \
EXTERNAL_VIEWER_ID='<some-external-viewer>' \
EXTERNAL_VALUE='<some-external-value>' \
~</path/to/example>

# Terminal will output: :rocket: Server running on http://localhost:3000