refactor: structure

This commit is contained in:
Dhruwang
2026-02-18 16:00:50 +05:30
parent b03eaf03b1
commit 2a89766500
51 changed files with 873 additions and 569 deletions

View File

@@ -1,4 +1,4 @@
import { ChartsListSkeleton } from "@/modules/ee/analysis/components/charts-list-skeleton";
import { ChartsListSkeleton } from "@/modules/ee/analysis/charts/components/charts-list-skeleton";
export default function ChartsListLoading() {
return <ChartsListSkeleton />;

View File

@@ -1 +1 @@
export { ChartsListPage as default } from "@/modules/ee/analysis/pages/charts-list-page";
export { ChartsListPage as default } from "@/modules/ee/analysis/charts/pages/charts-list-page";

View File

@@ -1,3 +1,3 @@
import { DashboardsListPage } from "@/modules/ee/analysis/pages/dashboards-list-page";
import { DashboardsListPage } from "@/modules/ee/analysis/dashboards/pages/dashboards-list-page";
export default DashboardsListPage;

View File

@@ -1,3 +1,3 @@
import { DashboardDetailPage } from "@/modules/ee/analysis/pages/dashboard-detail-page";
import { DashboardDetailPage } from "@/modules/ee/analysis/dashboards/pages/dashboard-detail-page";
export default DashboardDetailPage;

View File

@@ -622,6 +622,8 @@
},
"analysis": {
"charts": {
"edit_chart_description": "View and edit your chart configuration.",
"edit_chart_title": "Edit Chart",
"chart_deleted_successfully": "Chart deleted successfully",
"chart_duplicated_successfully": "Chart duplicated successfully",
"chart_duplication_error": "Failed to duplicate chart",

View File

@@ -10,9 +10,7 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-clie
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import { ZChartConfig, ZChartType, ZCubeQuery, ZDashboardStatus, ZWidgetLayout } from "./types/analysis";
// --- Chart actions ---
import { ZChartConfig, ZChartType, ZCubeQuery } from "../types/analysis";
const ZCreateChartAction = z.object({
environmentId: ZId,
@@ -205,265 +203,6 @@ export const duplicateChartAction = authenticatedActionClient.schema(ZDuplicateC
)
);
// --- Dashboard widget actions ---
const ZAddChartToDashboardAction = z.object({
environmentId: ZId,
chartId: ZId,
dashboardId: ZId,
title: z.string().optional(),
layout: ZWidgetLayout.optional().default({ x: 0, y: 0, w: 4, h: 3 }),
});
export const addChartToDashboardAction = authenticatedActionClient.schema(ZAddChartToDashboardAction).action(
withAuditLogging(
"created",
"dashboardWidget",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZAddChartToDashboardAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId,
},
],
});
const [chart, dashboard] = await Promise.all([
prisma.chart.findFirst({
where: { id: parsedInput.chartId, projectId },
}),
prisma.dashboard.findFirst({
where: { id: parsedInput.dashboardId, projectId },
}),
]);
if (!chart) {
throw new Error("Chart not found");
}
if (!dashboard) {
throw new Error("Dashboard not found");
}
const maxOrder = await prisma.dashboardWidget.aggregate({
where: { dashboardId: parsedInput.dashboardId },
_max: { order: true },
});
const widget = await prisma.dashboardWidget.create({
data: {
dashboardId: parsedInput.dashboardId,
chartId: parsedInput.chartId,
type: "chart",
title: parsedInput.title,
layout: parsedInput.layout,
order: (maxOrder._max.order ?? -1) + 1,
},
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.newObject = widget;
return widget;
}
)
);
// --- Dashboard actions ---
const ZCreateDashboardAction = z.object({
environmentId: ZId,
name: z.string().min(1),
description: z.string().optional(),
});
export const createDashboardAction = authenticatedActionClient.schema(ZCreateDashboardAction).action(
withAuditLogging(
"created",
"dashboard",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZCreateDashboardAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId,
},
],
});
const dashboard = await prisma.dashboard.create({
data: {
name: parsedInput.name,
description: parsedInput.description,
projectId,
status: "draft",
createdBy: ctx.user.id,
},
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.newObject = dashboard;
return dashboard;
}
)
);
const ZUpdateDashboardAction = z.object({
environmentId: ZId,
dashboardId: ZId,
name: z.string().min(1).optional(),
description: z.string().optional().nullable(),
status: ZDashboardStatus.optional(),
});
export const updateDashboardAction = authenticatedActionClient.schema(ZUpdateDashboardAction).action(
withAuditLogging(
"updated",
"dashboard",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZUpdateDashboardAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId,
},
],
});
const dashboard = await prisma.dashboard.findFirst({
where: { id: parsedInput.dashboardId, projectId },
});
if (!dashboard) {
throw new Error("Dashboard not found");
}
const updatedDashboard = await prisma.dashboard.update({
where: { id: parsedInput.dashboardId },
data: {
...(parsedInput.name !== undefined && { name: parsedInput.name }),
...(parsedInput.description !== undefined && { description: parsedInput.description }),
...(parsedInput.status !== undefined && { status: parsedInput.status }),
},
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.oldObject = dashboard;
ctx.auditLoggingCtx.newObject = updatedDashboard;
return updatedDashboard;
}
)
);
const ZDeleteDashboardAction = z.object({
environmentId: ZId,
dashboardId: ZId,
});
export const deleteDashboardAction = authenticatedActionClient.schema(ZDeleteDashboardAction).action(
withAuditLogging(
"deleted",
"dashboard",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZDeleteDashboardAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId,
},
],
});
const dashboard = await prisma.dashboard.findFirst({
where: { id: parsedInput.dashboardId, projectId },
});
if (!dashboard) {
throw new Error("Dashboard not found");
}
await prisma.dashboardWidget.deleteMany({
where: { dashboardId: parsedInput.dashboardId },
});
await prisma.dashboard.delete({
where: { id: parsedInput.dashboardId },
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.oldObject = dashboard;
return { success: true };
}
)
);
const ZDeleteChartAction = z.object({
environmentId: ZId,
chartId: ZId,
@@ -523,58 +262,6 @@ export const deleteChartAction = authenticatedActionClient.schema(ZDeleteChartAc
)
);
// --- Read actions ---
const ZGetDashboardsAction = z.object({
environmentId: ZId,
});
export const getDashboardsAction = authenticatedActionClient
.schema(ZGetDashboardsAction)
.action(
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZGetDashboardsAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "read",
projectId,
},
],
});
const dashboards = await prisma.dashboard.findMany({
where: { projectId },
orderBy: { createdAt: "desc" },
select: {
id: true,
name: true,
description: true,
status: true,
createdAt: true,
updatedAt: true,
},
});
return dashboards;
}
);
const ZGetChartAction = z.object({
environmentId: ZId,
chartId: ZId,
@@ -686,8 +373,6 @@ export const getChartsAction = authenticatedActionClient
}
);
// --- Query execution ---
const ZExecuteQueryAction = z.object({
environmentId: ZId,
query: ZCubeQuery,

View File

@@ -8,12 +8,8 @@ import { toast } from "react-hot-toast";
import { AnalyticsResponse } from "@/app/api/analytics/_lib/types";
import { Button } from "@/modules/ui/components/button";
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
import {
addChartToDashboardAction,
createChartAction,
executeQueryAction,
getDashboardsAction,
} from "../../actions";
import { createChartAction, executeQueryAction } from "../../actions";
import { addChartToDashboardAction, getDashboardsAction } from "@/modules/ee/analysis/dashboards/actions";
import { CHART_TYPES } from "../../lib/chart-types";
import { mapChartType } from "../../lib/chart-utils";
import {
@@ -23,8 +19,8 @@ import {
TimeDimensionConfig,
buildCubeQuery,
parseQueryToState,
} from "../../lib/query-builder";
import { TCubeQuery } from "../../types/analysis";
} from "@/modules/ee/analysis/lib/query-builder";
import { TCubeQuery } from "@/modules/ee/analysis/types/analysis";
import { AddToDashboardDialog } from "./add-to-dashboard-dialog";
import { ChartRenderer } from "./chart-renderer";
import { DimensionsPanel } from "./dimensions-panel";

View File

@@ -6,13 +6,12 @@ import { toast } from "react-hot-toast";
import { AnalyticsResponse } from "@/app/api/analytics/_lib/types";
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
import {
addChartToDashboardAction,
createChartAction,
executeQueryAction,
getChartAction,
getDashboardsAction,
updateChartAction,
} from "../../actions";
import { addChartToDashboardAction, getDashboardsAction } from "@/modules/ee/analysis/dashboards/actions";
import { mapChartType, mapDatabaseChartTypeToApi } from "../../lib/chart-utils";
import { AddToDashboardDialog } from "./add-to-dashboard-dialog";
import { AIQuerySection } from "./ai-query-section";

View File

@@ -1,7 +1,7 @@
"use client";
import { MultiSelect } from "@/modules/ui/components/multi-select";
import { FEEDBACK_FIELDS } from "../../lib/schema-definition";
import { FEEDBACK_FIELDS } from "@/modules/ee/analysis/lib/schema-definition";
interface DimensionsPanelProps {
selectedDimensions: string[];

View File

@@ -10,8 +10,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { FilterRow } from "../../lib/query-builder";
import { FEEDBACK_FIELDS, getFieldById, getFilterOperatorsForType } from "../../lib/schema-definition";
import { FilterRow } from "@/modules/ee/analysis/lib/query-builder";
import { FEEDBACK_FIELDS, getFieldById, getFilterOperatorsForType } from "@/modules/ee/analysis/lib/schema-definition";
interface FiltersPanelProps {
filters: FilterRow[];

View File

@@ -11,8 +11,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { CustomMeasure } from "../../lib/query-builder";
import { FEEDBACK_FIELDS } from "../../lib/schema-definition";
import { CustomMeasure } from "@/modules/ee/analysis/lib/query-builder";
import { FEEDBACK_FIELDS } from "@/modules/ee/analysis/lib/schema-definition";
interface MeasuresPanelProps {
selectedMeasures: string[];

View File

@@ -14,8 +14,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { TimeDimensionConfig } from "../../lib/query-builder";
import { DATE_PRESETS, FEEDBACK_FIELDS, TIME_GRANULARITIES } from "../../lib/schema-definition";
import { TimeDimensionConfig } from "@/modules/ee/analysis/lib/query-builder";
import { DATE_PRESETS, FEEDBACK_FIELDS, TIME_GRANULARITIES } from "@/modules/ee/analysis/lib/schema-definition";
interface TimeDimensionPanelProps {
timeDimension: TimeDimensionConfig | null;

View File

@@ -0,0 +1,76 @@
"use client";
import { PlusIcon, SaveIcon } from "lucide-react";
import { Button } from "@/modules/ui/components/button";
import { DialogFooter } from "@/modules/ui/components/dialog";
import { AddToDashboardDialog } from "./chart-builder/add-to-dashboard-dialog";
import { SaveChartDialog } from "./chart-builder/save-chart-dialog";
interface ChartDialogFooterWithModalsProps {
chartName: string;
onChartNameChange: (name: string) => void;
dashboards: Array<{ id: string; name: string }>;
selectedDashboardId: string;
onDashboardSelect: (id: string) => void;
onAddToDashboard: () => void;
onSave: () => void;
isSaving: boolean;
isSaveDialogOpen: boolean;
onSaveDialogOpenChange: (open: boolean) => void;
isAddToDashboardDialogOpen: boolean;
onAddToDashboardDialogOpenChange: (open: boolean) => void;
}
export function ChartDialogFooterWithModals({
chartName,
onChartNameChange,
dashboards,
selectedDashboardId,
onDashboardSelect,
onAddToDashboard,
onSave,
isSaving,
isSaveDialogOpen,
onSaveDialogOpenChange,
isAddToDashboardDialogOpen,
onAddToDashboardDialogOpenChange,
}: ChartDialogFooterWithModalsProps) {
return (
<>
<DialogFooter>
<Button
variant="outline"
onClick={() => onAddToDashboardDialogOpenChange(true)}
disabled={isSaving}>
<PlusIcon className="mr-2 h-4 w-4" />
Add to Dashboard
</Button>
<Button onClick={() => onSaveDialogOpenChange(true)} disabled={isSaving}>
<SaveIcon className="mr-2 h-4 w-4" />
Save Chart
</Button>
</DialogFooter>
<SaveChartDialog
open={isSaveDialogOpen}
onOpenChange={onSaveDialogOpenChange}
chartName={chartName}
onChartNameChange={onChartNameChange}
onSave={onSave}
isSaving={isSaving}
/>
<AddToDashboardDialog
open={isAddToDashboardDialogOpen}
onOpenChange={onAddToDashboardDialogOpenChange}
chartName={chartName}
onChartNameChange={onChartNameChange}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={onDashboardSelect}
onAdd={onAddToDashboard}
isSaving={isSaving}
/>
</>
);
}

View File

@@ -0,0 +1,21 @@
"use client";
import { Dialog, DialogContent } from "@/modules/ui/components/dialog";
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
interface ChartDialogLoadingViewProps {
open: boolean;
onClose: () => void;
}
export function ChartDialogLoadingView({ open, onClose }: ChartDialogLoadingViewProps) {
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="max-[90vw] max-h-[90vh] overflow-y-auto">
<div className="flex h-64 items-center justify-center">
<LoadingSpinner />
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -14,7 +14,7 @@ import {
} from "@/modules/ui/components/dropdown-menu";
import { Button } from "@/modules/ui/components/button";
import { deleteChartAction, duplicateChartAction } from "../actions";
import { TChart } from "../types/analysis";
import { TChart } from "../../types/analysis";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { useRouter } from "next/navigation";

View File

@@ -5,7 +5,7 @@ import { BarChart3Icon } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { CHART_TYPE_ICONS } from "../lib/chart-types";
import { TChart } from "../types/analysis";
import { TChart } from "../../types/analysis";
import { ChartDropdownMenu } from "./chart-dropdown-menu";
import { CreateChartDialog } from "./create-chart-dialog";

View File

@@ -0,0 +1,112 @@
"use client";
import { useCreateChartDialog } from "../hooks/use-create-chart-dialog";
import { CreateChartView } from "./create-chart-view";
import { ChartDialogLoadingView } from "./chart-dialog-loading-view";
import { EditChartView } from "./edit-chart-view";
export interface CreateChartDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
environmentId: string;
chartId?: string;
onSuccess?: () => void;
}
export function CreateChartDialog({
open,
onOpenChange,
environmentId,
chartId,
onSuccess,
}: Readonly<CreateChartDialogProps>) {
const hook = useCreateChartDialog({
open,
onOpenChange,
environmentId,
chartId,
onSuccess,
});
const {
chartData,
chartName,
setChartName,
selectedChartType,
setSelectedChartType,
isSaveDialogOpen,
setIsSaveDialogOpen,
isAddToDashboardDialogOpen,
setIsAddToDashboardDialogOpen,
dashboards,
selectedDashboardId,
setSelectedDashboardId,
isSaving,
isLoadingChart,
shouldShowAdvancedBuilder,
handleChartGenerated,
handleSaveChart,
handleAddToDashboard,
handleClose,
handleAdvancedBuilderSave,
handleAdvancedBuilderAddToDashboard,
} = hook;
if (chartId && isLoadingChart) {
return <ChartDialogLoadingView open={open} onClose={handleClose} />;
}
if (chartId && chartData) {
return (
<EditChartView
open={open}
onClose={handleClose}
environmentId={environmentId}
chartData={chartData}
chartName={chartName}
onChartNameChange={setChartName}
onChartGenerated={handleChartGenerated}
onAdvancedBuilderSave={handleAdvancedBuilderSave}
onAdvancedBuilderAddToDashboard={handleAdvancedBuilderAddToDashboard}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={setSelectedDashboardId}
onAddToDashboard={handleAddToDashboard}
onSave={handleSaveChart}
isSaving={isSaving}
isSaveDialogOpen={isSaveDialogOpen}
onSaveDialogOpenChange={setIsSaveDialogOpen}
isAddToDashboardDialogOpen={isAddToDashboardDialogOpen}
onAddToDashboardDialogOpenChange={setIsAddToDashboardDialogOpen}
/>
);
}
return (
<CreateChartView
open={open}
onClose={handleClose}
environmentId={environmentId}
chartId={chartId}
chartData={chartData}
chartName={chartName}
onChartNameChange={setChartName}
selectedChartType={selectedChartType}
onSelectedChartTypeChange={setSelectedChartType}
shouldShowAdvancedBuilder={shouldShowAdvancedBuilder}
onChartGenerated={handleChartGenerated}
onAdvancedBuilderSave={handleAdvancedBuilderSave}
onAdvancedBuilderAddToDashboard={handleAdvancedBuilderAddToDashboard}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={setSelectedDashboardId}
onAddToDashboard={handleAddToDashboard}
onSave={handleSaveChart}
isSaving={isSaving}
isSaveDialogOpen={isSaveDialogOpen}
onSaveDialogOpenChange={setIsSaveDialogOpen}
isAddToDashboardDialogOpen={isAddToDashboardDialogOpen}
onAddToDashboardDialogOpenChange={setIsAddToDashboardDialogOpen}
/>
);
}

View File

@@ -0,0 +1,131 @@
"use client";
import { Dialog, DialogBody, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/modules/ui/components/dialog";
import { AnalyticsResponse } from "@/app/api/analytics/_lib/types";
import { TCubeQuery } from "../../types/analysis";
import { AdvancedChartBuilder } from "./chart-builder/advanced-chart-builder";
import { AIQuerySection } from "./chart-builder/ai-query-section";
import { ChartPreview } from "./chart-builder/chart-preview";
import { ManualChartBuilder } from "./chart-builder/manual-chart-builder";
import { ChartDialogFooterWithModals } from "./chart-dialog-footer-with-modals";
interface CreateChartViewProps {
open: boolean;
onClose: () => void;
environmentId: string;
chartId?: string;
chartData: AnalyticsResponse | null;
chartName: string;
onChartNameChange: (name: string) => void;
selectedChartType: string;
onSelectedChartTypeChange: (type: string) => void;
shouldShowAdvancedBuilder: boolean;
onChartGenerated: (data: AnalyticsResponse) => void;
onAdvancedBuilderSave: (savedChartId: string) => void;
onAdvancedBuilderAddToDashboard: (savedChartId: string, _dashboardId?: string) => void;
dashboards: Array<{ id: string; name: string }>;
selectedDashboardId: string;
onDashboardSelect: (id: string) => void;
onAddToDashboard: () => void;
onSave: () => void;
isSaving: boolean;
isSaveDialogOpen: boolean;
onSaveDialogOpenChange: (open: boolean) => void;
isAddToDashboardDialogOpen: boolean;
onAddToDashboardDialogOpenChange: (open: boolean) => void;
}
export function CreateChartView({
open,
onClose,
environmentId,
chartId,
chartData,
chartName,
onChartNameChange,
selectedChartType,
onSelectedChartTypeChange,
shouldShowAdvancedBuilder,
onChartGenerated,
onAdvancedBuilderSave,
onAdvancedBuilderAddToDashboard,
dashboards,
selectedDashboardId,
onDashboardSelect,
onAddToDashboard,
onSave,
isSaving,
isSaveDialogOpen,
onSaveDialogOpenChange,
isAddToDashboardDialogOpen,
onAddToDashboardDialogOpenChange,
}: Readonly<CreateChartViewProps>) {
const handleAdvancedChartGenerated = (data: AnalyticsResponse) => {
onChartGenerated(data);
};
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="max-h-[90vh] overflow-y-auto" width="wide">
<DialogHeader>
<DialogTitle>{chartId ? "Edit Chart" : "Create Chart"}</DialogTitle>
<DialogDescription>
{chartId
? "View and edit your chart configuration."
: "Use AI to generate a chart or build one manually."}
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="grid gap-4">
<AIQuerySection onChartGenerated={onChartGenerated} />
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-200" />
</div>
<div className="relative flex justify-center">
<span className="bg-gray-50 px-2 text-sm text-gray-500">OR</span>
</div>
</div>
<ManualChartBuilder
selectedChartType={selectedChartType}
onChartTypeSelect={onSelectedChartTypeChange}
/>
{shouldShowAdvancedBuilder && (
<AdvancedChartBuilder
environmentId={environmentId}
initialChartType={selectedChartType || chartData?.chartType || ""}
initialQuery={chartData?.query as TCubeQuery | undefined}
hidePreview={true}
onChartGenerated={handleAdvancedChartGenerated}
onSave={onAdvancedBuilderSave}
onAddToDashboard={onAdvancedBuilderAddToDashboard}
/>
)}
{chartData && <ChartPreview chartData={chartData} />}
</div>
</DialogBody>
{chartData && (
<ChartDialogFooterWithModals
chartName={chartName}
onChartNameChange={onChartNameChange}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={onDashboardSelect}
onAddToDashboard={onAddToDashboard}
onSave={onSave}
isSaving={isSaving}
isSaveDialogOpen={isSaveDialogOpen}
onSaveDialogOpenChange={onSaveDialogOpenChange}
isAddToDashboardDialogOpen={isAddToDashboardDialogOpen}
onAddToDashboardDialogOpenChange={onAddToDashboardDialogOpenChange}
/>
)}
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,93 @@
"use client";
import { useTranslation } from "react-i18next";
import { Dialog, DialogBody, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/modules/ui/components/dialog";
import { AnalyticsResponse } from "@/app/api/analytics/_lib/types";
import { TCubeQuery } from "../../types/analysis";
import { AdvancedChartBuilder } from "./chart-builder/advanced-chart-builder";
import { ChartPreview } from "./chart-builder/chart-preview";
import { ChartDialogFooterWithModals } from "./chart-dialog-footer-with-modals";
interface EditChartViewProps {
open: boolean;
onClose: () => void;
environmentId: string;
chartData: AnalyticsResponse;
chartName: string;
onChartNameChange: (name: string) => void;
onChartGenerated: (data: AnalyticsResponse) => void;
onAdvancedBuilderSave: (savedChartId: string) => void;
onAdvancedBuilderAddToDashboard: (savedChartId: string, dashboardId?: string) => void;
dashboards: Array<{ id: string; name: string }>;
selectedDashboardId: string;
onDashboardSelect: (id: string) => void;
onAddToDashboard: () => void;
onSave: () => void;
isSaving: boolean;
isSaveDialogOpen: boolean;
onSaveDialogOpenChange: (open: boolean) => void;
isAddToDashboardDialogOpen: boolean;
onAddToDashboardDialogOpenChange: (open: boolean) => void;
}
export function EditChartView({
open,
onClose,
environmentId,
chartData,
chartName,
onChartNameChange,
onChartGenerated,
onAdvancedBuilderSave,
onAdvancedBuilderAddToDashboard,
dashboards,
selectedDashboardId,
onDashboardSelect,
onAddToDashboard,
onSave,
isSaving,
isSaveDialogOpen,
onSaveDialogOpenChange,
isAddToDashboardDialogOpen,
onAddToDashboardDialogOpenChange,
}: Readonly<EditChartViewProps>) {
const { t } = useTranslation();
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-7xl">
<DialogHeader>
<DialogTitle>{t("environments.analysis.charts.edit_chart_title")}</DialogTitle>
<DialogDescription>{t("environments.analysis.charts.edit_chart_description")}</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="grid gap-4 px-1">
<AdvancedChartBuilder
environmentId={environmentId}
initialChartType={chartData.chartType || ""}
initialQuery={chartData.query as TCubeQuery | undefined}
hidePreview={true}
onChartGenerated={onChartGenerated}
onSave={onAdvancedBuilderSave}
onAddToDashboard={onAdvancedBuilderAddToDashboard}
/>
<ChartPreview chartData={chartData} />
</div>
</DialogBody>
<ChartDialogFooterWithModals
chartName={chartName}
onChartNameChange={onChartNameChange}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={onDashboardSelect}
onAddToDashboard={onAddToDashboard}
onSave={onSave}
isSaving={isSaving}
isSaveDialogOpen={isSaveDialogOpen}
onSaveDialogOpenChange={onSaveDialogOpenChange}
isAddToDashboardDialogOpen={isAddToDashboardDialogOpen}
onAddToDashboardDialogOpenChange={onAddToDashboardDialogOpenChange}
/>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,38 +1,18 @@
"use client";
import { PlusIcon, SaveIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import toast from "react-hot-toast";
import { AnalyticsResponse } from "@/app/api/analytics/_lib/types";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
import {
addChartToDashboardAction,
createChartAction,
executeQueryAction,
getChartAction,
getDashboardsAction,
updateChartAction,
} from "../actions";
import { addChartToDashboardAction, getDashboardsAction } from "@/modules/ee/analysis/dashboards/actions";
import { mapChartType, mapDatabaseChartTypeToApi } from "../lib/chart-utils";
import { TCubeQuery } from "../types/analysis";
import { AddToDashboardDialog } from "./chart-builder/add-to-dashboard-dialog";
import { AdvancedChartBuilder } from "./chart-builder/advanced-chart-builder";
import { AIQuerySection } from "./chart-builder/ai-query-section";
import { ChartPreview } from "./chart-builder/chart-preview";
import { ManualChartBuilder } from "./chart-builder/manual-chart-builder";
import { SaveChartDialog } from "./chart-builder/save-chart-dialog";
interface CreateChartDialogProps {
export interface UseCreateChartDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
environmentId: string;
@@ -40,13 +20,13 @@ interface CreateChartDialogProps {
onSuccess?: () => void;
}
export function CreateChartDialog({
export function useCreateChartDialog({
open,
onOpenChange,
environmentId,
chartId,
onSuccess,
}: CreateChartDialogProps) {
}: UseCreateChartDialogProps) {
const [selectedChartType, setSelectedChartType] = useState<string>("");
const [chartData, setChartData] = useState<AnalyticsResponse | null>(null);
const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
@@ -255,161 +235,43 @@ export function CreateChartDialog({
}
};
if (chartId && isLoadingChart) {
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-[90vw] max-h-[90vh] overflow-y-auto">
<div className="flex h-64 items-center justify-center">
<LoadingSpinner />
</div>
</DialogContent>
</Dialog>
);
}
const handleAdvancedBuilderSave = (savedChartId: string) => {
setCurrentChartId(savedChartId);
setIsSaveDialogOpen(false);
onOpenChange(false);
onSuccess?.();
};
if (chartId && chartData) {
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-7xl">
<DialogHeader>
<DialogTitle>Edit Chart</DialogTitle>
<DialogDescription>View and edit your chart configuration.</DialogDescription>
</DialogHeader>
<DialogBody>
<ChartPreview chartData={chartData} />
</DialogBody>
const handleAdvancedBuilderAddToDashboard = (savedChartId: string, _dashboardId?: string) => {
setCurrentChartId(savedChartId);
setIsAddToDashboardDialogOpen(false);
onOpenChange(false);
onSuccess?.();
};
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddToDashboardDialogOpen(true)} disabled={isSaving}>
<PlusIcon className="mr-2 h-4 w-4" />
Add to Dashboard
</Button>
<Button onClick={() => setIsSaveDialogOpen(true)} disabled={isSaving}>
<SaveIcon className="mr-2 h-4 w-4" />
Save Chart
</Button>
</DialogFooter>
<SaveChartDialog
open={isSaveDialogOpen}
onOpenChange={setIsSaveDialogOpen}
chartName={chartName}
onChartNameChange={setChartName}
onSave={handleSaveChart}
isSaving={isSaving}
/>
<AddToDashboardDialog
open={isAddToDashboardDialogOpen}
onOpenChange={setIsAddToDashboardDialogOpen}
chartName={chartName}
onChartNameChange={setChartName}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={setSelectedDashboardId}
onAdd={handleAddToDashboard}
isSaving={isSaving}
/>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-h-[90vh] overflow-y-auto" width="wide">
<DialogHeader>
<DialogTitle>{chartId ? "Edit Chart" : "Create Chart"}</DialogTitle>
<DialogDescription>
{chartId
? "View and edit your chart configuration."
: "Use AI to generate a chart or build one manually."}
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="grid gap-4">
<AIQuerySection onChartGenerated={handleChartGenerated} />
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-200" />
</div>
<div className="relative flex justify-center">
<span className="bg-gray-50 px-2 text-sm text-gray-500">OR</span>
</div>
</div>
<ManualChartBuilder
selectedChartType={selectedChartType}
onChartTypeSelect={setSelectedChartType}
/>
{shouldShowAdvancedBuilder && (
<AdvancedChartBuilder
environmentId={environmentId}
initialChartType={selectedChartType || chartData?.chartType || ""}
initialQuery={chartData?.query as TCubeQuery | undefined}
hidePreview={true}
onChartGenerated={(data) => {
setChartData(data);
setChartName(data.chartType ? `Chart ${new Date().toLocaleString()}` : "");
if (data.chartType) {
setSelectedChartType(data.chartType);
}
}}
onSave={(savedChartId) => {
setCurrentChartId(savedChartId);
setIsSaveDialogOpen(false);
onOpenChange(false);
onSuccess?.();
}}
onAddToDashboard={(savedChartId) => {
setCurrentChartId(savedChartId);
setIsAddToDashboardDialogOpen(false);
onOpenChange(false);
onSuccess?.();
}}
/>
)}
{chartData && <ChartPreview chartData={chartData} />}
</div>
</DialogBody>
{chartData && (
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddToDashboardDialogOpen(true)} disabled={isSaving}>
<PlusIcon className="mr-2 h-4 w-4" />
Add to Dashboard
</Button>
<Button onClick={() => setIsSaveDialogOpen(true)} disabled={isSaving}>
<SaveIcon className="mr-2 h-4 w-4" />
Save Chart
</Button>
</DialogFooter>
)}
<SaveChartDialog
open={isSaveDialogOpen}
onOpenChange={setIsSaveDialogOpen}
chartName={chartName}
onChartNameChange={setChartName}
onSave={handleSaveChart}
isSaving={isSaving}
/>
<AddToDashboardDialog
open={isAddToDashboardDialogOpen}
onOpenChange={setIsAddToDashboardDialogOpen}
chartName={chartName}
onChartNameChange={setChartName}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={setSelectedDashboardId}
onAdd={handleAddToDashboard}
isSaving={isSaving}
/>
</DialogContent>
</Dialog>
);
return {
chartData,
chartName,
setChartName,
selectedChartType,
setSelectedChartType,
currentChartId,
setCurrentChartId,
isSaveDialogOpen,
setIsSaveDialogOpen,
isAddToDashboardDialogOpen,
setIsAddToDashboardDialogOpen,
dashboards,
selectedDashboardId,
setSelectedDashboardId,
isSaving,
isLoadingChart,
shouldShowAdvancedBuilder,
handleChartGenerated,
handleSaveChart,
handleAddToDashboard,
handleClose,
handleAdvancedBuilderSave,
handleAdvancedBuilderAddToDashboard,
};
}

View File

@@ -1,4 +1,4 @@
import { TApiChartType, TChartType } from "../types/analysis";
import { TApiChartType, TChartType } from "../../types/analysis";
/**
* Map API chart type (used in AnalyticsResponse) to database chart type (Prisma enum).

View File

@@ -0,0 +1,45 @@
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { getUser } from "@/lib/user/service";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { TChart, TChartType } from "../../types/analysis";
/**
* Fetches all charts for the given environment.
*/
export const getCharts = reactCache(async (environmentId: string): Promise<TChart[]> => {
const { project } = await getEnvironmentAuth(environmentId);
const charts = await prisma.chart.findMany({
where: { projectId: project.id },
orderBy: { createdAt: "desc" },
include: {
widgets: {
select: {
dashboardId: true,
},
},
},
});
const userIds = [...new Set(charts.map((c) => c.createdBy).filter(Boolean) as string[])];
const users = await Promise.all(userIds.map((id) => getUser(id)));
const userMap = new Map(users.filter(Boolean).map((u) => [u!.id, u!.name]));
return charts.map((chart) => {
const createdByName = chart.createdBy ? userMap.get(chart.createdBy) : undefined;
return {
id: chart.id,
name: chart.name,
type: chart.type as TChartType,
lastModified: chart.updatedAt.toISOString(),
createdAt: chart.createdAt.toISOString(),
updatedAt: chart.updatedAt.toISOString(),
createdBy: chart.createdBy || undefined,
createdByName,
dashboardIds: chart.widgets.map((widget) => widget.dashboardId),
config: (chart.config as Record<string, unknown>) || {},
};
});
});

View File

@@ -3,8 +3,8 @@
import { usePathname } from "next/navigation";
import { use } from "react";
import { AnalysisPageLayout } from "./analysis-page-layout";
import { CreateChartButton } from "./create-chart-button";
import { CreateDashboardButton } from "./create-dashboard-button";
import { CreateChartButton } from "../charts/components/create-chart-button";
import { CreateDashboardButton } from "../dashboards/components/create-dashboard-button";
interface AnalysisLayoutClientProps {
children: React.ReactNode;

View File

@@ -0,0 +1,316 @@
"use server";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/common";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import { ZDashboardStatus, ZWidgetLayout } from "../types/analysis";
const ZAddChartToDashboardAction = z.object({
environmentId: ZId,
chartId: ZId,
dashboardId: ZId,
title: z.string().optional(),
layout: ZWidgetLayout.optional().default({ x: 0, y: 0, w: 4, h: 3 }),
});
export const addChartToDashboardAction = authenticatedActionClient.schema(ZAddChartToDashboardAction).action(
withAuditLogging(
"created",
"dashboardWidget",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZAddChartToDashboardAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId,
},
],
});
const [chart, dashboard] = await Promise.all([
prisma.chart.findFirst({
where: { id: parsedInput.chartId, projectId },
}),
prisma.dashboard.findFirst({
where: { id: parsedInput.dashboardId, projectId },
}),
]);
if (!chart) {
throw new Error("Chart not found");
}
if (!dashboard) {
throw new Error("Dashboard not found");
}
const maxOrder = await prisma.dashboardWidget.aggregate({
where: { dashboardId: parsedInput.dashboardId },
_max: { order: true },
});
const widget = await prisma.dashboardWidget.create({
data: {
dashboardId: parsedInput.dashboardId,
chartId: parsedInput.chartId,
type: "chart",
title: parsedInput.title,
layout: parsedInput.layout,
order: (maxOrder._max.order ?? -1) + 1,
},
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.newObject = widget;
return widget;
}
)
);
const ZCreateDashboardAction = z.object({
environmentId: ZId,
name: z.string().min(1),
description: z.string().optional(),
});
export const createDashboardAction = authenticatedActionClient.schema(ZCreateDashboardAction).action(
withAuditLogging(
"created",
"dashboard",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZCreateDashboardAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId,
},
],
});
const dashboard = await prisma.dashboard.create({
data: {
name: parsedInput.name,
description: parsedInput.description,
projectId,
status: "draft",
createdBy: ctx.user.id,
},
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.newObject = dashboard;
return dashboard;
}
)
);
const ZUpdateDashboardAction = z.object({
environmentId: ZId,
dashboardId: ZId,
name: z.string().min(1).optional(),
description: z.string().optional().nullable(),
status: ZDashboardStatus.optional(),
});
export const updateDashboardAction = authenticatedActionClient.schema(ZUpdateDashboardAction).action(
withAuditLogging(
"updated",
"dashboard",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZUpdateDashboardAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId,
},
],
});
const dashboard = await prisma.dashboard.findFirst({
where: { id: parsedInput.dashboardId, projectId },
});
if (!dashboard) {
throw new Error("Dashboard not found");
}
const updatedDashboard = await prisma.dashboard.update({
where: { id: parsedInput.dashboardId },
data: {
...(parsedInput.name !== undefined && { name: parsedInput.name }),
...(parsedInput.description !== undefined && { description: parsedInput.description }),
...(parsedInput.status !== undefined && { status: parsedInput.status }),
},
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.oldObject = dashboard;
ctx.auditLoggingCtx.newObject = updatedDashboard;
return updatedDashboard;
}
)
);
const ZDeleteDashboardAction = z.object({
environmentId: ZId,
dashboardId: ZId,
});
export const deleteDashboardAction = authenticatedActionClient.schema(ZDeleteDashboardAction).action(
withAuditLogging(
"deleted",
"dashboard",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZDeleteDashboardAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId,
},
],
});
const dashboard = await prisma.dashboard.findFirst({
where: { id: parsedInput.dashboardId, projectId },
});
if (!dashboard) {
throw new Error("Dashboard not found");
}
await prisma.dashboardWidget.deleteMany({
where: { dashboardId: parsedInput.dashboardId },
});
await prisma.dashboard.delete({
where: { id: parsedInput.dashboardId },
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.oldObject = dashboard;
return { success: true };
}
)
);
const ZGetDashboardsAction = z.object({
environmentId: ZId,
});
export const getDashboardsAction = authenticatedActionClient
.schema(ZGetDashboardsAction)
.action(
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZGetDashboardsAction>;
}) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "read",
projectId,
},
],
});
const dashboards = await prisma.dashboard.findMany({
where: { projectId },
orderBy: { createdAt: "desc" },
select: {
id: true,
name: true,
description: true,
status: true,
createdAt: true,
updatedAt: true,
},
});
return dashboards;
}
);

