feat: gate AI chart generation behind all 3 AI checks

- Server-side: Replace hardcoded OpenAI with provider-agnostic `getAiModel(env)` and enforce
  `assertOrganizationAIConfigured(organizationId, "dataAnalysis")` which validates license
  entitlement, org-level toggle, and instance configuration
- Client-side: Instead of hiding AI section when unavailable, show it disabled with a tooltip
  explaining the reason (not in plan / not enabled / instance not configured), following the
  same pattern as AI translate
- Thread `isAIAvailable` and `aiUnavailableReason` through the component chain from server
  pages down to `AIQuerySection`
- Update test mocks to match new provider-agnostic AI imports
This commit is contained in:
Dhruwang
2026-05-05 17:21:22 +05:30
parent 94c9e8fcf1
commit 850fb8acc3
27 changed files with 195 additions and 24 deletions
+4
View File
@@ -1597,6 +1597,10 @@ checksums:
workspace/analysis/charts/add_filter: ed5d8e9bfcb05cd1e10e4c403befbae6
workspace/analysis/charts/add_to_dashboard: 9941c3d30895bb8e25ce8d4e03d33a08
workspace/analysis/charts/advanced_chart_builder_config_prompt: c2fe2c1a076f27d3ae62a4db75474b0a
workspace/analysis/charts/ai_instance_not_configured: 6deeb8aeaff3982d07e1d5a045e06d2d
workspace/analysis/charts/ai_not_available: 173abfcd32dd45edcc258dfdaaed494b
workspace/analysis/charts/ai_not_enabled: 8651fdac58cd311d17a48001a880318d
workspace/analysis/charts/ai_not_in_plan: 60bb0792a1ed98c07d8694029cdfdb43
workspace/analysis/charts/ai_query_placeholder: 24c3d18f514cb3a9953f04c3b04503a2
workspace/analysis/charts/ai_query_section_description: 66d06342f29bf6658793403856521fd7
workspace/analysis/charts/ai_query_section_title: c0e450a47af7c2a516b77f73cf54db1b
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Filter hinzufügen",
"add_to_dashboard": "Zum Dashboard hinzufügen",
"advanced_chart_builder_config_prompt": "Konfiguriere dein Diagramm und klicke auf \"Abfrage ausführen\", um eine Vorschau zu sehen",
"ai_instance_not_configured": "KI ist auf dieser Instanz nicht konfiguriert. Kontaktiere deinen Administrator.",
"ai_not_available": "KI-Datenanalyse ist nicht verfügbar.",
"ai_not_enabled": "KI-Datenanalyse ist für diese Organisation deaktiviert. Aktiviere sie in den Organisationseinstellungen.",
"ai_not_in_plan": "KI-Datenanalyse ist in deinem aktuellen Tarif nicht verfügbar. Führe ein Upgrade durch, um diese Funktion freizuschalten.",
"ai_query_placeholder": "z.B. Wie viele Nutzer haben sich letzte Woche angemeldet?",
"ai_query_section_description": "Beschreibe, was du sehen möchtest, und lass die KI das Diagramm erstellen.",
"ai_query_section_title": "Frag deine Daten",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Add filter",
"add_to_dashboard": "Add to Dashboard",
"advanced_chart_builder_config_prompt": "Configure your chart and click \"Run Query\" to preview",
"ai_instance_not_configured": "AI is not configured on this instance. Contact your administrator.",
"ai_not_available": "AI data analysis is not available.",
"ai_not_enabled": "AI data analysis is disabled for this organization. Enable it in organization settings.",
"ai_not_in_plan": "AI data analysis is not available on your current plan. Upgrade to unlock this feature.",
"ai_query_placeholder": "e.g. How many users signed up last week?",
"ai_query_section_description": "Describe what you want to see and let AI build the chart.",
"ai_query_section_title": "Ask your data",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Añadir filtro",
"add_to_dashboard": "Añadir al panel de control",
"advanced_chart_builder_config_prompt": "Configura tu gráfico y haz clic en \"Ejecutar consulta\" para previsualizar",
"ai_instance_not_configured": "La IA no está configurada en esta instancia. Contacta con tu administrador.",
"ai_not_available": "El análisis de datos con IA no está disponible.",
"ai_not_enabled": "El análisis de datos con IA está desactivado para esta organización. Actívalo en la configuración de la organización.",
"ai_not_in_plan": "El análisis de datos con IA no está disponible en tu plan actual. Actualiza para desbloquear esta función.",
"ai_query_placeholder": "p. ej. ¿Cuántos usuarios se registraron la semana pasada?",
"ai_query_section_description": "Describe lo que quieres ver y deja que la IA construya el gráfico.",
"ai_query_section_title": "Pregunta a tus datos",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Ajouter un filtre",
"add_to_dashboard": "Ajouter au tableau de bord",
"advanced_chart_builder_config_prompt": "Configurez votre graphique et cliquez sur « Exécuter la requête » pour prévisualiser",
"ai_instance_not_configured": "L'IA n'est pas configurée sur cette instance. Contacte ton administrateur.",
"ai_not_available": "L'analyse de données par IA n'est pas disponible.",
"ai_not_enabled": "L'analyse de données par IA est désactivée pour cette organisation. Active-la dans les paramètres de l'organisation.",
"ai_not_in_plan": "L'analyse de données par IA n'est pas disponible sur ton forfait actuel. Passe à un forfait supérieur pour débloquer cette fonctionnalité.",
"ai_query_placeholder": "ex. Combien d'utilisateurs se sont inscrits la semaine dernière?",
"ai_query_section_description": "Décrivez ce que vous souhaitez voir et laissez l'IA créer le graphique.",
"ai_query_section_title": "Interrogez vos données",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Szűrő hozzáadása",
"add_to_dashboard": "Hozzáadás a vezérlőpulthoz",
"advanced_chart_builder_config_prompt": "Állítsd be a diagramot, és kattints a \"Lekérdezés futtatása\" gombra az előnézethez",
"ai_instance_not_configured": "Az AI nincs konfigurálva ezen a példányon. Kérjük, lépjen kapcsolatba a rendszergazdával.",
"ai_not_available": "Az AI adatelemzés nem elérhető.",
"ai_not_enabled": "Az AI adatelemzés le van tiltva ezen szervezet számára. Kérjük, engedélyezze a szervezeti beállításokban.",
"ai_not_in_plan": "Az AI adatelemzés nem elérhető az Ön jelenlegi csomagjában. Kérjük, frissítsen magasabb csomagra ezen funkció feloldásához.",
"ai_query_placeholder": "pl. Hány felhasználó regisztrált a múlt héten?",
"ai_query_section_description": "Írd le, mit szeretnél látni, és hagyd, hogy az AI elkészítse a diagramot.",
"ai_query_section_title": "Kérdezd meg az adataidat",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "フィルターを追加",
"add_to_dashboard": "ダッシュボードに追加",
"advanced_chart_builder_config_prompt": "チャートを設定して「クエリを実行」をクリックしてプレビューを表示",
"ai_instance_not_configured": "このインスタンスではAIが設定されていません。管理者にお問い合わせください。",
"ai_not_available": "AIデータ分析は利用できません。",
"ai_not_enabled": "この組織ではAIデータ分析が無効になっています。組織設定で有効にしてください。",
"ai_not_in_plan": "AIデータ分析は現在のプランではご利用いただけません。この機能を利用するにはアップグレードしてください。",
"ai_query_placeholder": "例: 先週何人のユーザーが登録しましたか?",
"ai_query_section_description": "表示したい内容を説明すると、AIがチャートを作成します。",
"ai_query_section_title": "データに質問する",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Filter toevoegen",
"add_to_dashboard": "Toevoegen aan dashboard",
"advanced_chart_builder_config_prompt": "Configureer je grafiek en klik op \"Query uitvoeren\" om een voorbeeld te zien",
"ai_instance_not_configured": "AI is niet geconfigureerd op deze instantie. Neem contact op met je beheerder.",
"ai_not_available": "AI-data-analyse is niet beschikbaar.",
"ai_not_enabled": "AI-data-analyse is uitgeschakeld voor deze organisatie. Schakel het in bij de organisatie-instellingen.",
"ai_not_in_plan": "AI-data-analyse is niet beschikbaar in je huidige abonnement. Upgrade om deze functie te ontgrendelen.",
"ai_query_placeholder": "bijv. Hoeveel gebruikers hebben zich vorige week aangemeld?",
"ai_query_section_description": "Beschrijf wat je wilt zien en laat AI de grafiek bouwen.",
"ai_query_section_title": "Vraag het aan je data",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Adicionar filtro",
"add_to_dashboard": "Adicionar ao painel",
"advanced_chart_builder_config_prompt": "Configure seu gráfico e clique em \"Executar consulta\" para visualizar",
"ai_instance_not_configured": "A IA não está configurada nesta instância. Entre em contato com seu administrador.",
"ai_not_available": "A análise de dados com IA não está disponível.",
"ai_not_enabled": "A análise de dados com IA está desabilitada para esta organização. Habilite nas configurações da organização.",
"ai_not_in_plan": "A análise de dados com IA não está disponível no seu plano atual. Faça upgrade para desbloquear este recurso.",
"ai_query_placeholder": "ex: Quantos usuários se cadastraram na semana passada?",
"ai_query_section_description": "Descreva o que você quer ver e deixe a IA construir o gráfico.",
"ai_query_section_title": "Pergunte aos seus dados",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Adicionar filtro",
"add_to_dashboard": "Adicionar ao painel",
"advanced_chart_builder_config_prompt": "Configura o teu gráfico e clica em \"Executar consulta\" para pré-visualizar",
"ai_instance_not_configured": "A IA não está configurada nesta instância. Contacta o teu administrador.",
"ai_not_available": "A análise de dados por IA não está disponível.",
"ai_not_enabled": "A análise de dados por IA está desativada para esta organização. Ativa-a nas definições da organização.",
"ai_not_in_plan": "A análise de dados por IA não está disponível no teu plano atual. Faz upgrade para desbloquear esta funcionalidade.",
"ai_query_placeholder": "ex: Quantos utilizadores se registaram na semana passada?",
"ai_query_section_description": "Descreve o que queres ver e deixa a IA construir o gráfico.",
"ai_query_section_title": "Pergunta aos teus dados",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Adaugă filtru",
"add_to_dashboard": "Adaugă la Tablou de Bord",
"advanced_chart_builder_config_prompt": "Configurează graficul și apasă pe \"Rulează interogarea\" pentru previzualizare",
"ai_instance_not_configured": "AI nu este configurat pe această instanță. Contactează administratorul.",
"ai_not_available": "Analiza datelor cu AI nu este disponibilă.",
"ai_not_enabled": "Analiza datelor cu AI este dezactivată pentru această organizație. Activează-o în setările organizației.",
"ai_not_in_plan": "Analiza datelor cu AI nu este disponibilă în planul tău actual. Treci la un plan superior pentru a debloca această funcție.",
"ai_query_placeholder": "ex: Câți utilizatori s-au înscris săptămâna trecută?",
"ai_query_section_description": "Descrie ce vrei să vezi și lasă AI-ul să construiască graficul.",
"ai_query_section_title": "Întreabă-ți datele",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Добавить фильтр",
"add_to_dashboard": "Добавить на панель",
"advanced_chart_builder_config_prompt": "Настрой график и нажми «Выполнить запрос», чтобы посмотреть предварительный просмотр",
"ai_instance_not_configured": "ИИ не настроен на этом экземпляре. Свяжитесь с администратором.",
"ai_not_available": "Анализ данных с помощью ИИ недоступен.",
"ai_not_enabled": "Анализ данных с помощью ИИ отключён для этой организации. Включите его в настройках организации.",
"ai_not_in_plan": "Анализ данных с помощью ИИ недоступен в вашем текущем тарифе. Обновите тариф, чтобы получить эту функцию.",
"ai_query_placeholder": "например: Сколько пользователей зарегистрировались на прошлой неделе?",
"ai_query_section_description": "Опиши, что хочешь увидеть, и AI построит график.",
"ai_query_section_title": "Спроси свои данные",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Lägg till filter",
"add_to_dashboard": "Lägg till på instrumentpanelen",
"advanced_chart_builder_config_prompt": "Konfigurera ditt diagram och klicka på \"Kör fråga\" för att förhandsgranska",
"ai_instance_not_configured": "AI är inte konfigurerad på denna instans. Kontakta din administratör.",
"ai_not_available": "AI-dataanalys är inte tillgänglig.",
"ai_not_enabled": "AI-dataanalys är inaktiverad för denna organisation. Aktivera det i organisationsinställningarna.",
"ai_not_in_plan": "AI-dataanalys är inte tillgänglig i din nuvarande plan. Uppgradera för att låsa upp denna funktion.",
"ai_query_placeholder": "t.ex. Hur många användare registrerade sig förra veckan?",
"ai_query_section_description": "Beskriv vad du vill se så bygger AI diagrammet åt dig.",
"ai_query_section_title": "Fråga din data",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "Filtre ekle",
"add_to_dashboard": "Panoya Ekle",
"advanced_chart_builder_config_prompt": "Grafiğini yapılandır ve önizleme için \"Sorguyu Çalıştır\"a tıkla",
"ai_instance_not_configured": "Bu örnekte AI yapılandırılmamış. Yöneticinle iletişime geç.",
"ai_not_available": "AI veri analizi mevcut değil.",
"ai_not_enabled": "Bu organizasyon için AI veri analizi devre dışı. Organizasyon ayarlarından etkinleştir.",
"ai_not_in_plan": "AI veri analizi mevcut planında bulunmuyor. Bu özelliğin kilidini açmak için yükselt.",
"ai_query_placeholder": "örn. Geçen hafta kaç kullanıcı kaydoldu?",
"ai_query_section_description": "Ne görmek istediğini anlat, AI grafiği oluştursun.",
"ai_query_section_title": "Verilerine sor",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "添加过滤器",
"add_to_dashboard": "添加到 Dashboard",
"advanced_chart_builder_config_prompt": "配置你的图表,然后点击“运行查询”预览",
"ai_instance_not_configured": "此实例未配置 AI。请联系您的管理员。",
"ai_not_available": "AI 数据分析不可用。",
"ai_not_enabled": "此组织已禁用 AI 数据分析。请在组织设置中启用。",
"ai_not_in_plan": "您当前的套餐不包含 AI 数据分析。升级以解锁此功能。",
"ai_query_placeholder": "例如:上周有多少用户注册?",
"ai_query_section_description": "描述你想要看到的内容,让 AI 帮你生成图表。",
"ai_query_section_title": "向你的数据提问",
+4
View File
@@ -1662,6 +1662,10 @@
"add_filter": "新增篩選器",
"add_to_dashboard": "新增到儀表板",
"advanced_chart_builder_config_prompt": "設定你的圖表,然後點擊「執行查詢」預覽",
"ai_instance_not_configured": "此執行個體未設定 AI。請聯絡您的管理員。",
"ai_not_available": "AI 資料分析無法使用。",
"ai_not_enabled": "此組織已停用 AI 資料分析。請在組織設定中啟用。",
"ai_not_in_plan": "您目前的方案不包含 AI 資料分析。請升級以解鎖此功能。",
"ai_query_placeholder": "例如:上週有多少用戶註冊?",
"ai_query_section_description": "描述你想看到的內容,讓 AI 幫你建立圖表。",
"ai_query_section_title": "詢問你的數據",
@@ -11,7 +11,8 @@ const mocks = vi.hoisted(() => {
checkFeedbackDirectoryAccess: vi.fn(),
getIsDashboardsEnabled: vi.fn(),
createChart: vi.fn(),
createOpenAI: vi.fn(),
getAiModel: vi.fn(),
assertOrganizationAIConfigured: vi.fn(),
executeTenantScopedQuery: vi.fn(),
generateText: vi.fn(),
updateChart: vi.fn(),
@@ -64,8 +65,20 @@ vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
withAuditLogging: vi.fn((_eventName, _objectType, fn) => fn),
}));
vi.mock("@ai-sdk/openai", () => ({
createOpenAI: mocks.createOpenAI,
vi.mock("@formbricks/ai", () => ({
getAiModel: mocks.getAiModel,
}));
vi.mock("@/lib/ai/service", () => ({
assertOrganizationAIConfigured: mocks.assertOrganizationAIConfigured,
}));
vi.mock("@/lib/env", () => ({
env: {},
}));
vi.mock("@/modules/ee/analysis/lib/ai-schema-context", () => ({
generateSchemaContext: vi.fn(() => "schema context"),
}));
vi.mock("ai", () => ({
@@ -83,7 +96,6 @@ const ctx = {
describe("chart Cube actions", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubEnv("OPENAI_API_KEY", "openai-key");
mocks.actionClientAction.mockImplementation((fn) => fn);
mocks.actionClientInputSchema.mockReturnValue({ action: mocks.actionClientAction });
mocks.getIsDashboardsEnabled.mockResolvedValue(true);
@@ -94,7 +106,8 @@ describe("chart Cube actions", () => {
mocks.checkFeedbackDirectoryAccess.mockResolvedValue({
feedbackDirectoryId: "frd-1",
});
mocks.createOpenAI.mockReturnValue(() => "model");
mocks.assertOrganizationAIConfigured.mockResolvedValue(undefined);
mocks.getAiModel.mockReturnValue("model");
mocks.createChart.mockResolvedValue({
id: "chart-1",
name: "Chart",
@@ -110,7 +123,7 @@ describe("chart Cube actions", () => {
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
test("executeQueryAction delegates to the tenant-scoped Cube helper after authorization", async () => {
@@ -1,11 +1,13 @@
"use server";
import { createOpenAI } from "@ai-sdk/openai";
import { Output, generateText } from "ai";
import { z } from "zod";
import { getAiModel } from "@formbricks/ai";
import { type TChartQuery, ZChartQuery } from "@formbricks/types/analysis";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { assertOrganizationAIConfigured } from "@/lib/ai/service";
import { env } from "@/lib/env";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { executeTenantScopedQuery } from "@/modules/ee/analysis/api/lib/cube-client";
@@ -344,6 +346,9 @@ export const generateAIChartAction = authenticatedActionClient
await checkDashboardsEnabled(organizationId);
// Verify AI is entitled, enabled at org level, and configured at instance level
await assertOrganizationAIConfigured(organizationId, "dataAnalysis");
const { feedbackDirectoryId } = await checkFeedbackDirectoryAccess({
feedbackDirectoryId: parsedInput.feedbackDirectoryId,
organizationId,
@@ -352,15 +357,10 @@ export const generateAIChartAction = authenticatedActionClient
source: "charts.generateAIChartAction",
});
if (!process.env.OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY is not configured");
}
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const schemaContext = generateSchemaContext();
const { output } = await generateText({
model: openai("gpt-4o-mini"),
model: getAiModel(env),
output: Output.object({ schema: ZGenerateAIQueryResponse }),
system: schemaContext,
prompt: `User request: "${parsedInput.prompt}"`,
@@ -9,17 +9,22 @@ import { generateAIChartAction } from "@/modules/ee/analysis/charts/actions";
import type { AnalyticsResponse } from "@/modules/ee/analysis/types/analysis";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
interface AIQuerySectionProps {
workspaceId: string;
onChartGenerated: (data: AnalyticsResponse) => void;
feedbackDirectoryId: string;
isAIAvailable?: boolean;
aiUnavailableReason?: string;
}
export function AIQuerySection({
workspaceId,
onChartGenerated,
feedbackDirectoryId,
isAIAvailable = true,
aiUnavailableReason,
}: Readonly<AIQuerySectionProps>) {
const [userQuery, setUserQuery] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
@@ -70,16 +75,34 @@ export function AIQuerySection({
value={userQuery}
onChange={(e) => setUserQuery(e.target.value)}
maxLength={2000}
disabled={isGenerating}
disabled={!isAIAvailable || isGenerating}
/>
<Button
type="submit"
variant="default"
disabled={!userQuery.trim() || isGenerating}
loading={isGenerating}>
<WandSparklesIcon className="h-4 w-4" />
{t("workspace.analysis.charts.create_chart_with_ai")}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<Button
type="submit"
variant="default"
className="w-full"
disabled={!isAIAvailable || !userQuery.trim() || isGenerating}
loading={isGenerating}>
<WandSparklesIcon className="h-4 w-4" />
{t("workspace.analysis.charts.create_chart_with_ai")}
</Button>
</div>
</TooltipTrigger>
{!isAIAvailable && (
<TooltipContent>
{{
not_in_plan: t("workspace.analysis.charts.ai_not_in_plan"),
not_enabled: t("workspace.analysis.charts.ai_not_enabled"),
instance_not_configured: t("workspace.analysis.charts.ai_instance_not_configured"),
}[aiUnavailableReason ?? ""] ?? t("workspace.analysis.charts.ai_not_available")}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</form>
</div>
);
@@ -1,4 +1,6 @@
import { use } from "react";
import { getOrganizationAIConfig } from "@/lib/ai/service";
import type { TOrganizationAIConfig } from "@/lib/ai/service";
import { getConnectorsWithMappings } from "@/lib/connector/service";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
@@ -14,6 +16,15 @@ import { getIsDashboardsEnabled } from "@/modules/ee/license-check/lib/utils";
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
const getAIUnavailableReason = (
aiConfig: TOrganizationAIConfig
): "not_in_plan" | "not_enabled" | "instance_not_configured" | undefined => {
if (!aiConfig.isAIDataAnalysisEntitled) return "not_in_plan";
if (!aiConfig.isAIDataAnalysisEnabled) return "not_enabled";
if (!aiConfig.isInstanceConfigured) return "instance_not_configured";
return undefined;
};
interface ChartsListContentProps {
chartsPromise: Promise<TChartWithCreator[]>;
workspaceId: string;
@@ -71,10 +82,13 @@ export async function ChartsListPage({ workspaceId }: Readonly<ChartsListPagePro
);
}
const [directories, connectors] = await Promise.all([
const [directories, connectors, aiConfig] = await Promise.all([
getFeedbackDirectoriesByWorkspaceId(workspaceId),
getConnectorsWithMappings(workspaceId),
getOrganizationAIConfig(organization.id),
]);
const aiUnavailableReason = getAIUnavailableReason(aiConfig);
const isAIAvailable = !aiUnavailableReason;
const hasFeedbackRecords = await hasFeedbackRecordsInDirectories(
directories.map((directory) => directory.id)
);
@@ -90,6 +104,8 @@ export async function ChartsListPage({ workspaceId }: Readonly<ChartsListPagePro
workspaceId={workspaceId}
directories={directories}
buttonProps={{ disabled: !hasFeedbackRecords }}
isAIAvailable={isAIAvailable}
aiUnavailableReason={aiUnavailableReason}
/>
)
}>
@@ -14,6 +14,8 @@ interface CreateChartButtonProps {
onSuccess?: () => void;
showIcon?: boolean;
buttonProps?: Omit<ButtonProps, "onClick" | "children">;
isAIAvailable?: boolean;
aiUnavailableReason?: string;
}
export function CreateChartButton({
@@ -24,6 +26,8 @@ export function CreateChartButton({
onSuccess,
showIcon = true,
buttonProps,
isAIAvailable,
aiUnavailableReason,
}: Readonly<CreateChartButtonProps>) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const { t } = useTranslation();
@@ -41,6 +45,8 @@ export function CreateChartButton({
autoAddToDashboardId={autoAddToDashboardId}
directories={directories}
onSuccess={onSuccess}
isAIAvailable={isAIAvailable}
aiUnavailableReason={aiUnavailableReason}
/>
</>
);
@@ -12,6 +12,8 @@ export interface CreateChartDialogProps {
initialChart?: TChartWithCreator;
onSuccess?: () => void;
directories: { id: string; name: string }[];
isAIAvailable?: boolean;
aiUnavailableReason?: string;
}
export function CreateChartDialog({
@@ -23,6 +25,8 @@ export function CreateChartDialog({
initialChart,
onSuccess,
directories,
isAIAvailable,
aiUnavailableReason,
}: Readonly<CreateChartDialogProps>) {
return (
<CreateChartView
@@ -34,6 +38,8 @@ export function CreateChartDialog({
autoAddToDashboardId={autoAddToDashboardId}
onSuccess={onSuccess}
directories={directories}
isAIAvailable={isAIAvailable}
aiUnavailableReason={aiUnavailableReason}
/>
);
}
@@ -34,6 +34,8 @@ interface CreateChartViewProps {
autoAddToDashboardId?: string;
onSuccess?: () => void;
directories: { id: string; name: string }[];
isAIAvailable?: boolean;
aiUnavailableReason?: string;
}
export function CreateChartView({
@@ -45,6 +47,8 @@ export function CreateChartView({
autoAddToDashboardId,
onSuccess,
directories,
isAIAvailable,
aiUnavailableReason,
}: Readonly<CreateChartViewProps>) {
const { t } = useTranslation();
const isEditing = !!chartId;
@@ -150,6 +154,8 @@ export function CreateChartView({
workspaceId={workspaceId}
onChartGenerated={handleChartGenerated}
feedbackDirectoryId={selectedDirectoryId}
isAIAvailable={isAIAvailable}
aiUnavailableReason={aiUnavailableReason}
/>
<div className="relative">
@@ -30,6 +30,8 @@ interface AddExistingChartsDialogProps {
directories: { id: string; name: string }[];
existingChartIds: string[];
onSuccess: () => void;
isAIAvailable?: boolean;
aiUnavailableReason?: string;
}
interface ChartOption {
@@ -45,6 +47,8 @@ export function AddExistingChartsDialog({
directories,
existingChartIds,
onSuccess,
isAIAvailable,
aiUnavailableReason,
}: Readonly<AddExistingChartsDialogProps>) {
const { t } = useTranslation();
const router = useRouter();
@@ -160,6 +164,8 @@ export function AddExistingChartsDialog({
onSuccess();
}}
buttonProps={{ variant: "secondary", size: "default", disabled: isAdding }}
isAIAvailable={isAIAvailable}
aiUnavailableReason={aiUnavailableReason}
/>
<div className="flex flex-col-reverse gap-2 sm:flex-row">
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isAdding}>
@@ -21,6 +21,8 @@ interface DashboardControlBarProps {
isSaving: boolean;
hasChanges: boolean;
isReadOnly: boolean;
isAIAvailable?: boolean;
aiUnavailableReason?: string;
onRefresh: () => void;
onEditToggle: () => void;
onSave: () => void;
@@ -36,6 +38,8 @@ export const DashboardControlBar = ({
isSaving,
hasChanges,
isReadOnly,
isAIAvailable,
aiUnavailableReason,
onRefresh,
onEditToggle,
onSave,
@@ -139,6 +143,8 @@ export const DashboardControlBar = ({
setIsAddExistingDialogOpen(false);
router.refresh();
}}
isAIAvailable={isAIAvailable}
aiUnavailableReason={aiUnavailableReason}
/>
</>
);
@@ -34,6 +34,8 @@ interface DashboardDetailClientProps {
>;
directories: { id: string; name: string }[];
isReadOnly: boolean;
isAIAvailable: boolean;
aiUnavailableReason?: string;
}
const widgetsToLayout = (widgets: TDashboardWidget[]): LayoutItem[] => {
@@ -151,6 +153,8 @@ export function DashboardDetailClient({
widgetDataPromises,
directories,
isReadOnly,
isAIAvailable,
aiUnavailableReason,
}: Readonly<DashboardDetailClientProps>) {
const router = useRouter();
const { t } = useTranslation();
@@ -297,6 +301,8 @@ export function DashboardDetailClient({
isSaving={isSaving}
hasChanges={hasChanges}
isReadOnly={isReadOnly}
isAIAvailable={isAIAvailable}
aiUnavailableReason={aiUnavailableReason}
onRefresh={() => router.refresh()}
onEditToggle={handleEnterEditMode}
onSave={handleSave}
@@ -363,6 +369,8 @@ export function DashboardDetailClient({
router.refresh();
}}
directories={directories}
isAIAvailable={isAIAvailable}
aiUnavailableReason={aiUnavailableReason}
/>
)}
</PageContentWrapper>
@@ -2,6 +2,7 @@ import { notFound } from "next/navigation";
import { logger } from "@formbricks/logger";
import type { TChartQuery } from "@formbricks/types/analysis";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { type TOrganizationAIConfig, getOrganizationAIConfig } from "@/lib/ai/service";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { executeTenantScopedQuery } from "@/modules/ee/analysis/api/lib/cube-client";
@@ -16,6 +17,15 @@ import { DashboardDetailClient } from "../components/dashboard-detail-client";
import { getDashboard } from "../lib/dashboards";
import { DASHBOARD_WIDGET_LOAD_ERROR, type TDashboardWidgetError } from "../lib/widget-errors";
const getAIUnavailableReason = (
aiConfig: TOrganizationAIConfig
): "not_in_plan" | "not_enabled" | "instance_not_configured" | undefined => {
if (!aiConfig.isAIDataAnalysisEntitled) return "not_in_plan";
if (!aiConfig.isAIDataAnalysisEnabled) return "not_enabled";
if (!aiConfig.isInstanceConfigured) return "instance_not_configured";
return undefined;
};
type TDashboardDetail = Awaited<ReturnType<typeof getDashboard>>;
type TDashboardWidget = TDashboardDetail["widgets"][number];
type TDashboardWidgetWithChart = TDashboardWidget & { chart: NonNullable<TDashboardWidget["chart"]> };
@@ -96,7 +106,12 @@ export async function DashboardDetailPage({
);
}
const directories = await getFeedbackDirectoriesByWorkspaceId(workspaceId);
const [directories, aiConfig] = await Promise.all([
getFeedbackDirectoriesByWorkspaceId(workspaceId),
getOrganizationAIConfig(organization.id),
]);
const aiUnavailableReason = getAIUnavailableReason(aiConfig);
const isAIAvailable = !aiUnavailableReason;
let dashboard;
try {
@@ -132,6 +147,8 @@ export async function DashboardDetailPage({
widgetDataPromises={widgetDataPromises}
directories={directories}
isReadOnly={isReadOnly}
isAIAvailable={isAIAvailable}
aiUnavailableReason={aiUnavailableReason}
/>
);
}