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
2 changes: 2 additions & 0 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ import { processTrackingModule } from "@posthog/workspace-server/services/proces
import { SECURE_STORE_SERVICE } from "@posthog/workspace-server/services/secure-store/identifiers";
import { shellModule } from "@posthog/workspace-server/services/shell/shell.module";
import { skillsModule } from "@posthog/workspace-server/services/skills/skills.module";
import { skillsMarketplaceModule } from "@posthog/workspace-server/services/skills-marketplace/skills-marketplace.module";
import {
SUSPENSION_FILE_WATCHER,
SUSPENSION_SERVICE,
Expand Down Expand Up @@ -574,6 +575,7 @@ container
container.load(posthogPluginModule);
container.bind(MAIN_POSTHOG_PLUGIN_SERVICE).toService(POSTHOG_PLUGIN_SERVICE);
container.load(skillsModule);
container.load(skillsMarketplaceModule);
container.load(onboardingImportModule);
container.load(additionalDirectoriesModule);
container.bind(MAIN_SLEEP_SERVICE).to(SleepService);
Expand Down
39 changes: 39 additions & 0 deletions packages/host-router/src/routers/skills.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ import {
skillPathOutput,
} from "@posthog/workspace-server/services/skills/schemas";
import type { SkillsService } from "@posthog/workspace-server/services/skills/skills";
import { SKILLS_MARKETPLACE_SERVICE } from "@posthog/workspace-server/services/skills-marketplace/identifiers";
import {
marketplaceInstallInput,
marketplaceInstallOutput,
marketplacePreviewOutput,
marketplaceSearchInput,
marketplaceSearchOutput,
marketplaceSkillRef,
} from "@posthog/workspace-server/services/skills-marketplace/schemas";
import type { SkillsMarketplaceService } from "@posthog/workspace-server/services/skills-marketplace/skills-marketplace";

export const skillsRouter = router({
list: publicProcedure
Expand Down Expand Up @@ -89,4 +99,33 @@ export const skillsRouter = router({
yield event;
}
}),
marketplace: router({
search: publicProcedure
.input(marketplaceSearchInput)
.output(marketplaceSearchOutput)
.query(({ ctx, input }) =>
ctx.container
.get<SkillsMarketplaceService>(SKILLS_MARKETPLACE_SERVICE)
.search(input.query),
),
preview: publicProcedure
.input(marketplaceSkillRef)
.output(marketplacePreviewOutput)
.query(({ ctx, input }) =>
ctx.container
.get<SkillsMarketplaceService>(SKILLS_MARKETPLACE_SERVICE)
.preview(input),
),
install: publicProcedure
.input(marketplaceInstallInput)
.output(marketplaceInstallOutput)
.mutation(({ ctx, input }) =>
ctx.container
.get<SkillsMarketplaceService>(SKILLS_MARKETPLACE_SERVICE)
.install(
{ source: input.source, skillId: input.skillId },
input.overwrite ?? false,
),
),
}),
});
161 changes: 161 additions & 0 deletions packages/ui/src/features/skills/MarketplaceBrowse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { MagnifyingGlass, Storefront } from "@phosphor-icons/react";
import { useDebouncedValue } from "@posthog/ui/primitives/hooks/useDebouncedValue";
import { ResizableSidebar } from "@posthog/ui/primitives/ResizableSidebar";
import {
Badge,
Box,
Flex,
ScrollArea,
Text,
TextField,
} from "@radix-ui/themes";
import { useState } from "react";
import { MarketplaceSkillPanel } from "./MarketplaceSkillPanel";
import { useSkillsSidebarStore } from "./skillsSidebarStore";
import {
type MarketplaceSkillSummary,
useMarketplaceSearch,
} from "./useMarketplace";

const installsFormatter = new Intl.NumberFormat(undefined, {
notation: "compact",
});

export function MarketplaceBrowse() {
const [query, setQuery] = useState("");
const { debounced: debouncedQuery } = useDebouncedValue(query, 300);
const [selected, setSelected] = useState<MarketplaceSkillSummary | null>(
null,
);

const { data, isLoading, error } = useMarketplaceSearch(debouncedQuery);
const results = data?.results ?? [];

const {
width: sidebarWidth,
setWidth: setSidebarWidth,
isResizing,
setIsResizing,
} = useSkillsSidebarStore();

return (
<Flex className="min-h-0 flex-1">
<Box flexGrow="1" className="min-w-0">
<ScrollArea type="auto" className="scroll-area-constrain-width h-full">
<Box px="4" py="3">
<Box pb="3">
<TextField.Root
size="2"
placeholder="Search community skills..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="text-[13px]"
>
<TextField.Slot>
<MagnifyingGlass size={14} />
</TextField.Slot>
</TextField.Root>
</Box>

{debouncedQuery.trim().length < 2 ? (
<BrowseEmptyState message="Search the community skills index from skills.sh" />
) : error ? (
<BrowseEmptyState message="Could not reach the skills index. Check your connection and try again." />
) : isLoading ? (
<Text className="text-[12px] text-gray-9">Searching...</Text>
) : results.length === 0 ? (
<BrowseEmptyState message="No skills found" />
) : (
<Flex direction="column" gap="1">
{results.map((result) => (
<Flex
key={result.id}
align="center"
gap="2"
px="3"
py="2"
className={`cursor-pointer rounded-lg border transition-colors ${
selected?.id === result.id
? "border-accent-8 bg-accent-3"
: "border-gray-6 bg-gray-2 hover:border-gray-8 hover:bg-gray-3"
}`}
onClick={() =>
setSelected((prev) =>
prev?.id === result.id ? null : result,
)
}
>
<Box className="flex shrink-0 items-center justify-center rounded bg-gray-4 p-1.5">
<Storefront
size={14}
weight="duotone"
className="text-gray-11"
/>
</Box>
<Flex direction="column" gap="0" className="min-w-0 flex-1">
<Text className="truncate font-medium text-[13px] text-gray-12">
{result.name}
</Text>
<Text className="truncate text-[12px] text-gray-10">
{result.source}
</Text>
</Flex>
{result.installed && (
Comment thread
k11kirky marked this conversation as resolved.
<Badge
size="1"
variant="soft"
color="green"
className="shrink-0"
>
Installed
</Badge>
)}
<Text className="shrink-0 text-[12px] text-gray-9 tabular-nums">
{installsFormatter.format(result.installs)}
</Text>
</Flex>
))}
</Flex>
)}
</Box>
</ScrollArea>
</Box>

<ResizableSidebar
open={!!selected}
width={sidebarWidth}
setWidth={setSidebarWidth}
isResizing={isResizing}
setIsResizing={setIsResizing}
side="right"
>
{selected && (
<MarketplaceSkillPanel
key={selected.id}
result={selected}
onClose={() => setSelected(null)}
/>
)}
</ResizableSidebar>
</Flex>
);
}

function BrowseEmptyState({ message }: { message: string }) {
return (
<Flex
align="center"
justify="center"
direction="column"
gap="3"
className="py-12"
>
<Box className="rounded-lg border border-gray-6 border-dashed p-4">
<Storefront size={24} className="text-gray-8" />
</Box>
<Text className="max-w-[360px] text-center text-[13px] text-gray-10">
{message}
</Text>
</Flex>
);
}
Loading
Loading