View File

@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { IconBar } from "@/modules/ui/components/iconbar";
import { deleteDashboardAction } from "../actions";
import { TDashboard } from "../types/analysis";
import { TDashboard } from "../../types/analysis";
import { EditDashboardDialog } from "./edit-dashboard-dialog";
interface DashboardControlBarProps {

View File

@@ -15,7 +15,7 @@ import {
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { deleteDashboardAction } from "../actions";
import { TDashboard } from "../types/analysis";
import { TDashboard } from "../../types/analysis";
interface DashboardDropdownMenuProps {
environmentId: string;

View File

@@ -1,4 +1,4 @@
import { ChartRenderer } from "./chart-builder/chart-renderer";
import { ChartRenderer } from "@/modules/ee/analysis/charts/components/chart-builder/chart-renderer";
interface DashboardWidgetDataProps {
dataPromise: Promise<{ data: Record<string, unknown>[] } | { error: string }>;

View File

@@ -4,7 +4,7 @@ import { format, formatDistanceToNow } from "date-fns";
import { BarChart3Icon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { TDashboard } from "../types/analysis";
import { TDashboard } from "../../types/analysis";
import { DashboardDropdownMenu } from "./dashboard-dropdown-menu";
interface DashboardsListClientProps {

View File

@@ -3,7 +3,13 @@ import { prisma } from "@formbricks/database";
import { executeQuery } from "@/app/api/analytics/_lib/cube-client";
import { getUser } from "@/lib/user/service";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { TChart, TChartConfig, TChartType, TCubeQuery, TDashboard, TWidgetType } from "../types/analysis";
import {
TChartConfig,
TChartType,
TCubeQuery,
TDashboard,
TWidgetType,
} from "../../types/analysis";
/**
* Fetches all dashboards for the given environment.
@@ -57,46 +63,6 @@ export const getDashboards = reactCache(async (environmentId: string): Promise<T
});
});
/**
* Fetches all charts for the given environment.
*/
export const getCharts = reactCache(async (environmentId: string): Promise<TChart[]> => {
const { project } = await getEnvironmentAuth(environmentId);
const charts = await prisma.chart.findMany({
where: { projectId: project.id },
orderBy: { createdAt: "desc" },
include: {
widgets: {
select: {
dashboardId: true,
},
},
},
});
const userIds = [...new Set(charts.map((c) => c.createdBy).filter(Boolean) as string[])];
const users = await Promise.all(userIds.map((id) => getUser(id)));
const userMap = new Map(users.filter(Boolean).map((u) => [u!.id, u!.name]));
return charts.map((chart) => {
const createdByName = chart.createdBy ? userMap.get(chart.createdBy) : undefined;
return {
id: chart.id,
name: chart.name,
type: chart.type as TChartType,
lastModified: chart.updatedAt.toISOString(),
createdAt: chart.createdAt.toISOString(),
updatedAt: chart.updatedAt.toISOString(),
createdBy: chart.createdBy || undefined,
createdByName,
dashboardIds: chart.widgets.map((widget) => widget.dashboardId),
config: (chart.config as Record<string, unknown>) || {},
};
});
});
/**
* Executes a Cube.js query server-side and returns the result rows.
* Intended to be called from server components so data is fetched on

View File

@@ -3,13 +3,13 @@ import { notFound } from "next/navigation";
import { Suspense } from "react";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageHeader } from "@/modules/ui/components/page-header";
import { CreateChartButton } from "../components/create-chart-button";
import { CreateChartButton } from "@/modules/ee/analysis/charts/components/create-chart-button";
import { DashboardControlBar } from "../components/dashboard-control-bar";
import { DashboardWidget } from "../components/dashboard-widget";
import { DashboardWidgetData } from "../components/dashboard-widget-data";
import { DashboardWidgetSkeleton } from "../components/dashboard-widget-skeleton";
import { executeWidgetQuery, getDashboard } from "../lib/data";
import { TDashboardWidget } from "../types/analysis";
import { TDashboardWidget } from "../../types/analysis";
function getColSpan(w: number) {
if (w <= 2) return "col-span-12 md:col-span-2";