Skip to content
Merged
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
8 changes: 8 additions & 0 deletions apps/code/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,14 @@ snapshots:
hash: v1.k4693efd2.39d47f04f9ee6a6508342a8cddd00169b4b8c6628d1f63d81759e20f6dec136a.XJ6pqjbtYJv9ESWNflv286xYvR2Pn3xg44mLVyWTnv4
git-dialogs--sync-success--light:
hash: v1.k4693efd2.8d79d77c9338aeaa73734836f0e17fde987f6d3239914f4013889d3110e5088d.Rq-w2o1uhes_a5hUWqMXeiDkfnoPCZPt932LkEQXCco
loops-loopslistview--comprehensive--dark:
hash: v1.k4693efd2.a9b9a8b23cca2079c3ced1878b6537ee9fbbdd0d3de205a9584fbe7d6875d405.dWfER84S8r_MU7EVDmjW8CuCf6lNhTx6RjfV_cNzKOA
loops-loopslistview--comprehensive--light:
hash: v1.k4693efd2.9ff3f3787ef56ab9d1d39562eed6f19b6c9c1e7a6d79965fbf99fbac16ba33fb.DDuj_KZx0pcUxXwCgzNGFmCemCT1MuCzIQmbbPMgatw
loops-loopslistview--long-mixed-list--dark:
hash: v1.k4693efd2.683dedbfa7da3313eeaf52ff9e3cc7908af5c77173b1cad450b2acd61ac30cd4.YFm7fXVZSr29fa0R40pzq-FjRZrqIBEsIMiOiddB840
loops-loopslistview--long-mixed-list--light:
hash: v1.k4693efd2.577fa23e3bc43f0ba033a59669fa73035ff2d65df1f994cf5753ac306ca830cf.r3v1hne0uC4dHpkzXriz6yzcA3fD2b8wD6ZLxA_0Fuo
scouts-scoutsfleetlist--filtered-to-you--dark:
hash: v1.k4693efd2.a3af6e76ef36598eaa347bedc4430878ed62754660166398e0576a9978cd084b.x3Ky-O6HOw0srWdOmnnKJwFNpmGmQVWip0t7CwVaRXY
scouts-scoutsfleetlist--filtered-to-you--light:
Expand Down
12 changes: 10 additions & 2 deletions packages/api-client/src/posthog-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2589,6 +2589,14 @@ export class PostHogAPIClient {
// Everyone in the current organization — the pool of taggable teammates for
// thread @-mentions. Membership churn is slow, so callers cache aggressively.
async listOrganizationMembers(): Promise<OrganizationMemberBasic[]> {
const result = await this.listOrganizationMembersWithStatus();
return result.members;
}

async listOrganizationMembersWithStatus(): Promise<{
members: OrganizationMemberBasic[];
isComplete: boolean;
}> {
const ORG_MEMBERS_MAX_PAGES = 20;
const ORG_MEMBERS_PAGE_SIZE = 200;
const all: OrganizationMemberBasic[] = [];
Expand All @@ -2609,15 +2617,15 @@ export class PostHogAPIClient {
next: string | null;
};
all.push(...page.results);
if (!page.next) return all;
if (!page.next) return { members: all, isComplete: true };
const nextUrl = new URL(page.next);
urlPath = `${nextUrl.pathname}${nextUrl.search}`;
}
log.warn(
`listOrganizationMembers hit MAX_PAGES (${ORG_MEMBERS_MAX_PAGES}); returning partial results`,
{ returned: all.length },
);
return all;
return { members: all, isComplete: false };
}

