mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-27 04:18:35 +02:00
Add custom analytics script
This commit is contained in:
@@ -9,8 +9,11 @@ from danswer.auth.users import current_admin_user
|
|||||||
from danswer.db.engine import get_session
|
from danswer.db.engine import get_session
|
||||||
from danswer.db.file_store import get_default_file_store
|
from danswer.db.file_store import get_default_file_store
|
||||||
from danswer.db.models import User
|
from danswer.db.models import User
|
||||||
|
from ee.danswer.server.enterprise_settings.models import AnalyticsScriptUpload
|
||||||
from ee.danswer.server.enterprise_settings.models import EnterpriseSettings
|
from ee.danswer.server.enterprise_settings.models import EnterpriseSettings
|
||||||
|
from ee.danswer.server.enterprise_settings.store import load_analytics_script
|
||||||
from ee.danswer.server.enterprise_settings.store import load_settings
|
from ee.danswer.server.enterprise_settings.store import load_settings
|
||||||
|
from ee.danswer.server.enterprise_settings.store import store_analytics_script
|
||||||
from ee.danswer.server.enterprise_settings.store import store_settings
|
from ee.danswer.server.enterprise_settings.store import store_settings
|
||||||
|
|
||||||
|
|
||||||
@@ -65,3 +68,18 @@ def fetch_logo(db_session: Session = Depends(get_session)) -> Response:
|
|||||||
# NOTE: specifying "image/jpeg" here, but it still works for pngs
|
# NOTE: specifying "image/jpeg" here, but it still works for pngs
|
||||||
# TODO: do this properly
|
# TODO: do this properly
|
||||||
return Response(content=file_io.read(), media_type="image/jpeg")
|
return Response(content=file_io.read(), media_type="image/jpeg")
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.put("/custom-analytics-script")
|
||||||
|
def upload_custom_analytics_script(
|
||||||
|
script_upload: AnalyticsScriptUpload, _: User | None = Depends(current_admin_user)
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
store_analytics_script(script_upload)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@basic_router.get("/custom-analytics-script")
|
||||||
|
def fetch_custom_analytics_script() -> str | None:
|
||||||
|
return load_analytics_script()
|
||||||
|
@@ -16,3 +16,8 @@ class EnterpriseSettings(BaseModel):
|
|||||||
|
|
||||||
def check_validity(self) -> None:
|
def check_validity(self) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsScriptUpload(BaseModel):
|
||||||
|
script: str
|
||||||
|
secret_key: str
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
|
import os
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from danswer.dynamic_configs.factory import get_dynamic_config_store
|
from danswer.dynamic_configs.factory import get_dynamic_config_store
|
||||||
from danswer.dynamic_configs.interface import ConfigNotFoundError
|
from danswer.dynamic_configs.interface import ConfigNotFoundError
|
||||||
|
from ee.danswer.server.enterprise_settings.models import AnalyticsScriptUpload
|
||||||
from ee.danswer.server.enterprise_settings.models import EnterpriseSettings
|
from ee.danswer.server.enterprise_settings.models import EnterpriseSettings
|
||||||
|
|
||||||
|
|
||||||
@@ -23,3 +25,27 @@ def load_settings() -> EnterpriseSettings:
|
|||||||
|
|
||||||
def store_settings(settings: EnterpriseSettings) -> None:
|
def store_settings(settings: EnterpriseSettings) -> None:
|
||||||
get_dynamic_config_store().store(_ENTERPRISE_SETTINGS_KEY, settings.dict())
|
get_dynamic_config_store().store(_ENTERPRISE_SETTINGS_KEY, settings.dict())
|
||||||
|
|
||||||
|
|
||||||
|
_CUSTOM_ANALYTICS_SCRIPT_KEY = "__custom_analytics_script__"
|
||||||
|
_CUSTOM_ANALYTICS_SECRET_KEY = os.environ.get("CUSTOM_ANALYTICS_SECRET_KEY")
|
||||||
|
|
||||||
|
|
||||||
|
def load_analytics_script() -> str | None:
|
||||||
|
dynamic_config_store = get_dynamic_config_store()
|
||||||
|
try:
|
||||||
|
return cast(str, dynamic_config_store.load(_CUSTOM_ANALYTICS_SCRIPT_KEY))
|
||||||
|
except ConfigNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def store_analytics_script(analytics_script_upload: AnalyticsScriptUpload) -> None:
|
||||||
|
if (
|
||||||
|
not _CUSTOM_ANALYTICS_SECRET_KEY
|
||||||
|
or analytics_script_upload.secret_key != _CUSTOM_ANALYTICS_SECRET_KEY
|
||||||
|
):
|
||||||
|
raise ValueError("Invalid secret key")
|
||||||
|
|
||||||
|
get_dynamic_config_store().store(
|
||||||
|
_CUSTOM_ANALYTICS_SCRIPT_KEY, analytics_script_upload.script
|
||||||
|
)
|
||||||
|
@@ -27,8 +27,8 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
// analytics / audit log pages
|
// analytics / audit log pages
|
||||||
{
|
{
|
||||||
source: "/admin/performance/analytics",
|
source: "/admin/performance/usage",
|
||||||
destination: "/ee/admin/performance/analytics",
|
destination: "/ee/admin/performance/usage",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/admin/performance/query-history",
|
source: "/admin/performance/query-history",
|
||||||
@@ -43,6 +43,11 @@ const nextConfig = {
|
|||||||
source: "/admin/whitelabeling",
|
source: "/admin/whitelabeling",
|
||||||
destination: "/ee/admin/whitelabeling",
|
destination: "/ee/admin/whitelabeling",
|
||||||
},
|
},
|
||||||
|
// custom analytics/tracking
|
||||||
|
{
|
||||||
|
source: "/admin/performance/custom-analytics",
|
||||||
|
destination: "/ee/admin/performance/custom-analytics",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ import { Settings } from "./interfaces";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { DefaultDropdown, Option } from "@/components/Dropdown";
|
import { DefaultDropdown, Option } from "@/components/Dropdown";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { SettingsContext } from "@/components/settings/SettingsProviderClientSideHelper";
|
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||||
|
|
||||||
function Checkbox({
|
function Checkbox({
|
||||||
label,
|
label,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { SettingsContext } from "@/components/settings/SettingsProviderClientSideHelper";
|
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
@@ -15,11 +15,13 @@ export function ChatBanner() {
|
|||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
z-[39]
|
z-[39]
|
||||||
w-full
|
|
||||||
h-[30px]
|
h-[30px]
|
||||||
bg-background-custom-header
|
bg-background-custom-header
|
||||||
|
shadow-sm
|
||||||
|
m-2
|
||||||
|
rounded
|
||||||
border-border
|
border-border
|
||||||
border-b
|
border
|
||||||
flex`}
|
flex`}
|
||||||
>
|
>
|
||||||
<div className="mx-auto text-emphasis text-sm flex flex-col">
|
<div className="mx-auto text-emphasis text-sm flex flex-col">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Modal } from "@/components/Modal";
|
import { Modal } from "@/components/Modal";
|
||||||
import { SettingsContext } from "@/components/settings/SettingsProviderClientSideHelper";
|
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||||
import { Button } from "@tremor/react";
|
import { Button } from "@tremor/react";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
@@ -0,0 +1,111 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Label, SubLabel } from "@/components/admin/connectors/Field";
|
||||||
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
|
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||||
|
import { Button, Text } from "@tremor/react";
|
||||||
|
import { useContext, useState } from "react";
|
||||||
|
|
||||||
|
export function CustomAnalyticsUpdateForm() {
|
||||||
|
const settings = useContext(SettingsContext);
|
||||||
|
if (!settings) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const customAnalyticsScript = settings.customAnalyticsScript;
|
||||||
|
|
||||||
|
const [newCustomAnalyticsScript, setNewCustomAnalyticsScript] =
|
||||||
|
useState<string>(customAnalyticsScript || "");
|
||||||
|
const [secretKey, setSecretKey] = useState<string>("");
|
||||||
|
|
||||||
|
const { popup, setPopup } = usePopup();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{popup}
|
||||||
|
<div className="mb-4">
|
||||||
|
<Label>Script</Label>
|
||||||
|
<Text className="mb-3">
|
||||||
|
Specify the Javascript that should run on page load in order to
|
||||||
|
initialize your custom tracking/analytics.
|
||||||
|
</Text>
|
||||||
|
<Text className="mb-2">
|
||||||
|
Do not include the{" "}
|
||||||
|
<span className="font-mono"><script></script></span> tags.
|
||||||
|
If you upload a script below but you are not recieving any events in
|
||||||
|
your analytics platform, try removing all extra whitespace before each
|
||||||
|
line of JavaScript.
|
||||||
|
</Text>
|
||||||
|
<textarea
|
||||||
|
className={`
|
||||||
|
border
|
||||||
|
border-border
|
||||||
|
rounded
|
||||||
|
w-full
|
||||||
|
py-2
|
||||||
|
px-3
|
||||||
|
mt-1
|
||||||
|
h-28`}
|
||||||
|
value={newCustomAnalyticsScript}
|
||||||
|
onChange={(e) => setNewCustomAnalyticsScript(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Label>Secret Key</Label>
|
||||||
|
<SubLabel>
|
||||||
|
<>
|
||||||
|
For security reasons, you must provide a secret key to update this
|
||||||
|
script. This should be the value of the{" "}
|
||||||
|
<i>CUSTOM_ANALYTICS_SECRET_KEY</i> environment variable set when
|
||||||
|
initially setting up Danswer.
|
||||||
|
</>
|
||||||
|
</SubLabel>
|
||||||
|
<input
|
||||||
|
className={`
|
||||||
|
border
|
||||||
|
border-border
|
||||||
|
rounded
|
||||||
|
w-full
|
||||||
|
py-2
|
||||||
|
px-3
|
||||||
|
mt-1`}
|
||||||
|
type="password"
|
||||||
|
value={secretKey}
|
||||||
|
onChange={(e) => setSecretKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
onClick={async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
"/api/admin/enterprise-settings/custom-analytics-script",
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
script: newCustomAnalyticsScript,
|
||||||
|
secret_key: secretKey,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
setPopup({
|
||||||
|
type: "success",
|
||||||
|
message: "Custom analytics script updated successfully!",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const errorMsg = (await response.json()).detail;
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message: `Failed to update custom analytics script: "${errorMsg}"`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSecretKey("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
46
web/src/app/ee/admin/performance/custom-analytics/page.tsx
Normal file
46
web/src/app/ee/admin/performance/custom-analytics/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
|
import { CUSTOM_ANALYTICS_ENABLED } from "@/lib/constants";
|
||||||
|
import { Callout, Text } from "@tremor/react";
|
||||||
|
import { FiBarChart2 } from "react-icons/fi";
|
||||||
|
import { CustomAnalyticsUpdateForm } from "./CustomAnalyticsUpdateForm";
|
||||||
|
|
||||||
|
function Main() {
|
||||||
|
if (!CUSTOM_ANALYTICS_ENABLED) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Callout title="Custom Analytics is not enabled." color="red">
|
||||||
|
To set up custom analytics scripts, please work with the team who
|
||||||
|
setup Danswer in your organization to set the{" "}
|
||||||
|
<i>CUSTOM_ANALYTICS_SECRET_KEY</i> environment variable.
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text className="mb-8">
|
||||||
|
This allows you to bring your own analytics tool to Danswer! Copy the
|
||||||
|
Web snippet from your analytics provider into the box below, and we'll
|
||||||
|
start sending usage events.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<CustomAnalyticsUpdateForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<main className="pt-4 mx-auto container">
|
||||||
|
<AdminPageTitle
|
||||||
|
title="Custom Analytics"
|
||||||
|
icon={<FiBarChart2 size={32} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Main />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
@@ -4,7 +4,7 @@ import {
|
|||||||
ChatSessionSnapshot,
|
ChatSessionSnapshot,
|
||||||
QueryAnalytics,
|
QueryAnalytics,
|
||||||
UserAnalytics,
|
UserAnalytics,
|
||||||
} from "./analytics/types";
|
} from "./usage/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { buildApiPath } from "@/lib/urlBuilder";
|
import { buildApiPath } from "@/lib/urlBuilder";
|
||||||
import { Feedback } from "@/lib/types";
|
import { Feedback } from "@/lib/types";
|
||||||
|
@@ -13,7 +13,7 @@ import {
|
|||||||
import { Divider } from "@tremor/react";
|
import { Divider } from "@tremor/react";
|
||||||
import { Select, SelectItem } from "@tremor/react";
|
import { Select, SelectItem } from "@tremor/react";
|
||||||
import { ThreeDotsLoader } from "@/components/Loading";
|
import { ThreeDotsLoader } from "@/components/Loading";
|
||||||
import { ChatSessionSnapshot } from "../analytics/types";
|
import { ChatSessionSnapshot } from "../usage/types";
|
||||||
import { timestampToReadableDate } from "@/lib/dateUtils";
|
import { timestampToReadableDate } from "@/lib/dateUtils";
|
||||||
import { FiFrown, FiMinus, FiSmile } from "react-icons/fi";
|
import { FiFrown, FiMinus, FiSmile } from "react-icons/fi";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Bold, Text, Card, Title, Divider } from "@tremor/react";
|
import { Bold, Text, Card, Title, Divider } from "@tremor/react";
|
||||||
import { ChatSessionSnapshot, MessageSnapshot } from "../../analytics/types";
|
import { ChatSessionSnapshot, MessageSnapshot } from "../../usage/types";
|
||||||
import { FiBook } from "react-icons/fi";
|
import { FiBook } from "react-icons/fi";
|
||||||
import { timestampToReadableDate } from "@/lib/dateUtils";
|
import { timestampToReadableDate } from "@/lib/dateUtils";
|
||||||
import { BackButton } from "@/components/BackButton";
|
import { BackButton } from "@/components/BackButton";
|
||||||
|
@@ -6,6 +6,7 @@ import { QueryPerformanceChart } from "./QueryPerformanceChart";
|
|||||||
import { BarChartIcon } from "@/components/icons/icons";
|
import { BarChartIcon } from "@/components/icons/icons";
|
||||||
import { useTimeRange } from "../lib";
|
import { useTimeRange } from "../lib";
|
||||||
import { AdminPageTitle } from "@/components/admin/Title";
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
|
import { FiActivity } from "react-icons/fi";
|
||||||
|
|
||||||
export default function AnalyticsPage() {
|
export default function AnalyticsPage() {
|
||||||
const [timeRange, setTimeRange] = useTimeRange();
|
const [timeRange, setTimeRange] = useTimeRange();
|
||||||
@@ -13,7 +14,10 @@ export default function AnalyticsPage() {
|
|||||||
return (
|
return (
|
||||||
<main className="pt-4 mx-auto container">
|
<main className="pt-4 mx-auto container">
|
||||||
{/* TODO: remove this `dark` once we have a mode selector */}
|
{/* TODO: remove this `dark` once we have a mode selector */}
|
||||||
<AdminPageTitle title="Analytics" icon={<BarChartIcon size={32} />} />
|
<AdminPageTitle
|
||||||
|
title="Usage Statistics"
|
||||||
|
icon={<FiActivity size={32} />}
|
||||||
|
/>
|
||||||
|
|
||||||
<DateRangeSelector value={timeRange} onValueChange={setTimeRange} />
|
<DateRangeSelector value={timeRange} onValueChange={setTimeRange} />
|
||||||
|
|
@@ -3,7 +3,7 @@
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { EnterpriseSettings } from "@/app/admin/settings/interfaces";
|
import { EnterpriseSettings } from "@/app/admin/settings/interfaces";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { SettingsContext } from "@/components/settings/SettingsProviderClientSideHelper";
|
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||||
import { Form, Formik } from "formik";
|
import { Form, Formik } from "formik";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import {
|
import {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { fetchSettingsSS } from "@/components/settings/lib";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
|
import { fetchSettingsSS } from "@/components/settings/lib";
|
||||||
|
import { CUSTOM_ANALYTICS_ENABLED } from "@/lib/constants";
|
||||||
import { SettingsProvider } from "@/components/settings/SettingsProvider";
|
import { SettingsProvider } from "@/components/settings/SettingsProvider";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
@@ -25,6 +26,16 @@ export default async function RootLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
{CUSTOM_ANALYTICS_ENABLED && combinedSettings.customAnalyticsScript && (
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: combinedSettings.customAnalyticsScript,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
)}
|
||||||
<body
|
<body
|
||||||
className={`${inter.variable} font-sans text-default bg-background ${
|
className={`${inter.variable} font-sans text-default bg-background ${
|
||||||
// TODO: remove this once proper dark mode exists
|
// TODO: remove this once proper dark mode exists
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { SettingsContext } from "./settings/SettingsProviderClientSideHelper";
|
import { SettingsContext } from "./settings/SettingsProvider";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export function Logo({
|
export function Logo({
|
||||||
|
@@ -21,7 +21,16 @@ import {
|
|||||||
} from "@/lib/userSS";
|
} from "@/lib/userSS";
|
||||||
import { EE_ENABLED } from "@/lib/constants";
|
import { EE_ENABLED } from "@/lib/constants";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { FiCpu, FiImage, FiPackage, FiSettings, FiSlack, FiTool } from "react-icons/fi";
|
import {
|
||||||
|
FiActivity,
|
||||||
|
FiBarChart2,
|
||||||
|
FiCpu,
|
||||||
|
FiImage,
|
||||||
|
FiPackage,
|
||||||
|
FiSettings,
|
||||||
|
FiSlack,
|
||||||
|
FiTool,
|
||||||
|
} from "react-icons/fi";
|
||||||
|
|
||||||
export async function Layout({ children }: { children: React.ReactNode }) {
|
export async function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS()];
|
const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS()];
|
||||||
@@ -216,11 +225,11 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
name: (
|
name: (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<BarChartIcon size={18} />
|
<FiActivity size={18} />
|
||||||
<div className="ml-1">Analytics</div>
|
<div className="ml-1">Usage Statistics</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
link: "/admin/performance/analytics",
|
link: "/admin/performance/usage",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: (
|
name: (
|
||||||
@@ -231,6 +240,15 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
),
|
),
|
||||||
link: "/admin/performance/query-history",
|
link: "/admin/performance/query-history",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<div className="flex">
|
||||||
|
<FiBarChart2 size={18} />
|
||||||
|
<div className="ml-1">Custom Analytics</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
link: "/admin/performance/custom-analytics",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { CombinedSettings } from "@/app/admin/settings/interfaces";
|
|
||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
export const SettingsContext = createContext<CombinedSettings | null>(null);
|
|
||||||
|
|
||||||
export function SettingsProviderClientSideHelper({
|
|
||||||
children,
|
|
||||||
settings,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode | JSX.Element;
|
|
||||||
settings: CombinedSettings;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SettingsContext.Provider value={settings}>
|
|
||||||
{children}
|
|
||||||
</SettingsContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
Reference in New Issue
Block a user