mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-19 00:51:06 -06:00
refactor: structure
This commit is contained in:
@@ -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 />;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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[];
|
||||
@@ -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[];
|
||||
@@ -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[];
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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).
|
||||
45
apps/web/modules/ee/analysis/charts/lib/data.ts
Normal file
45
apps/web/modules/ee/analysis/charts/lib/data.ts
Normal 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>) || {},
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
316
apps/web/modules/ee/analysis/dashboards/actions.ts
Normal file
316
apps/web/modules/ee/analysis/dashboards/actions.ts
Normal 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;
|
||||
}
|
||||
);
|
||||
@@ -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 {
|
||||
@@ -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;
|
||||
@@ -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 }>;
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user