mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-12 11:28:58 -05:00
fix setup + tweak chart & dashboards creation UX
This commit is contained in:
+6
-7
@@ -287,15 +287,14 @@ REDIS_URL=redis://localhost:6379
|
||||
# API token sent with each Cube.js request; must match CUBEJS_API_SECRET when CUBEJS_DEV_MODE is off
|
||||
# CUBEJS_API_TOKEN=
|
||||
#
|
||||
# Cube connects to the Hub DB. When using docker-compose.dev.yml with the hub network,
|
||||
# use the container name and internal port. Hub credentials: formbricks/formbricks_dev, db: hub
|
||||
# CUBEJS_DB_HOST=formbricks_hub_postgres
|
||||
# Cube connects to the Hub DB. With docker-compose.dev.yml defaults, use the local postgres service.
|
||||
# CUBEJS_DB_HOST=postgres
|
||||
# CUBEJS_DB_PORT=5432
|
||||
# CUBEJS_DB_NAME=hub
|
||||
# CUBEJS_DB_USER=formbricks
|
||||
# CUBEJS_DB_PASS=formbricks_dev
|
||||
# CUBEJS_DB_NAME=postgres
|
||||
# CUBEJS_DB_USER=postgres
|
||||
# CUBEJS_DB_PASS=postgres
|
||||
#
|
||||
# Alternative (when not on same Docker network): host.docker.internal and port 5433
|
||||
# Alternative (external Hub/Postgres on the hub network): formbricks_hub_postgres, db: hub, user/pass: formbricks/formbricks_dev
|
||||
|
||||
# Lingo.dev API key for translation generation
|
||||
LINGO_API_KEY=your_api_key_here
|
||||
|
||||
@@ -167,7 +167,7 @@ export const MainNavigation = ({
|
||||
disabled: isMembershipPending || isBilling,
|
||||
},
|
||||
{
|
||||
name: t("common.analysis"),
|
||||
name: t("common.dashboards"),
|
||||
href: `/workspaces/${workspace.id}/dashboards`,
|
||||
icon: BarChart3Icon,
|
||||
isActive: pathname?.includes("/dashboards") || pathname?.includes("/charts"),
|
||||
|
||||
@@ -171,15 +171,6 @@ export const OrganizationBreadcrumb = ({
|
||||
href: `${workspaceBasePath}/settings/billing`,
|
||||
hidden: !isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
id: "feedback-record-directories",
|
||||
label: t("workspace.settings.feedback_record_directories.title"),
|
||||
href: `${workspaceBasePath}/settings/feedback-record-directories`,
|
||||
disabled: isMembershipPending || isMember,
|
||||
disabledMessage: isMembershipPending
|
||||
? t("common.loading")
|
||||
: t("common.you_are_not_authorized_to_perform_this_action"),
|
||||
},
|
||||
{
|
||||
id: "enterprise",
|
||||
label: t("common.enterprise_license"),
|
||||
|
||||
@@ -185,6 +185,24 @@ describe("transformResponseToFeedbackRecords", () => {
|
||||
expect(result[0].collected_at).toBe(NOW.toISOString());
|
||||
});
|
||||
|
||||
test("falls back to updatedAt when createdAt is missing", () => {
|
||||
const updatedAt = new Date("2026-02-25T10:00:00.000Z");
|
||||
const response = { ...mockResponse, createdAt: undefined, updatedAt } as unknown as TResponse;
|
||||
const mappings = [createMapping({ elementId: "el-text", hubFieldType: "text" })];
|
||||
const result = transformResponseToFeedbackRecords(response, mockSurvey, mappings);
|
||||
expect(result[0].collected_at).toBe(updatedAt.toISOString());
|
||||
});
|
||||
|
||||
test("parses string createdAt values for collected_at", () => {
|
||||
const response = {
|
||||
...mockResponse,
|
||||
createdAt: "2026-02-26T10:00:00.000Z",
|
||||
} as unknown as TResponse;
|
||||
const mappings = [createMapping({ elementId: "el-text", hubFieldType: "text" })];
|
||||
const result = transformResponseToFeedbackRecords(response, mockSurvey, mappings);
|
||||
expect(result[0].collected_at).toBe("2026-02-26T10:00:00.000Z");
|
||||
});
|
||||
|
||||
test("includes tenant_id when provided", () => {
|
||||
const mappings = [createMapping({ elementId: "el-text", hubFieldType: "text" })];
|
||||
const result = transformResponseToFeedbackRecords(mockResponse, mockSurvey, mappings, "tenant-abc");
|
||||
|
||||
@@ -14,6 +14,23 @@ const getHeadlineFromElement = (element?: TSurveyElement): string => {
|
||||
return getTextContent(raw) || "Untitled";
|
||||
};
|
||||
|
||||
const toIsoTimestamp = (value: unknown): string | null => {
|
||||
if (value instanceof Date) {
|
||||
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
||||
}
|
||||
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getCollectedAt = (response: TResponse): string => {
|
||||
return toIsoTimestamp(response.createdAt) ?? toIsoTimestamp(response.updatedAt) ?? new Date().toISOString();
|
||||
};
|
||||
|
||||
function extractResponseValue(responseData: TResponseData, elementId: string): TResponseDataValue {
|
||||
if (!responseData || typeof responseData !== "object") return undefined;
|
||||
return responseData[elementId];
|
||||
@@ -99,8 +116,7 @@ export function transformResponseToFeedbackRecords(
|
||||
const valueFields = convertValueToHubFields(value, mapping.hubFieldType);
|
||||
|
||||
const feedbackRecord = {
|
||||
collected_at:
|
||||
response.createdAt instanceof Date ? response.createdAt.toISOString() : String(response.createdAt),
|
||||
collected_at: getCollectedAt(response),
|
||||
source_type: "formbricks",
|
||||
submission_id: response.id,
|
||||
tenant_id: tenantId,
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
"activity": "Activity",
|
||||
"add": "Add",
|
||||
"add_action": "Add action",
|
||||
"add_chart": "Add chart",
|
||||
"add_charts": "Add charts",
|
||||
"add_existing_chart_description": "Search and select charts to add to this dashboard.",
|
||||
"add_filter": "Add filter",
|
||||
@@ -1786,6 +1787,7 @@
|
||||
"create_dashboard": "Create dashboard",
|
||||
"create_dashboard_description": "Enter a name for your new dashboard.",
|
||||
"create_failed": "Failed to create dashboard",
|
||||
"create_new_chart": "Create new chart",
|
||||
"create_success": "Dashboard created successfully!",
|
||||
"dashboard": "Dashboard",
|
||||
"dashboard_delete_confirmation": "Are you sure you want to delete this dashboard? This action cannot be undone.",
|
||||
@@ -1800,7 +1802,7 @@
|
||||
"duplicate_failed": "Failed to duplicate dashboard",
|
||||
"duplicate_success": "Dashboard duplicated successfully!",
|
||||
"failed_to_load_chart_data": "Failed to load chart data",
|
||||
"no_charts_available_description": "There are no charts that can be added to this dashboard. Either no charts exist yet, or all existing charts have already been added. Go to the Charts page to create new charts.",
|
||||
"no_charts_available_description": "No more charts available to add. Create a new one.",
|
||||
"no_charts_to_add_message": "No charts to add to this dashboard.",
|
||||
"no_dashboards_found": "No dashboards found.",
|
||||
"no_data_message": "No Data. There is currently no information to display. Add charts to build your dashboard.",
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function ChartsListPage({ workspaceId }: Readonly<ChartsListPagePro
|
||||
|
||||
return (
|
||||
<AnalysisPageLayout
|
||||
pageTitle={t("common.analysis")}
|
||||
pageTitle={t("common.dashboards")}
|
||||
workspaceId={workspaceId}
|
||||
cta={
|
||||
isReadOnly ? undefined : <CreateChartButton workspaceId={workspaceId} directories={directories} />
|
||||
|
||||
@@ -4,28 +4,40 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CreateChartDialog } from "@/modules/ee/analysis/charts/components/create-chart-dialog";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Button, type ButtonProps } from "@/modules/ui/components/button";
|
||||
|
||||
interface CreateChartButtonProps {
|
||||
workspaceId: string;
|
||||
directories: { id: string; name: string }[];
|
||||
label?: string;
|
||||
onSuccess?: () => void;
|
||||
showIcon?: boolean;
|
||||
buttonProps?: Omit<ButtonProps, "onClick" | "children">;
|
||||
}
|
||||
|
||||
export function CreateChartButton({ workspaceId, directories }: Readonly<CreateChartButtonProps>) {
|
||||
export function CreateChartButton({
|
||||
workspaceId,
|
||||
directories,
|
||||
label,
|
||||
onSuccess,
|
||||
showIcon = true,
|
||||
buttonProps,
|
||||
}: Readonly<CreateChartButtonProps>) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" onClick={() => setIsDialogOpen(true)}>
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
{t("workspace.analysis.charts.create_chart")}
|
||||
<Button size="sm" onClick={() => setIsDialogOpen(true)} {...buttonProps}>
|
||||
{showIcon && <PlusIcon className="mr-2 h-4 w-4" />}
|
||||
{label ?? t("workspace.analysis.charts.create_chart")}
|
||||
</Button>
|
||||
<CreateChartDialog
|
||||
open={isDialogOpen}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
workspaceId={workspaceId}
|
||||
directories={directories}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { getChartsAction } from "@/modules/ee/analysis/charts/actions";
|
||||
import { CreateChartButton } from "@/modules/ee/analysis/charts/components/create-chart-button";
|
||||
import { addChartToDashboardAction } from "@/modules/ee/analysis/dashboards/actions";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/modules/ui/components/dialog";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { MultiSelect } from "@/modules/ui/components/multi-select";
|
||||
|
||||
interface AddExistingChartsDialogProps {
|
||||
@@ -25,6 +27,7 @@ interface AddExistingChartsDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workspaceId: string;
|
||||
dashboardId: string;
|
||||
directories: { id: string; name: string }[];
|
||||
existingChartIds: string[];
|
||||
onSuccess: () => void;
|
||||
}
|
||||
@@ -39,6 +42,7 @@ export function AddExistingChartsDialog({
|
||||
onOpenChange,
|
||||
workspaceId,
|
||||
dashboardId,
|
||||
directories,
|
||||
existingChartIds,
|
||||
onSuccess,
|
||||
}: Readonly<AddExistingChartsDialogProps>) {
|
||||
@@ -48,30 +52,29 @@ export function AddExistingChartsDialog({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
|
||||
const loadCharts = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setSelectedChartIds([]);
|
||||
try {
|
||||
const result = await getChartsAction({ workspaceId });
|
||||
if (result?.data) {
|
||||
const availableCharts = result.data.filter((chart) => !existingChartIds.includes(chart.id));
|
||||
setChartOptions(availableCharts.map((chart) => ({ value: chart.id, label: chart.name })));
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(result);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch {
|
||||
toast.error(t("workspace.analysis.dashboards.charts_load_failed"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [workspaceId, existingChartIds, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const loadCharts = async () => {
|
||||
setIsLoading(true);
|
||||
setSelectedChartIds([]);
|
||||
try {
|
||||
const result = await getChartsAction({ workspaceId });
|
||||
if (result?.data) {
|
||||
const availableCharts = result.data.filter((chart) => !existingChartIds.includes(chart.id));
|
||||
setChartOptions(availableCharts.map((chart) => ({ value: chart.id, label: chart.name })));
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(result);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch {
|
||||
toast.error(t("workspace.analysis.dashboards.charts_load_failed"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadCharts();
|
||||
}, [open, workspaceId, existingChartIds, t]);
|
||||
}, [open, loadCharts]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (selectedChartIds.length === 0) return;
|
||||
@@ -136,25 +139,40 @@ export function AddExistingChartsDialog({
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<MultiSelect
|
||||
options={chartOptions}
|
||||
value={selectedChartIds}
|
||||
onChange={setSelectedChartIds}
|
||||
placeholder={t("common.search_charts")}
|
||||
disabled={chartOptions.length === 0}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("common.add_chart")}</Label>
|
||||
<MultiSelect
|
||||
options={chartOptions}
|
||||
value={selectedChartIds}
|
||||
onChange={setSelectedChartIds}
|
||||
placeholder={t("common.search_charts")}
|
||||
disabled={chartOptions.length === 0}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isAdding}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleAdd} loading={isAdding} disabled={selectedChartIds.length === 0 || isAdding}>
|
||||
{selectedChartIds.length > 0
|
||||
? t("workspace.analysis.dashboards.add_count_charts", { count: selectedChartIds.length })
|
||||
: t("common.add")}
|
||||
</Button>
|
||||
<DialogFooter className="sm:justify-between">
|
||||
<CreateChartButton
|
||||
workspaceId={workspaceId}
|
||||
directories={directories}
|
||||
label={t("workspace.analysis.dashboards.create_new_chart")}
|
||||
onSuccess={loadCharts}
|
||||
buttonProps={{ variant: "outline", size: "default", disabled: isAdding }}
|
||||
/>
|
||||
<div className="flex flex-col-reverse gap-2 sm:flex-row">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isAdding}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
loading={isAdding}
|
||||
disabled={selectedChartIds.length === 0 || isAdding}>
|
||||
{selectedChartIds.length > 0
|
||||
? t("workspace.analysis.dashboards.add_count_charts", { count: selectedChartIds.length })
|
||||
: t("common.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CheckIcon, PencilIcon, PlusIcon, RefreshCwIcon, TrashIcon, XIcon } from "lucide-react";
|
||||
import { CheckIcon, PencilIcon, RefreshCwIcon, TrashIcon, XIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
@@ -8,12 +8,14 @@ import { useTranslation } from "react-i18next";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { deleteDashboardAction } from "@/modules/ee/analysis/dashboards/actions";
|
||||
import { AddExistingChartsDialog } from "@/modules/ee/analysis/dashboards/components/add-existing-charts-dialog";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { IconBar } from "@/modules/ui/components/iconbar";
|
||||
|
||||
interface DashboardControlBarProps {
|
||||
workspaceId: string;
|
||||
dashboardId: string;
|
||||
directories: { id: string; name: string }[];
|
||||
existingChartIds: string[];
|
||||
isEditing: boolean;
|
||||
isSaving: boolean;
|
||||
@@ -28,6 +30,7 @@ interface DashboardControlBarProps {
|
||||
export const DashboardControlBar = ({
|
||||
workspaceId,
|
||||
dashboardId,
|
||||
directories,
|
||||
existingChartIds,
|
||||
isEditing,
|
||||
isSaving,
|
||||
@@ -82,12 +85,6 @@ export const DashboardControlBar = ({
|
||||
];
|
||||
|
||||
const viewModeActions = [
|
||||
{
|
||||
icon: PlusIcon,
|
||||
tooltip: t("common.add_charts"),
|
||||
onClick: () => setIsAddExistingDialogOpen(true),
|
||||
isVisible: !isReadOnly,
|
||||
},
|
||||
{
|
||||
icon: RefreshCwIcon,
|
||||
tooltip: t("common.refresh"),
|
||||
@@ -110,7 +107,12 @@ export const DashboardControlBar = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconBar actions={isEditing ? editModeActions : viewModeActions} />
|
||||
<div className="flex items-center gap-2">
|
||||
<IconBar actions={isEditing ? editModeActions : viewModeActions} />
|
||||
{!isEditing && !isReadOnly && (
|
||||
<Button onClick={() => setIsAddExistingDialogOpen(true)}>{t("common.add_chart")}</Button>
|
||||
)}
|
||||
</div>
|
||||
<DeleteDialog
|
||||
deleteWhat={t("workspace.analysis.dashboards.dashboard")}
|
||||
open={isDeleteDialogOpen}
|
||||
@@ -124,6 +126,7 @@ export const DashboardControlBar = ({
|
||||
onOpenChange={setIsAddExistingDialogOpen}
|
||||
workspaceId={workspaceId}
|
||||
dashboardId={dashboardId}
|
||||
directories={directories}
|
||||
existingChartIds={existingChartIds}
|
||||
onSuccess={() => {
|
||||
setIsAddExistingDialogOpen(false);
|
||||
|
||||
@@ -27,6 +27,7 @@ interface DashboardDetailClientProps {
|
||||
workspaceId: string;
|
||||
dashboard: TDashboardDetail;
|
||||
widgetDataPromises: Map<string, Promise<{ data: TChartDataRow[]; query: TChartQuery } | { error: string }>>;
|
||||
directories: { id: string; name: string }[];
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
@@ -134,6 +135,7 @@ export function DashboardDetailClient({
|
||||
workspaceId,
|
||||
dashboard,
|
||||
widgetDataPromises,
|
||||
directories,
|
||||
isReadOnly,
|
||||
}: Readonly<DashboardDetailClientProps>) {
|
||||
const router = useRouter();
|
||||
@@ -248,6 +250,7 @@ export function DashboardDetailClient({
|
||||
<DashboardControlBar
|
||||
workspaceId={workspaceId}
|
||||
dashboardId={dashboard.id}
|
||||
directories={directories}
|
||||
existingChartIds={widgets.map((w) => w.chartId)}
|
||||
isEditing={isEditing}
|
||||
isSaving={isSaving}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function DashboardPageHeader({
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
className="w-full rounded-md border border-dashed border-slate-300 bg-transparent px-2 py-1 text-3xl font-bold text-slate-800 focus:border-brand-dark focus:outline-none focus:ring-0"
|
||||
className="w-full rounded-md border border-dashed border-slate-300 bg-white px-2 py-1 text-3xl font-bold text-slate-800 focus:border-brand-dark focus:outline-none focus:ring-0"
|
||||
aria-label={t("workspace.analysis.dashboards.dashboard_name_placeholder")}
|
||||
placeholder={t("workspace.analysis.dashboards.dashboard_name_placeholder")}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { executeQuery } from "@/modules/ee/analysis/api/lib/cube-client";
|
||||
import { injectTenantFilter } from "@/modules/ee/analysis/charts/lib/chart-utils";
|
||||
import type { TChartDataRow } from "@/modules/ee/analysis/types/analysis";
|
||||
import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
|
||||
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
|
||||
import { DashboardDetailClient } from "../components/dashboard-detail-client";
|
||||
import { getDashboard } from "../lib/dashboards";
|
||||
@@ -59,12 +60,14 @@ export async function DashboardDetailPage({
|
||||
widgetDataPromises.set(widgetId, Promise.resolve(result));
|
||||
}
|
||||
});
|
||||
const directories = await getFeedbackRecordDirectoriesByWorkspaceId(workspaceId);
|
||||
|
||||
return (
|
||||
<DashboardDetailClient
|
||||
workspaceId={workspaceId}
|
||||
dashboard={dashboard}
|
||||
widgetDataPromises={widgetDataPromises}
|
||||
directories={directories}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ export const DashboardsListPage = async ({ workspaceId }: Readonly<DashboardsLis
|
||||
|
||||
return (
|
||||
<AnalysisPageLayout
|
||||
pageTitle={t("common.analysis")}
|
||||
pageTitle={t("common.dashboards")}
|
||||
workspaceId={workspaceId}
|
||||
cta={isReadOnly ? undefined : <CreateDashboardButton workspaceId={workspaceId} />}>
|
||||
<DashboardsListContent
|
||||
|
||||
@@ -14,7 +14,7 @@ export const AnalysisListLoading = () => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.analysis")}>
|
||||
<PageHeader pageTitle={t("common.dashboards")}>
|
||||
{workspaceId ? <AnalysisSecondaryNavigation workspaceId={workspaceId} /> : null}
|
||||
</PageHeader>
|
||||
<DashboardsListSkeleton
|
||||
|
||||
@@ -89,10 +89,12 @@ services:
|
||||
- 4001:4001 # Cube Playground UI (dev only)
|
||||
environment:
|
||||
CUBEJS_DB_TYPE: postgres
|
||||
CUBEJS_DB_HOST: ${CUBEJS_DB_HOST:-formbricks_hub_postgres}
|
||||
CUBEJS_DB_NAME: ${CUBEJS_DB_NAME:-hub}
|
||||
CUBEJS_DB_USER: ${CUBEJS_DB_USER:-formbricks}
|
||||
CUBEJS_DB_PASS: ${CUBEJS_DB_PASS:-formbricks_dev}
|
||||
# Default to the local postgres service in this compose stack.
|
||||
# Override CUBEJS_DB_* when using an external Hub/Postgres network.
|
||||
CUBEJS_DB_HOST: ${CUBEJS_DB_HOST:-postgres}
|
||||
CUBEJS_DB_NAME: ${CUBEJS_DB_NAME:-postgres}
|
||||
CUBEJS_DB_USER: ${CUBEJS_DB_USER:-postgres}
|
||||
CUBEJS_DB_PASS: ${CUBEJS_DB_PASS:-postgres}
|
||||
CUBEJS_DB_PORT: ${CUBEJS_DB_PORT:-5432}
|
||||
CUBEJS_DEV_MODE: "true"
|
||||
CUBEJS_API_SECRET: ${CUBEJS_API_SECRET}
|
||||
|
||||
Reference in New Issue
Block a user