diff --git a/web/components/templates/dashboard/dashboardPage.tsx b/web/components/templates/dashboard/dashboardPage.tsx index 28ccf90df0..568febf590 100644 --- a/web/components/templates/dashboard/dashboardPage.tsx +++ b/web/components/templates/dashboard/dashboardPage.tsx @@ -19,10 +19,7 @@ import { getTimeIntervalAgo, TimeInterval, } from "../../../lib/timeCalculations/time"; -import { - useGetReport, - useGetUnauthorized, -} from "../../../services/hooks/dashboard"; +import { useGetUnauthorized } from "../../../services/hooks/dashboard"; import { useDebounce } from "../../../services/hooks/debounce"; import { useOrganizationLayout } from "../../../services/hooks/organization_layout"; import { @@ -59,7 +56,6 @@ import { useDashboardPage } from "./useDashboardPage"; import { formatLargeNumber } from "../../shared/utils/numberFormat"; import { ThemedSwitch } from "../../shared/themed/themedSwitch"; import { useLocalStorage } from "../../../services/hooks/localStorage"; -import { Button } from "@/components/ui/button"; const ResponsiveGridLayout = WidthProvider(Responsive); @@ -168,13 +164,6 @@ const DashboardPage = (props: DashboardPageProps) => { const [timeFilter, setTimeFilter] = useState(getTimeFilter()); const [open, setOpen] = useState(false); - const [openReports, setOpenReports] = useState(false); - - const { - data: report, - isLoading: isLoadingReport, - refetch: refetchReport, - } = useGetReport(); const [advancedFilters, setAdvancedFilters] = useState( getRootFilterNode() @@ -185,7 +174,6 @@ const DashboardPage = (props: DashboardPageProps) => { const timeIncrement = getTimeInterval(timeFilter); const { unauthorized, currentTier } = useGetUnauthorized(user.id); - const { setNotification } = useNotification(); // eslint-disable-next-line react-hooks/exhaustive-deps const encodeFilters = (filters: UIFilterRowTree): string => { @@ -552,21 +540,8 @@ const DashboardPage = (props: DashboardPageProps) => { } actions={ -
- -
- -
+
+
} /> @@ -1025,14 +1000,6 @@ const DashboardPage = (props: DashboardPageProps) => { - {!isLoadingReport && ( - - )} ); }; diff --git a/web/components/templates/settings/reportsPage.tsx b/web/components/templates/settings/reportsPage.tsx new file mode 100644 index 0000000000..731813ad13 --- /dev/null +++ b/web/components/templates/settings/reportsPage.tsx @@ -0,0 +1,346 @@ +import { FormEvent, useEffect, useMemo, useState } from "react"; +import { useOrgPlanPage } from "../organization/plan/useOrgPlanPage"; +import { + addMonths, + endOfMonth, + formatISO, + isAfter, + startOfMonth, + subMonths, +} from "date-fns"; +import { BarChart, MultiSelect, MultiSelectItem } from "@tremor/react"; +import { getTimeMap } from "../../../lib/timeCalculations/constants"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import useNotification from "../../shared/notification/useNotification"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useGetReport } from "@/services/hooks/dashboard"; +import { useJawnClient } from "@/lib/clients/jawnHook"; +import { useOrg } from "@/components/layout/organizationContext"; +import { getHeliconeCookie } from "@/lib/cookies"; +import { + useGetOrgMembers, + useGetOrgSlackChannels, + useGetOrgSlackIntegration, +} from "@/services/hooks/organizations"; +import { Switch } from "@/components/ui/switch"; + +const ReportsPage = () => { + const { + data: report, + isLoading: isLoadingReport, + refetch: refetchReport, + isRefetching: isRefetchingReport, + } = useGetReport(); + + const slackRedirectUrl = useMemo(() => { + if (typeof window !== "undefined" && window) { + return `${ + window.location.protocol === "http:" ? "https://redirectmeto.com/" : "" + }${window.location.origin}/slack/redirect`; + } + return null; + }, []); + + const jawn = useJawnClient(); + + const orgContext = useOrg(); + const { setNotification } = useNotification(); + + const { data, isLoading, refetch } = useGetOrgMembers( + orgContext?.currentOrg?.id || "" + ); + + const members: { + email: string; + member: string; + org_role: string; + }[] = [...(data || [])]; + + const { data: slackIntegration, isLoading: isLoadingSlackIntegration } = + useGetOrgSlackIntegration(orgContext?.currentOrg?.id || ""); + + const { data: slackChannelsData, isLoading: isLoadingSlackChannels } = + useGetOrgSlackChannels(orgContext?.currentOrg?.id || ""); + + const slackChannels: { + id: string; + name: string; + }[] = [...(slackChannelsData || [])]; + + const [reportEnabled, setReportEnabled] = useState( + report?.active ? report?.active : false + ); + const [selectedEmails, setSelectedEmails] = useState( + (report?.settings?.emails as string[]) || [] + ); + const [selectedSlackChannels, setSelectedSlackChannels] = useState( + (report?.settings?.slack_channels as string[]) || [] + ); + + const [showEmails, setShowEmails] = useState( + report?.active ? (report?.settings?.emails as string[]).length > 0 : true + ); + + const [showSlackChannels, setShowSlackChannels] = useState( + report?.active + ? (report?.settings?.slack_channels as string[]).length > 0 + : false + ); + + useEffect(() => { + if (!isLoadingReport && !isRefetchingReport) { + setShowEmails( + report?.active + ? (report?.settings?.emails as string[]).length > 0 + : true + ); + setShowSlackChannels( + report?.active + ? (report?.settings?.slack_channels as string[]).length > 0 + : false + ); + setSelectedEmails( + report?.active ? (report?.settings?.emails as string[]) : [] + ); + setSelectedSlackChannels( + report?.active ? (report?.settings?.slack_channels as string[]) : [] + ); + setReportEnabled(report?.active ?? false); + } + }, [isLoadingReport, isRefetchingReport]); + + // useEffect(() => { + // setShowEmails( + // report?.active ? (report?.settings?.emails as string[]).length > 0 : true + // ); + // setShowSlackChannels( + // report?.active + // ? (report?.settings?.slack_channels as string[]).length > 0 + // : false + // ); + // setSelectedEmails( + // report?.active ? (report?.settings?.emails as string[]) : [] + // ); + // setSelectedSlackChannels( + // report?.active ? (report?.settings?.slack_channels as string[]) : [] + // ); + // }, []); + + const handleCustomizeReports = async (event: FormEvent) => { + event.preventDefault(); + + if (orgContext?.currentOrg?.id === undefined) { + return; + } + + if ( + reportEnabled && + ((!showEmails && !showSlackChannels) || + (selectedEmails.length < 1 && selectedSlackChannels.length < 1)) + ) { + setNotification( + "Please select at least one email or slack channel", + "error" + ); + return; + } + + const authFromCookie = getHeliconeCookie(); + if (authFromCookie.error || !authFromCookie.data) { + setNotification("Please login to create an alert", "error"); + return; + } + + const req_body = { + integration_name: "report", + settings: reportEnabled + ? { + emails: showEmails ? selectedEmails : [], + slack_channels: showSlackChannels ? selectedSlackChannels : [], + } + : report?.settings + ? report?.settings + : {}, + active: reportEnabled, + }; + + if (report?.id) { + const { error } = await jawn.POST(`/v1/integration/{integrationId}`, { + params: { + path: { + integrationId: report.id, + }, + }, + body: req_body, + }); + + if (error) { + setNotification(`Failed to update report ${error}`, "error"); + return; + } + + setNotification("Successfully configured report", "success"); + refetchReport(); + return; + } + + const { error } = await jawn.POST("/v1/integration", { + body: req_body, + }); + + if (error) { + setNotification(`Failed to create report ${error}`, "error"); + return; + } + + setNotification("Successfully enabled report", "success"); + refetchReport(); + }; + + return ( +
+
+
+

+ Reports +

+ + +
+ + Receive a weekly summary report every Monday at{" "} + 10am UTC. + + {reportEnabled && ( +
+

Notify By

+
+
+ + +
+ {showEmails && ( + { + setSelectedEmails(values); + }} + className="!mb-8" + > + {members.map((member, idx) => { + return ( + + {member.email} + + ); + })} + + )} +
+
+
+ + +
+ {showSlackChannels && + (slackIntegration?.data ? ( + <> + { + setSelectedSlackChannels(values); + }} + > + {slackChannels.map((channel, idx) => { + return ( + + {channel.name} + + ); + })} + + + If the channel is private, you will need to add the bot to + the channel by mentioning @Helicone in + the channel. + + + ) : ( + + ))} +
+
+ )} +
+ + +
+
+
+ ); +}; + +export default ReportsPage; diff --git a/web/components/templates/settings/settingsLayout.tsx b/web/components/templates/settings/settingsLayout.tsx index 6e479c7550..63363e9594 100644 --- a/web/components/templates/settings/settingsLayout.tsx +++ b/web/components/templates/settings/settingsLayout.tsx @@ -2,6 +2,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { BuildingOfficeIcon, CreditCardIcon, + DocumentTextIcon, NoSymbolIcon, UserGroupIcon, } from "@heroicons/react/24/outline"; @@ -18,6 +19,12 @@ const tabs = [ icon: BuildingOfficeIcon, href: "/settings", }, + { + id: "reports", + title: "Reports", + icon: DocumentTextIcon, + href: "/settings/reports", + }, { id: "api-keys", title: "API Keys", diff --git a/web/pages/settings/reports.tsx b/web/pages/settings/reports.tsx new file mode 100644 index 0000000000..fda73fb41f --- /dev/null +++ b/web/pages/settings/reports.tsx @@ -0,0 +1,19 @@ +import { NextPageWithLayout } from "../_app"; +import AuthLayout from "../../components/layout/auth/authLayout"; +import { ReactElement } from "react"; +import ReportsPage from "@/components/templates/settings/reportsPage"; +import SettingsLayout from "@/components/templates/settings/settingsLayout"; + +const ReportsSettings: NextPageWithLayout = () => { + return ; +}; + +ReportsSettings.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReportsSettings;