Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export interface SignalSourceConfig {
| "linear"
| "zendesk"
| "conversations"
| "error_tracking";
| "error_tracking"
| "pganalyze";
source_type:
| "session_analysis_cluster"
| "evaluation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataSourceType, string[]> = {
github: ["issues"],
linear: ["issues"],
zendesk: ["tickets"],
pganalyze: ["issues", "servers"],
};

/** PostHog DWH: full table replication (non-incremental); API enum value `full_refresh`. */
Expand Down Expand Up @@ -47,6 +48,8 @@ export function DataSourceSetup({
return <LinearSetup onComplete={onComplete} onCancel={onCancel} />;
case "zendesk":
return <ZendeskSetup onComplete={onComplete} onCancel={onCancel} />;
case "pganalyze":
return <PgAnalyzeSetup onComplete={onComplete} onCancel={onCancel} />;
}
}

Expand Down Expand Up @@ -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 (
<SetupFormContainer title="Connect pganalyze">
<Flex direction="column" gap="3">
<TextField.Root
placeholder="Organization slug (e.g. my-company)"
value={organizationSlug}
onChange={(e) => setOrganizationSlug(e.target.value)}
/>
<TextField.Root
placeholder="API key"
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>

<Flex gap="2" justify="end">
<Button size="2" variant="soft" onClick={onCancel} disabled={loading}>
Cancel
</Button>
<Button
size="2"
onClick={handleSubmit}
disabled={!canSubmit || loading}
>
{loading ? "Creating..." : "Create source"}
</Button>
</Flex>
</Flex>
</SetupFormContainer>
);
}

function SetupFormContainer({
title,
children,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Badge } from "@components/ui/Badge";
import { PgAnalyzeIcon } from "@features/inbox/components/utils/PgAnalyzeIcon";
import {
ArrowSquareOutIcon,
BrainIcon,
Expand Down Expand Up @@ -29,6 +30,7 @@ export interface SignalSourceValues {
linear: boolean;
zendesk: boolean;
conversations: boolean;
pganalyze: boolean;
}

interface SignalSourceToggleCardProps {
Expand Down Expand Up @@ -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 (
<Flex gap="4">
Expand Down Expand Up @@ -395,6 +402,18 @@ export function SignalSourceToggles({
loading={sourceStates?.zendesk?.loading}
syncStatus={sourceStates?.zendesk?.syncStatus}
/>
<SignalSourceToggleCard
icon={<PgAnalyzeIcon size={20} />}
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}
/>
</Flex>
</Flex>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, " ");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PgAnalyzeIcon } from "@features/inbox/components/utils/PgAnalyzeIcon";
import {
type SourceProduct,
useInboxSignalsFilterStore,
Expand Down Expand Up @@ -103,6 +104,7 @@ const SOURCE_PRODUCT_OPTIONS: {
label: "Conversations",
icon: <LifebuoyIcon size={14} />,
},
{ value: "pganalyze", label: "pganalyze", icon: <PgAnalyzeIcon size={14} /> },
];

const ITEM_CLASS_NAME =
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<svg
width={size}
height={size}
className={className}
viewBox="0 0 58.54 58.54"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M46.65,52.18a16.12,16.12,0,0,1-1.19-3.88.39.39,0,0,0-.53-.25,23.21,23.21,0,0,1-6.43,1.67c-.84.09-1.69.19-2.54.27s-1.67.12-2.51.13a8.16,8.16,0,0,1-1.1-.09c-.21,0-.3-.37-.35-.55l0-.15c2.51-.3,5-.62,7.49-1.17,1.1-.25,2.21-.52,3.29-.87.54-.17,1.08-.36,1.6-.57l.79-.34.31-.15a.39.39,0,0,0,.22-.36c0-.47,0-1.45,0-1.73a.2.2,0,0,0-.25-.22c-12.3,3.54-24.06,3.21-29,2.11-1.74-.39-3.14-1.05-3.54-1.78a19.61,19.61,0,0,1-1.27-3.73c-.08-.27-.75-.79-1.06-1a.1.1,0,0,0-.18.06,12.73,12.73,0,0,0,.16,5c.13.55.29,1.1.45,1.59a.19.19,0,0,1-.17.24,24.31,24.31,0,0,0-4.1.7c.4.51.82,1,1.26,1.5a18.11,18.11,0,0,1,7.67.09C21,50.15,24.55,55,26.27,57.87a29.83,29.83,0,0,0,3,.15,28.68,28.68,0,0,0,17.38-5.84" />
<path d="M7.57,29.14c-.24-2.45.51-4.36,1.93-4.49a1.47,1.47,0,0,1,.47,0A4.42,4.42,0,0,0,9.59,27c.17,1.69,1.09,3,2.06,2.89a1.32,1.32,0,0,0,.87-.52c.05,2.2-.86,4-2.17,4.11S7.8,31.59,7.57,29.14M13.29,39c.65.42,1.2,2.47,1.44,3.16a3.44,3.44,0,0,0,2.53,1.74,45.38,45.38,0,0,0,6.58.74c11,.21,17.59-1.59,21.23-2.82.33-.11.88-.3.89-.83l-.1-1.51c0-.47-.14-.9-.7-.66-.25.11-.38.43-.59.6a2.82,2.82,0,0,1-.94.45c-4.21,1.36-13.21,1.8-17.6,1.88a3.71,3.71,0,0,1-3.14-1.33c-1.25-1.33-2.5-3.56-3.42-3.69-5.47-.78-5.55-3.5-5.55-3.5s.21-4.3,0-6.12c-.37-3.87-2.21-5.75-4.12-5.57-1.6.16-3.1,2.87-3.25,5.91,0,0-1,6.59,6.71,11.55" />
<path d="M22.76,26.3a.43.43,0,0,0,.07.62,8.16,8.16,0,0,0,1.26.78,5.7,5.7,0,0,1,1.73,2,9.38,9.38,0,0,0,1.32,1.83.22.22,0,0,0,.36-.24,19.75,19.75,0,0,1-.75-2.11,3.77,3.77,0,0,1,0-2.53c.22-.41.42-.82.6-1.21a.46.46,0,0,0-.27-.63c-.34-.11-.7-.25-1.06-.39s-.69-.16-1.07-1.17c-.27-.75-.45-1.22-.63-1.63a.15.15,0,0,0-.29.05,14.6,14.6,0,0,0,0,1.92c.06,1.08-.12,1.23-.45,1.66a10.79,10.79,0,0,1-.8,1" />
<path d="M33.9,39.77a27.92,27.92,0,0,0,8-1,3.41,3.41,0,0,0,2.71-3,43.56,43.56,0,0,0-.17-5.07,38.17,38.17,0,0,0-1.32-6.53,27.66,27.66,0,0,0-2.85-6.44c-.28-.45-.77-1.77-5.25-.81a.43.43,0,0,0-.33.62c6.63,10.74,1.35,19.07-1.08,21.78a.3.3,0,0,0,.25.5" />
<path d="M31.77,9.77a.23.23,0,0,0,.06-.46A40.35,40.35,0,0,0,23,8.05a26.2,26.2,0,0,0-6,.65c-.71.26-.87,1.57-.87,1.57s5.2,1.4,6.52,3.36a.26.26,0,0,1-.05.34L14,22.08a.08.08,0,0,0,0,.1,9.3,9.3,0,0,1,1.25,4c0,.12,0,.38,0,.38a.12.12,0,0,0,.12.1.19.19,0,0,0,.1-.05s9.45-8.47,9.45-8.47a5.38,5.38,0,0,1,1.47-.93c8.24-3.52,12.34-1.92,12.93-1.66a.08.08,0,0,0,.12,0l.25-.2s-.05-.11-.05-.11L35.94,12a.54.54,0,0,0-.16-.1c-3.91-1.74-10,.44-11,.82a.22.22,0,0,1-.23-.05l-.72-.72a.21.21,0,0,1,0-.33,14.69,14.69,0,0,1,8-1.84" />
<circle
cx="29.27"
cy="29.27"
r="28.76"
fill="none"
stroke="currentColor"
strokeWidth="1.02"
/>
</svg>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const SOURCE_TYPE_MAP: Record<
linear: "issue",
zendesk: "ticket",
conversations: "ticket",
pganalyze: "issue",
};

const ERROR_TRACKING_SOURCE_TYPES: SourceType[] = [
Expand All @@ -42,6 +43,7 @@ const SOURCE_LABELS: Record<keyof SignalSourceValues, string> = {
linear: "Linear Issues",
zendesk: "Zendesk Tickets",
conversations: "PostHog Support",
pganalyze: "pganalyze",
};

const DATA_WAREHOUSE_SOURCES: Record<
Expand All @@ -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)[] = [
Expand All @@ -60,6 +63,7 @@ const ALL_SOURCE_PRODUCTS: (keyof SignalSourceValues)[] = [
"linear",
"zendesk",
"conversations",
"pganalyze",
];

function computeValues(
Expand All @@ -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) {
Expand Down Expand Up @@ -113,7 +118,7 @@ export function useSignalSourceManager() {
const pendingRef = useRef(new Set<keyof SignalSourceValues>());

const [setupSource, setSetupSource] = useState<
"github" | "linear" | "zendesk" | null
"github" | "linear" | "zendesk" | "pganalyze" | null
>(null);
const [loadingSources, setLoadingSources] = useState<
Partial<Record<keyof SignalSourceValues, boolean>>
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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);
}
}, []);
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export type SourceProduct =
| "github"
| "linear"
| "zendesk"
| "conversations";
| "conversations"
| "pganalyze";

const DEFAULT_STATUS_FILTER: SignalReportStatus[] = [
"ready",
Expand Down
Loading