feat(analytics): integrate Langfuse with Keycloak SSO
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 2m49s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / E2E Tests (push) Has been skipped
CI / Deploy (push) Has been skipped
CI / Format (pull_request) Successful in 3s
CI / Security Audit (pull_request) Has been cancelled
CI / Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Deploy (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 2m49s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / E2E Tests (push) Has been skipped
CI / Deploy (push) Has been skipped
CI / Format (pull_request) Successful in 3s
CI / Security Audit (pull_request) Has been cancelled
CI / Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Deploy (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
Add certifai-langfuse OIDC client to Keycloak realm export and configure the Langfuse Docker service with Keycloak SSO env vars (shared realm, account linking, local auth disabled). Replace the iframe-based analytics page with an informational landing since cross-origin SSO breaks in iframes. Users open Langfuse in a new tab where the active Keycloak session authenticates them transparently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -118,7 +118,16 @@
|
|||||||
"agents_col_name": "Name",
|
"agents_col_name": "Name",
|
||||||
"agents_col_id": "ID",
|
"agents_col_id": "ID",
|
||||||
"agents_col_description": "Beschreibung",
|
"agents_col_description": "Beschreibung",
|
||||||
"agents_col_status": "Status"
|
"agents_col_status": "Status",
|
||||||
|
"analytics_status_connected": "Verbunden",
|
||||||
|
"analytics_status_not_connected": "Nicht verbunden",
|
||||||
|
"analytics_config_hint": "Setzen Sie LANGFUSE_URL in .env, um eine Verbindung herzustellen",
|
||||||
|
"analytics_sso_hint": "Langfuse nutzt Keycloak-SSO. Sie werden automatisch mit Ihrem CERTifAI-Konto angemeldet.",
|
||||||
|
"analytics_quick_actions": "Schnellaktionen",
|
||||||
|
"analytics_traces": "Traces",
|
||||||
|
"analytics_traces_desc": "Alle LLM-Aufrufe, Latenzen und Token-Verbrauch anzeigen und filtern.",
|
||||||
|
"analytics_dashboard": "Dashboard",
|
||||||
|
"analytics_dashboard_desc": "Ueberblick ueber Kosten, Qualitaetsmetriken und Nutzungstrends."
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"title": "Organisation",
|
"title": "Organisation",
|
||||||
|
|||||||
@@ -118,7 +118,16 @@
|
|||||||
"agents_col_name": "Name",
|
"agents_col_name": "Name",
|
||||||
"agents_col_id": "ID",
|
"agents_col_id": "ID",
|
||||||
"agents_col_description": "Description",
|
"agents_col_description": "Description",
|
||||||
"agents_col_status": "Status"
|
"agents_col_status": "Status",
|
||||||
|
"analytics_status_connected": "Connected",
|
||||||
|
"analytics_status_not_connected": "Not Connected",
|
||||||
|
"analytics_config_hint": "Set LANGFUSE_URL in .env to connect",
|
||||||
|
"analytics_sso_hint": "Langfuse uses Keycloak SSO. You will be signed in automatically with your CERTifAI account.",
|
||||||
|
"analytics_quick_actions": "Quick Actions",
|
||||||
|
"analytics_traces": "Traces",
|
||||||
|
"analytics_traces_desc": "View and filter all LLM call traces, latencies, and token usage.",
|
||||||
|
"analytics_dashboard": "Dashboard",
|
||||||
|
"analytics_dashboard_desc": "Overview of costs, quality metrics, and usage trends."
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"title": "Organization",
|
"title": "Organization",
|
||||||
|
|||||||
@@ -118,7 +118,16 @@
|
|||||||
"agents_col_name": "Nombre",
|
"agents_col_name": "Nombre",
|
||||||
"agents_col_id": "ID",
|
"agents_col_id": "ID",
|
||||||
"agents_col_description": "Descripcion",
|
"agents_col_description": "Descripcion",
|
||||||
"agents_col_status": "Estado"
|
"agents_col_status": "Estado",
|
||||||
|
"analytics_status_connected": "Conectado",
|
||||||
|
"analytics_status_not_connected": "No conectado",
|
||||||
|
"analytics_config_hint": "Configure LANGFUSE_URL en .env para conectar",
|
||||||
|
"analytics_sso_hint": "Langfuse utiliza SSO de Keycloak. Iniciara sesion automaticamente con su cuenta CERTifAI.",
|
||||||
|
"analytics_quick_actions": "Acciones rapidas",
|
||||||
|
"analytics_traces": "Trazas",
|
||||||
|
"analytics_traces_desc": "Ver y filtrar todas las llamadas LLM, latencias y uso de tokens.",
|
||||||
|
"analytics_dashboard": "Panel de control",
|
||||||
|
"analytics_dashboard_desc": "Resumen de costos, metricas de calidad y tendencias de uso."
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"title": "Organizacion",
|
"title": "Organizacion",
|
||||||
|
|||||||
@@ -118,7 +118,16 @@
|
|||||||
"agents_col_name": "Nom",
|
"agents_col_name": "Nom",
|
||||||
"agents_col_id": "ID",
|
"agents_col_id": "ID",
|
||||||
"agents_col_description": "Description",
|
"agents_col_description": "Description",
|
||||||
"agents_col_status": "Statut"
|
"agents_col_status": "Statut",
|
||||||
|
"analytics_status_connected": "Connecte",
|
||||||
|
"analytics_status_not_connected": "Non connecte",
|
||||||
|
"analytics_config_hint": "Definissez LANGFUSE_URL dans .env pour vous connecter",
|
||||||
|
"analytics_sso_hint": "Langfuse utilise le SSO Keycloak. Vous serez connecte automatiquement avec votre compte CERTifAI.",
|
||||||
|
"analytics_quick_actions": "Actions rapides",
|
||||||
|
"analytics_traces": "Traces",
|
||||||
|
"analytics_traces_desc": "Afficher et filtrer tous les appels LLM, latences et consommation de tokens.",
|
||||||
|
"analytics_dashboard": "Tableau de bord",
|
||||||
|
"analytics_dashboard_desc": "Apercu des couts, metriques de qualite et tendances d'utilisation."
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"title": "Organisation",
|
"title": "Organisation",
|
||||||
|
|||||||
@@ -118,7 +118,16 @@
|
|||||||
"agents_col_name": "Nome",
|
"agents_col_name": "Nome",
|
||||||
"agents_col_id": "ID",
|
"agents_col_id": "ID",
|
||||||
"agents_col_description": "Descricao",
|
"agents_col_description": "Descricao",
|
||||||
"agents_col_status": "Estado"
|
"agents_col_status": "Estado",
|
||||||
|
"analytics_status_connected": "Conectado",
|
||||||
|
"analytics_status_not_connected": "Nao conectado",
|
||||||
|
"analytics_config_hint": "Defina LANGFUSE_URL no .env para conectar",
|
||||||
|
"analytics_sso_hint": "O Langfuse utiliza SSO do Keycloak. Sera autenticado automaticamente com a sua conta CERTifAI.",
|
||||||
|
"analytics_quick_actions": "Acoes rapidas",
|
||||||
|
"analytics_traces": "Traces",
|
||||||
|
"analytics_traces_desc": "Ver e filtrar todas as chamadas LLM, latencias e uso de tokens.",
|
||||||
|
"analytics_dashboard": "Painel",
|
||||||
|
"analytics_dashboard_desc": "Resumo de custos, metricas de qualidade e tendencias de uso."
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"title": "Organizacao",
|
"title": "Organizacao",
|
||||||
|
|||||||
@@ -3614,11 +3614,102 @@ h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.agents-page {
|
.agents-page,
|
||||||
|
.analytics-page {
|
||||||
padding: 20px 16px;
|
padding: 20px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agents-grid {
|
.agents-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Analytics Page ===== */
|
||||||
|
.analytics-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 32px;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-hero {
|
||||||
|
max-width: 720px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-hero-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-hero-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
min-width: 48px;
|
||||||
|
background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
|
||||||
|
color: var(--avatar-text);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-hero-title {
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-heading);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-hero-desc {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-sso-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-launch-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
|
||||||
|
color: var(--avatar-text);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-launch-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-stats-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.analytics-stats-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +139,8 @@ services:
|
|||||||
container_name: certifai-langfuse
|
container_name: certifai-langfuse
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
keycloak:
|
||||||
|
condition: service_healthy
|
||||||
langfuse-db:
|
langfuse-db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
langfuse-clickhouse:
|
langfuse-clickhouse:
|
||||||
@@ -155,6 +157,13 @@ services:
|
|||||||
NEXTAUTH_SECRET: certifai-langfuse-dev-secret
|
NEXTAUTH_SECRET: certifai-langfuse-dev-secret
|
||||||
SALT: certifai-langfuse-dev-salt
|
SALT: certifai-langfuse-dev-salt
|
||||||
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000"
|
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
# Keycloak OIDC SSO - shared realm with CERTifAI dashboard
|
||||||
|
AUTH_KEYCLOAK_CLIENT_ID: certifai-langfuse
|
||||||
|
AUTH_KEYCLOAK_CLIENT_SECRET: certifai-langfuse-secret
|
||||||
|
AUTH_KEYCLOAK_ISSUER: http://keycloak:8080/realms/certifai
|
||||||
|
AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING: "true"
|
||||||
|
# Disable local email/password auth (SSO only)
|
||||||
|
AUTH_DISABLE_USERNAME_PASSWORD: "true"
|
||||||
CLICKHOUSE_URL: http://langfuse-clickhouse:8123
|
CLICKHOUSE_URL: http://langfuse-clickhouse:8123
|
||||||
CLICKHOUSE_MIGRATION_URL: clickhouse://langfuse-clickhouse:9000
|
CLICKHOUSE_MIGRATION_URL: clickhouse://langfuse-clickhouse:9000
|
||||||
CLICKHOUSE_USER: clickhouse
|
CLICKHOUSE_USER: clickhouse
|
||||||
|
|||||||
@@ -123,17 +123,51 @@ test.describe("Developer section", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("analytics page shows Not Configured when URL is empty", async ({
|
test("analytics page renders informational landing", async ({ page }) => {
|
||||||
|
await page.goto("/developer/analytics");
|
||||||
|
await page.waitForSelector(".analytics-page", { timeout: 15_000 });
|
||||||
|
|
||||||
|
// Hero section
|
||||||
|
await expect(page.locator(".analytics-hero-title")).toBeVisible();
|
||||||
|
await expect(page.locator(".analytics-hero-desc")).toBeVisible();
|
||||||
|
|
||||||
|
// Connection status indicator
|
||||||
|
await expect(page.locator(".agents-status")).toBeVisible();
|
||||||
|
|
||||||
|
// Metrics bar
|
||||||
|
await expect(page.locator(".analytics-stats-bar")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("analytics page shows Not Connected when URL is empty", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto("/developer/analytics");
|
await page.goto("/developer/analytics");
|
||||||
await page.waitForSelector(".placeholder-page", { timeout: 15_000 });
|
await page.waitForSelector(".analytics-page", { timeout: 15_000 });
|
||||||
|
|
||||||
await expect(
|
await expect(page.locator(".agents-status")).toContainText(
|
||||||
page.locator("h2", { hasText: "Analytics" })
|
"Not Connected"
|
||||||
).toBeVisible();
|
|
||||||
await expect(page.locator(".placeholder-badge")).toContainText(
|
|
||||||
"Not Configured"
|
|
||||||
);
|
);
|
||||||
|
await expect(page.locator(".agents-status-dot--off")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("analytics page shows quick action cards", async ({ page }) => {
|
||||||
|
await page.goto("/developer/analytics");
|
||||||
|
await page.waitForSelector(".analytics-page", { timeout: 15_000 });
|
||||||
|
|
||||||
|
const grid = page.locator(".agents-grid");
|
||||||
|
const cards = grid.locator(".agents-card, .agents-card--disabled");
|
||||||
|
await expect(cards).toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("analytics page shows SSO hint when connected", async ({ page }) => {
|
||||||
|
// Only meaningful when LANGFUSE_URL is configured.
|
||||||
|
await page.goto("/developer/analytics");
|
||||||
|
await page.waitForSelector(".analytics-page", { timeout: 15_000 });
|
||||||
|
|
||||||
|
const connectedDot = page.locator(".agents-status-dot--on");
|
||||||
|
if (await connectedDot.isVisible()) {
|
||||||
|
await expect(page.locator(".analytics-sso-hint")).toBeVisible();
|
||||||
|
await expect(page.locator(".analytics-launch-btn")).toBeVisible();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,6 +79,39 @@
|
|||||||
"offline_access"
|
"offline_access"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"clientId": "certifai-langfuse",
|
||||||
|
"name": "CERTifAI Langfuse",
|
||||||
|
"description": "Langfuse OIDC client for CERTifAI",
|
||||||
|
"enabled": true,
|
||||||
|
"publicClient": false,
|
||||||
|
"directAccessGrantsEnabled": false,
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"implicitFlowEnabled": false,
|
||||||
|
"serviceAccountsEnabled": false,
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"secret": "certifai-langfuse-secret",
|
||||||
|
"rootUrl": "http://localhost:3000",
|
||||||
|
"baseUrl": "http://localhost:3000",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:3000/*"
|
||||||
|
],
|
||||||
|
"webOrigins": [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:8000"
|
||||||
|
],
|
||||||
|
"attributes": {
|
||||||
|
"post.logout.redirect.uris": "http://localhost:3000"
|
||||||
|
},
|
||||||
|
"defaultClientScopes": [
|
||||||
|
"openid",
|
||||||
|
"profile",
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"optionalClientScopes": [
|
||||||
|
"offline_access"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"clientId": "certifai-librechat",
|
"clientId": "certifai-librechat",
|
||||||
"name": "CERTifAI Chat",
|
"name": "CERTifAI Chat",
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_free_icons::icons::bs_icons::{
|
||||||
|
BsBarChartLine, BsBoxArrowUpRight, BsGraphUp, BsSpeedometer,
|
||||||
|
};
|
||||||
|
use dioxus_free_icons::Icon;
|
||||||
|
|
||||||
use crate::components::ToolEmbed;
|
|
||||||
use crate::i18n::{t, Locale};
|
use crate::i18n::{t, Locale};
|
||||||
use crate::models::{AnalyticsMetric, ServiceUrlsContext};
|
use crate::models::{AnalyticsMetric, ServiceUrlsContext};
|
||||||
|
|
||||||
/// Analytics page embedding Langfuse for observability.
|
/// Analytics & Observability page for Langfuse.
|
||||||
///
|
///
|
||||||
/// Always shows a stats bar with sample metrics. Below that, when
|
/// Langfuse is configured with Keycloak SSO (shared realm with CERTifAI).
|
||||||
/// `langfuse_url` is configured, embeds the service in an iframe
|
/// When users open Langfuse, the existing Keycloak session auto-authenticates
|
||||||
/// with a pop-out button. Otherwise shows a "Not Configured" placeholder.
|
/// them transparently. This page shows a metrics bar, connection status,
|
||||||
|
/// and a prominent button to open Langfuse in a new tab.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn AnalyticsPage() -> Element {
|
pub fn AnalyticsPage() -> Element {
|
||||||
let locale = use_context::<Signal<Locale>>();
|
let locale = use_context::<Signal<Locale>>();
|
||||||
@@ -16,10 +20,55 @@ pub fn AnalyticsPage() -> Element {
|
|||||||
let l = *locale.read();
|
let l = *locale.read();
|
||||||
let url = svc.read().langfuse_url.clone();
|
let url = svc.read().langfuse_url.clone();
|
||||||
|
|
||||||
|
let connected = !url.is_empty();
|
||||||
let metrics = mock_metrics(l);
|
let metrics = mock_metrics(l);
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
section { class: "placeholder-page",
|
div { class: "analytics-page",
|
||||||
|
// -- Hero section --
|
||||||
|
div { class: "analytics-hero",
|
||||||
|
div { class: "analytics-hero-row",
|
||||||
|
div { class: "analytics-hero-icon",
|
||||||
|
Icon { icon: BsGraphUp, width: 24, height: 24 }
|
||||||
|
}
|
||||||
|
h2 { class: "analytics-hero-title",
|
||||||
|
{t(l, "developer.analytics_title")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p { class: "analytics-hero-desc",
|
||||||
|
{t(l, "developer.analytics_desc")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Connection status --
|
||||||
|
if connected {
|
||||||
|
div { class: "agents-status",
|
||||||
|
span {
|
||||||
|
class: "agents-status-dot agents-status-dot--on",
|
||||||
|
}
|
||||||
|
span { {t(l, "developer.analytics_status_connected")} }
|
||||||
|
code { class: "agents-status-url", {url.clone()} }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
div { class: "agents-status",
|
||||||
|
span {
|
||||||
|
class: "agents-status-dot agents-status-dot--off",
|
||||||
|
}
|
||||||
|
span { {t(l, "developer.analytics_status_not_connected")} }
|
||||||
|
span { class: "agents-status-hint",
|
||||||
|
{t(l, "developer.analytics_config_hint")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- SSO info --
|
||||||
|
if connected {
|
||||||
|
p { class: "analytics-sso-hint",
|
||||||
|
{t(l, "developer.analytics_sso_hint")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Metrics bar --
|
||||||
div { class: "analytics-stats-bar",
|
div { class: "analytics-stats-bar",
|
||||||
for metric in &metrics {
|
for metric in &metrics {
|
||||||
div { class: "analytics-stat",
|
div { class: "analytics-stat",
|
||||||
@@ -36,13 +85,59 @@ pub fn AnalyticsPage() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ToolEmbed {
|
// -- Open Langfuse button --
|
||||||
url,
|
if connected {
|
||||||
title: t(l, "developer.analytics_title"),
|
a {
|
||||||
description: t(l, "developer.analytics_desc"),
|
class: "analytics-launch-btn",
|
||||||
icon: "L",
|
href: "{url}",
|
||||||
launch_label: t(l, "developer.launch_analytics"),
|
target: "_blank",
|
||||||
|
rel: "noopener noreferrer",
|
||||||
|
Icon { icon: BsBoxArrowUpRight, width: 16, height: 16 }
|
||||||
|
span { {t(l, "developer.launch_analytics")} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Quick actions --
|
||||||
|
h3 { class: "agents-section-title",
|
||||||
|
{t(l, "developer.analytics_quick_actions")}
|
||||||
|
}
|
||||||
|
|
||||||
|
div { class: "agents-grid",
|
||||||
|
// Traces
|
||||||
|
a {
|
||||||
|
class: if connected { "agents-card" } else { "agents-card agents-card--disabled" },
|
||||||
|
href: if connected { format!("{url}/project") } else { "#".to_string() },
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noopener noreferrer",
|
||||||
|
div { class: "agents-card-icon",
|
||||||
|
Icon { icon: BsBarChartLine, width: 18, height: 18 }
|
||||||
|
}
|
||||||
|
div { class: "agents-card-title",
|
||||||
|
{t(l, "developer.analytics_traces")}
|
||||||
|
}
|
||||||
|
div { class: "agents-card-desc",
|
||||||
|
{t(l, "developer.analytics_traces_desc")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
a {
|
||||||
|
class: if connected { "agents-card" } else { "agents-card agents-card--disabled" },
|
||||||
|
href: if connected { format!("{url}/project") } else { "#".to_string() },
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noopener noreferrer",
|
||||||
|
div { class: "agents-card-icon",
|
||||||
|
Icon { icon: BsSpeedometer, width: 18, height: 18 }
|
||||||
|
}
|
||||||
|
div { class: "agents-card-title",
|
||||||
|
{t(l, "developer.analytics_dashboard")}
|
||||||
|
}
|
||||||
|
div { class: "agents-card-desc",
|
||||||
|
{t(l, "developer.analytics_dashboard_desc")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user