diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index cf3595faa..4f9a11dbb 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -92,7 +92,8 @@ export interface SignalSourceConfig { | "linear" | "zendesk" | "conversations" - | "error_tracking"; + | "error_tracking" + | "pganalyze"; source_type: | "session_analysis_cluster" | "evaluation" diff --git a/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx b/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx index ae0d944cb..cb540f66a 100644 --- a/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx +++ b/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx @@ -10,12 +10,13 @@ import { trpcClient } from "@renderer/trpc"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -type DataSourceType = "github" | "linear" | "zendesk"; +type DataSourceType = "github" | "linear" | "zendesk" | "pganalyze"; const REQUIRED_SCHEMAS: Record = { github: ["issues"], linear: ["issues"], zendesk: ["tickets"], + pganalyze: ["issues", "servers"], }; /** PostHog DWH: full table replication (non-incremental); API enum value `full_refresh`. */ @@ -47,6 +48,8 @@ export function DataSourceSetup({ return ; case "zendesk": return ; + case "pganalyze": + return ; } } @@ -489,6 +492,75 @@ function ZendeskSetup({ onComplete, onCancel }: SetupFormProps) { ); } +function PgAnalyzeSetup({ onComplete, onCancel }: SetupFormProps) { + const projectId = useAuthStateValue((state) => state.projectId); + const client = useAuthenticatedClient(); + const [apiKey, setApiKey] = useState(""); + const [organizationSlug, setOrganizationSlug] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = useCallback(async () => { + if (!projectId || !client) return; + if (!apiKey.trim() || !organizationSlug.trim()) { + toast.error("Please fill in all fields"); + return; + } + + setLoading(true); + try { + await client.createExternalDataSource(projectId, { + source_type: "PgAnalyze", + payload: { + api_key: apiKey.trim(), + organization_slug: organizationSlug.trim(), + schemas: schemasPayload("pganalyze"), + }, + }); + toast.success("pganalyze data source created"); + onComplete(); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to create data source", + ); + } finally { + setLoading(false); + } + }, [projectId, client, apiKey, organizationSlug, onComplete]); + + const canSubmit = apiKey.trim() && organizationSlug.trim(); + + return ( + + + setOrganizationSlug(e.target.value)} + /> + setApiKey(e.target.value)} + /> + + + + + + + + ); +} + function SetupFormContainer({ title, children, diff --git a/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx b/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx index 921a7eccf..f727f2308 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx @@ -1,4 +1,5 @@ import { Badge } from "@components/ui/Badge"; +import { PgAnalyzeIcon } from "@features/inbox/components/utils/PgAnalyzeIcon"; import { ArrowSquareOutIcon, BrainIcon, @@ -29,6 +30,7 @@ export interface SignalSourceValues { linear: boolean; zendesk: boolean; conversations: boolean; + pganalyze: boolean; } interface SignalSourceToggleCardProps { @@ -295,9 +297,14 @@ export function SignalSourceToggles({ (checked: boolean) => onToggle("conversations", checked), [onToggle], ); + const togglePgAnalyze = useCallback( + (checked: boolean) => onToggle("pganalyze", checked), + [onToggle], + ); const setupGithub = useCallback(() => onSetup?.("github"), [onSetup]); const setupLinear = useCallback(() => onSetup?.("linear"), [onSetup]); const setupZendesk = useCallback(() => onSetup?.("zendesk"), [onSetup]); + const setupPgAnalyze = useCallback(() => onSetup?.("pganalyze"), [onSetup]); return ( @@ -395,6 +402,18 @@ export function SignalSourceToggles({ loading={sourceStates?.zendesk?.loading} syncStatus={sourceStates?.zendesk?.syncStatus} /> + } + label="pganalyze" + description="Postgres performance findings, slow queries, and index recommendations" + checked={value.pganalyze} + onCheckedChange={togglePgAnalyze} + disabled={disabled} + requiresSetup={sourceStates?.pganalyze?.requiresSetup} + onSetup={setupPgAnalyze} + loading={sourceStates?.pganalyze?.loading} + syncStatus={sourceStates?.pganalyze?.syncStatus} + /> diff --git a/apps/code/src/renderer/features/inbox/components/detail/SignalCard.tsx b/apps/code/src/renderer/features/inbox/components/detail/SignalCard.tsx index ea254b4fa..18c34c632 100644 --- a/apps/code/src/renderer/features/inbox/components/detail/SignalCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/detail/SignalCard.tsx @@ -65,6 +65,9 @@ function signalCardSourceLine(signal: { if (source_product === "linear" && source_type === "issue") { return "Linear · Issue"; } + if (source_product === "pganalyze" && source_type === "issue") { + return "pganalyze · Issue"; + } const productLabel = source_product.replace(/_/g, " "); const typeLabel = source_type.replace(/_/g, " "); diff --git a/apps/code/src/renderer/features/inbox/components/list/FilterSortMenu.tsx b/apps/code/src/renderer/features/inbox/components/list/FilterSortMenu.tsx index e383772de..ccc8800e5 100644 --- a/apps/code/src/renderer/features/inbox/components/list/FilterSortMenu.tsx +++ b/apps/code/src/renderer/features/inbox/components/list/FilterSortMenu.tsx @@ -1,3 +1,4 @@ +import { PgAnalyzeIcon } from "@features/inbox/components/utils/PgAnalyzeIcon"; import { type SourceProduct, useInboxSignalsFilterStore, @@ -103,6 +104,7 @@ const SOURCE_PRODUCT_OPTIONS: { label: "Conversations", icon: , }, + { value: "pganalyze", label: "pganalyze", icon: }, ]; const ITEM_CLASS_NAME = diff --git a/apps/code/src/renderer/features/inbox/components/utils/PgAnalyzeIcon.tsx b/apps/code/src/renderer/features/inbox/components/utils/PgAnalyzeIcon.tsx new file mode 100644 index 000000000..bd19b4958 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/components/utils/PgAnalyzeIcon.tsx @@ -0,0 +1,36 @@ +import type { IconProps } from "@phosphor-icons/react"; + +// Inlined so the SVG inherits `currentColor` from the parent text color and +// adapts to light/dark mode the same way as the Phosphor icons used elsewhere. +// Source asset: posthog/frontend/public/services/pganalyze.svg. +// +// Accepts the Phosphor `IconProps` shape so it can be substituted for one in +// the SOURCE_PRODUCT_META table without a type cast. Only `size` and +// `className` are honored — `weight`, `mirrored`, etc. are ignored. +export function PgAnalyzeIcon({ size = 20, className }: IconProps) { + return ( + + ); +} diff --git a/apps/code/src/renderer/features/inbox/components/utils/source-product-icons.tsx b/apps/code/src/renderer/features/inbox/components/utils/source-product-icons.tsx index f7fdbc7c5..fa67fa90b 100644 --- a/apps/code/src/renderer/features/inbox/components/utils/source-product-icons.tsx +++ b/apps/code/src/renderer/features/inbox/components/utils/source-product-icons.tsx @@ -9,6 +9,7 @@ import { VideoIcon, } from "@phosphor-icons/react"; import type { ComponentType } from "react"; +import { PgAnalyzeIcon } from "./PgAnalyzeIcon"; /** * Shared source product metadata used across inbox components. @@ -53,4 +54,9 @@ export const SOURCE_PRODUCT_META: Record< color: "var(--cyan-9)", label: "Conversations", }, + pganalyze: { + Icon: PgAnalyzeIcon, + color: "var(--gray-12)", + label: "pganalyze", + }, }; diff --git a/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts b/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts index 70d1c86dc..7d393be88 100644 --- a/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts +++ b/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts @@ -27,6 +27,7 @@ const SOURCE_TYPE_MAP: Record< linear: "issue", zendesk: "ticket", conversations: "ticket", + pganalyze: "issue", }; const ERROR_TRACKING_SOURCE_TYPES: SourceType[] = [ @@ -42,6 +43,7 @@ const SOURCE_LABELS: Record = { linear: "Linear Issues", zendesk: "Zendesk Tickets", conversations: "PostHog Support", + pganalyze: "pganalyze", }; const DATA_WAREHOUSE_SOURCES: Record< @@ -51,6 +53,7 @@ const DATA_WAREHOUSE_SOURCES: Record< github: { dwSourceType: "Github", requiredTable: "issues" }, linear: { dwSourceType: "Linear", requiredTable: "issues" }, zendesk: { dwSourceType: "Zendesk", requiredTable: "tickets" }, + pganalyze: { dwSourceType: "PgAnalyze", requiredTable: "issues" }, }; const ALL_SOURCE_PRODUCTS: (keyof SignalSourceValues)[] = [ @@ -60,6 +63,7 @@ const ALL_SOURCE_PRODUCTS: (keyof SignalSourceValues)[] = [ "linear", "zendesk", "conversations", + "pganalyze", ]; function computeValues( @@ -72,6 +76,7 @@ function computeValues( linear: false, zendesk: false, conversations: false, + pganalyze: false, }; if (!configs?.length) return result; for (const product of ALL_SOURCE_PRODUCTS) { @@ -113,7 +118,7 @@ export function useSignalSourceManager() { const pendingRef = useRef(new Set()); const [setupSource, setSetupSource] = useState< - "github" | "linear" | "zendesk" | null + "github" | "linear" | "zendesk" | "pganalyze" | null >(null); const [loadingSources, setLoadingSources] = useState< Partial> @@ -159,7 +164,8 @@ export function useSignalSourceManager() { if ( product === "github" || product === "linear" || - product === "zendesk" + product === "zendesk" || + product === "pganalyze" ) { const hasExternalSource = !!findExternalSource(product); const isEnabled = serverValues[product]; @@ -267,7 +273,12 @@ export function useSignalSourceManager() { ); const handleSetup = useCallback((source: keyof SignalSourceValues) => { - if (source === "github" || source === "linear" || source === "zendesk") { + if ( + source === "github" || + source === "linear" || + source === "zendesk" || + source === "pganalyze" + ) { setSetupSource(source); } }, []); @@ -295,7 +306,9 @@ export function useSignalSourceManager() { if (enabled && product in DATA_WAREHOUSE_SOURCES) { const hasExternalSource = !!findExternalSource(product); if (!hasExternalSource) { - setSetupSource(product as "github" | "linear" | "zendesk"); + setSetupSource( + product as "github" | "linear" | "zendesk" | "pganalyze", + ); return; } diff --git a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts index c53c4c9e5..36ca2b969 100644 --- a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts +++ b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts @@ -19,7 +19,8 @@ export type SourceProduct = | "github" | "linear" | "zendesk" - | "conversations"; + | "conversations" + | "pganalyze"; const DEFAULT_STATUS_FILTER: SignalReportStatus[] = [ "ready",