async sendRunCommand(
Expand Down
20 changes: 15 additions & 5 deletions packages/ui/src/features/canvas/hooks/useOrgMembers.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,43 @@
import type { UserBasic } from "@posthog/shared/domain-types";
import { useAuthStateValue } from "@posthog/ui/features/auth/store";
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
import { useAuthenticatedQuery } from "@posthog/ui/hooks/useAuthenticatedQuery";
import { useMemo } from "react";

// Membership churn is slow; one fetch per session window is plenty.
const ORG_MEMBERS_STALE_MS = 5 * 60_000;

export const ORG_MEMBERS_QUERY_KEY = ["org-members"] as const;
export const orgMembersQueryKey = (orgId: string | null) =>
["org-members", orgId] as const;

/** Members of the current organization, sorted by display name. */
export function useOrgMembers(options?: { enabled?: boolean }): {
members: UserBasic[];
isLoading: boolean;
isError: boolean;
isComplete: boolean;
} {
const currentOrgId = useAuthStateValue((state) => state.currentOrgId);
const query = useAuthenticatedQuery(
ORG_MEMBERS_QUERY_KEY,
(client) => client.listOrganizationMembers(),
orgMembersQueryKey(currentOrgId),
(client) => client.listOrganizationMembersWithStatus(),
{
enabled: options?.enabled ?? true,
staleTime: ORG_MEMBERS_STALE_MS,
},
);
const members = useMemo(
() =>
(query.data ?? [])
(query.data?.members ?? [])
.map((member) => member.user)
.filter((user) => !!user?.email)
.sort((a, b) => userDisplayName(a).localeCompare(userDisplayName(b))),
[query.data],
);
return { members, isLoading: query.isLoading };
return {
members,
isLoading: query.isLoading,
isError: query.isError,
isComplete: query.data?.isComplete ?? false,
};
}
41 changes: 41 additions & 0 deletions packages/ui/src/features/loops/components/LoopDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
Switch,
Textarea,
} from "@posthog/quill";
import { UserAvatar } from "@posthog/ui/features/auth/UserAvatar";
import { useOrgMembers } from "@posthog/ui/features/canvas/hooks/useOrgMembers";
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent";
import { toast } from "@posthog/ui/primitives/toast";
import {
Expand All @@ -33,6 +36,7 @@ import {
describeTrigger,
loopStatusColor,
loopStatusLabel,
summarizeNotificationDestinations,
} from "../loopDisplay";
import { LoopLoadError } from "./LoopFallbacks";
import { LoopRunRow } from "./LoopRunRow";
Expand Down Expand Up @@ -259,6 +263,33 @@ function loopStatusBadgeVariant(

function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
const displayModel = useLoopDisplayModel(loop.runtime_adapter, loop.model);
const {
members,
isLoading: membersLoading,
isError: membersError,
isComplete: membersComplete,
} = useOrgMembers({ enabled: loop.visibility === "team" });
const creator = members.find((member) => member.id === loop.created_by_id);
let creatorContent: React.ReactNode = null;
if (loop.visibility === "team" && membersError) {
creatorContent = "Creator unavailable";
} else if (loop.visibility === "team" && membersLoading) {
creatorContent = "Loading…";
} else if (loop.visibility === "team" && creator) {
creatorContent = (
<Flex align="center" gap="2">
<UserAvatar user={creator} size="xs" />
{userDisplayName(creator)}
</Flex>
);
} else if (loop.visibility === "team" && membersComplete) {
creatorContent = "Former organization member";
} else if (loop.visibility === "team") {
creatorContent = "Creator unavailable";
}
const notificationDestinations = summarizeNotificationDestinations(
loop.notifications,
);

return (
<Flex direction="column" gap="3">
Expand Down Expand Up @@ -287,6 +318,10 @@ function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
: "None (connector-only loop)"}
</SummaryRow>

{loop.visibility === "team" ? (
<SummaryRow label="Created by">{creatorContent}</SummaryRow>
) : null}

<SummaryRow label="Triggers">
{loop.triggers.length === 0 ? (
"No triggers configured"
Expand All @@ -301,6 +336,12 @@ function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
</Flex>
)}
</SummaryRow>

{notificationDestinations.length > 0 ? (
<SummaryRow label="Notifications">
{notificationDestinations.join(", ")}
</SummaryRow>
) : null}
</Flex>
</Flex>
);
Expand Down
66 changes: 58 additions & 8 deletions packages/ui/src/features/loops/components/LoopRow.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
import { CaretRightIcon, RepeatIcon } from "@phosphor-icons/react";
import type { LoopSchemas } from "@posthog/api-client/loops";
import type { UserBasic } from "@posthog/shared/domain-types";
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
import { Badge } from "@posthog/ui/primitives/Badge";
import { Flex, Text } from "@radix-ui/themes";
import { Link } from "@tanstack/react-router";
import { loopStatusColor, loopStatusLabel } from "../loopDisplay";
import {
loopStatusColor,
loopStatusLabel,
summarizeNotificationDestinations,
} from "../loopDisplay";

export function LoopRow({
loop,
creator,
creatorLoading = false,
creatorError = false,
creatorLookupComplete = true,
}: {
loop: LoopSchemas.Loop;
creator?: UserBasic;
creatorLoading?: boolean;
creatorError?: boolean;
creatorLookupComplete?: boolean;
}) {
const description = loop.description.trim();
const triggerLabel = loop.triggers.length === 1 ? "trigger" : "triggers";
const triggerSummary =
loop.triggers.length === 0
? "No triggers configured"
: `${loop.triggers.length} ${triggerLabel}`;

let creatorLabel: string | null = null;
if (loop.visibility === "team" && creatorError) {
creatorLabel = "Creator unavailable";
} else if (
loop.visibility === "team" &&
!creatorLoading &&
!creatorLookupComplete
) {
creatorLabel = "Creator unavailable";
} else if (loop.visibility === "team" && !creatorLoading) {
creatorLabel = creator
? `Created by ${userDisplayName(creator)}`
: "Created by a former organization member";
}

const metadata = [triggerSummary];
const notificationDestinations = summarizeNotificationDestinations(
loop.notifications,
);
if (notificationDestinations.length > 0) {
metadata.push(`Notifications: ${notificationDestinations.join(", ")}`);
}
if (creatorLabel) metadata.push(creatorLabel);

export function LoopRow({ loop }: { loop: LoopSchemas.Loop }) {
return (
<Link
to="/code/loops/$loopId"
Expand All @@ -22,13 +71,14 @@ export function LoopRow({ loop }: { loop: LoopSchemas.Loop }) {
<Badge color={loopStatusColor(loop)}>{loopStatusLabel(loop)}</Badge>
<Badge color="gray">{loop.visibility}</Badge>
</Flex>
<Text className="truncate text-[12px] text-gray-11 leading-snug">
{loop.description.trim()
? loop.description
: loop.triggers.length === 0
? "No triggers configured"
: `${loop.triggers.length} trigger${loop.triggers.length === 1 ? "" : "s"}`}
<Text className="text-[12px] text-gray-11 leading-snug">
{metadata.join(" · ")}
</Text>
{description ? (
<Text className="truncate text-[12px] text-gray-10 leading-snug">
{description}
</Text>
) : null}
</Flex>
</Flex>
<Flex align="center" gap="3" className="shrink-0">
Expand Down
Loading
Loading