Merge branch 'dev' into sur-73-impr-implement-new-main-app-ux
|
|
@ -5,7 +5,7 @@ import { Navbar } from "@/components/homepage/navbar";
|
|||
|
||||
export default function HomePageLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white overflow-x-hidden">
|
||||
<main className="min-h-screen bg-linear-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white overflow-x-hidden">
|
||||
<Navbar />
|
||||
{children}
|
||||
<FooterNew />
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
createInviteMutationAtom,
|
||||
|
|
@ -116,6 +116,7 @@ import type {
|
|||
} from "@/contracts/types/roles.types";
|
||||
import { invitesApiService } from "@/lib/apis/invites-api.service";
|
||||
import { rolesApiService } from "@/lib/apis/roles-api.service";
|
||||
import { trackSearchSpaceInviteSent, trackSearchSpaceUsersViewed } from "@/lib/posthog/events";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -297,6 +298,14 @@ export default function TeamManagementPage() {
|
|||
toast.success("Team data refreshed");
|
||||
}, [fetchMembers, fetchRoles, fetchInvites]);
|
||||
|
||||
// Track users per search space when team page is viewed
|
||||
useEffect(() => {
|
||||
if (members.length > 0 && !membersLoading) {
|
||||
const ownerCount = members.filter((m) => m.is_owner).length;
|
||||
trackSearchSpaceUsersViewed(searchSpaceId, members.length, ownerCount);
|
||||
}
|
||||
}, [members, membersLoading, searchSpaceId]);
|
||||
|
||||
if (accessLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
|
|
@ -1088,10 +1097,12 @@ function InvitesTab({
|
|||
function CreateInviteDialog({
|
||||
roles,
|
||||
onCreateInvite,
|
||||
searchSpaceId,
|
||||
className,
|
||||
}: {
|
||||
roles: Role[];
|
||||
onCreateInvite: (data: CreateInviteRequest["data"]) => Promise<Invite>;
|
||||
searchSpaceId: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
|
@ -1114,6 +1125,17 @@ function CreateInviteDialog({
|
|||
|
||||
const invite = await onCreateInvite(data);
|
||||
setCreatedInvite(invite);
|
||||
|
||||
// Track invite sent event
|
||||
const roleName =
|
||||
roleId && roleId !== "default"
|
||||
? roles.find((r) => r.id.toString() === roleId)?.name
|
||||
: undefined;
|
||||
trackSearchSpaceInviteSent(searchSpaceId, {
|
||||
roleName,
|
||||
hasExpiry: !!expiresAt,
|
||||
hasMaxUses: !!maxUses,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to create invite:", error);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ const DashboardPage = () => {
|
|||
email:
|
||||
user?.email ||
|
||||
(isLoadingUser ? "Loading..." : userError ? "Error loading user" : "Unknown User"),
|
||||
avatar: "/icon-128.png", // Default avatar
|
||||
avatar: "/icon-128.svg", // Default avatar
|
||||
};
|
||||
|
||||
if (loading) return <LoadingScreen />;
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -33,6 +33,11 @@ import {
|
|||
import type { AcceptInviteResponse } from "@/contracts/types/invites.types";
|
||||
import { invitesApiService } from "@/lib/apis/invites-api.service";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
import {
|
||||
trackSearchSpaceInviteAccepted,
|
||||
trackSearchSpaceInviteDeclined,
|
||||
trackSearchSpaceUserAdded,
|
||||
} from "@/lib/posthog/events";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
export default function InviteAcceptPage() {
|
||||
|
|
@ -91,6 +96,18 @@ export default function InviteAcceptPage() {
|
|||
if (result) {
|
||||
setAccepted(true);
|
||||
setAcceptedData(result);
|
||||
|
||||
// Track invite accepted and user added events
|
||||
trackSearchSpaceInviteAccepted(
|
||||
result.search_space_id,
|
||||
result.search_space_name,
|
||||
result.role_name
|
||||
);
|
||||
trackSearchSpaceUserAdded(
|
||||
result.search_space_id,
|
||||
result.search_space_name,
|
||||
result.role_name
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to accept invite");
|
||||
|
|
@ -99,6 +116,12 @@ export default function InviteAcceptPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
// Track invite declined event
|
||||
trackSearchSpaceInviteDeclined(inviteInfo?.search_space_name);
|
||||
router.push("/dashboard");
|
||||
};
|
||||
|
||||
const handleLoginRedirect = () => {
|
||||
// Store the invite code to redirect back after login
|
||||
localStorage.setItem("pending_invite_code", inviteCode);
|
||||
|
|
@ -324,11 +347,7 @@ export default function InviteAcceptPage() {
|
|||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => router.push("/dashboard")}
|
||||
>
|
||||
<Button variant="outline" className="flex-1" onClick={handleDecline}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="flex-1 gap-2" onClick={handleAccept} disabled={accepting}>
|
||||
|
|
@ -360,7 +379,7 @@ export default function InviteAcceptPage() {
|
|||
href="/"
|
||||
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Image src="/icon-128.png" alt="SurfSense" width={24} height={24} className="rounded" />
|
||||
<Image src="/icon-128.svg" alt="SurfSense" width={24} height={24} className="rounded" />
|
||||
<span className="text-sm font-medium">SurfSense</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -1,60 +1,179 @@
|
|||
import type { MetadataRoute } from "next";
|
||||
|
||||
// Returns a date rounded to the current hour (updates only once per hour)
|
||||
function getHourlyDate(): Date {
|
||||
const now = new Date();
|
||||
now.setMinutes(0, 0, 0);
|
||||
return now;
|
||||
}
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const lastModified = getHourlyDate();
|
||||
|
||||
return [
|
||||
{
|
||||
url: "https://www.surfsense.com/",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/contact",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 1,
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/pricing",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/privacy",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/terms",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
// Documentation pages
|
||||
{
|
||||
url: "https://www.surfsense.com/docs",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
priority: 0.9,
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/installation",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/docker-installation",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/manual-installation",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
// Connector documentation
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/airtable",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/bookstack",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/circleback",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/clickup",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/confluence",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/discord",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/elasticsearch",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/github",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/gmail",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/google-calendar",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/google-drive",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/jira",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/linear",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/luma",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/microsoft-teams",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/notion",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/slack",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/web-crawler",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
48
surfsense_web/changelog/content/2026-01-08.mdx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
title: "SurfSense v0.0.11 - Connectors And More Connectors"
|
||||
description: "SurfSense v0.0.11 delivers powerful new integrations for our AI enterprise search platform, including Google Drive and Circleback connectors, multi-account support, and a fully responsive mobile interface."
|
||||
date: "2026-01-08"
|
||||
tags: ["Mobile", "UX", "Integrations", "Connectors"]
|
||||
version: "0.0.11"
|
||||
---
|
||||
|
||||
<img src="/changelog/0.0.11/header.gif" alt="SurfSense v0.0.11 - Connectors And More Connectors" className="rounded-lg w-full" />
|
||||
|
||||
## What's New in v0.0.11
|
||||
|
||||
This release focuses on **connectivity and ease of use** for your enterprise search software. We've begun a comprehensive UX overhaul, streamlining how you connect your data sources, alongside a fully responsive mobile interface that lets you access SurfSense's AI enterprise search capabilities from anywhere.
|
||||
|
||||
### New Features
|
||||
|
||||
- **Mobile-Ready Interface**: A fully responsive UI implementation allows you to search and collaborate seamlessly from your mobile device, bringing enterprise search solutions to your pocket.
|
||||
- **Streamlined Connector Management**: We've simplified the connector setup and management pages as part of a larger, ongoing UX overhaul for a smoother experience.
|
||||
- **Google Drive Integration**: Added a dedicated connector for Google Drive, featuring granular file selection to index only what you need for precise enterprise search.
|
||||
- **Circleback Support**: Introducing a new connector for Circleback to integrate your meeting notes and insights into your unified knowledge base.
|
||||
- **Simplified OAuth Authentication**: All supported connectors have been migrated to OAuth, making setup faster and more secure across your enterprise search software stack.
|
||||
- **Multi-Account Support**: Connect multiple accounts for the same service (e.g., Personal and Work Google Drives) to unify all your data sources in one AI-powered search hub.
|
||||
|
||||
<Accordion type="multiple" className="w-full not-prose">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Bug Fixes</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<ul className="list-disc space-y-2 pl-4">
|
||||
<li>Fixed a login issue affecting specific Google accounts on surfsense.com</li>
|
||||
<li>Resolved most Docker self-hosting configuration issues for easier deployment</li>
|
||||
</ul>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>For Self-Hosters</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<ul className="list-disc space-y-2 pl-4">
|
||||
<li>Docker configuration has been streamlined for smoother self-hosted deployments</li>
|
||||
<li>OAuth setup is now consistent across all connectors</li>
|
||||
</ul>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
SurfSense is an open-source AI enterprise search solution that connects all your knowledge sources, from Google Drive to Slack to meeting notes, in one intelligent, federated search platform. Whether you're looking for enterprise search software for your team or a personal knowledge assistant, SurfSense delivers powerful enterprise search solutions with the flexibility of self-hosting.
|
||||
|
||||
🚀 Connect more, search smarter!
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ import { cn } from "@/lib/utils";
|
|||
export const Logo = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<Link href="/">
|
||||
<Image src="/icon-128.png" className={cn(className)} alt="logo" width={128} height={128} />
|
||||
<Image src="/icon-128.svg" className={cn("dark:invert", className)} alt="logo" width={128} height={128} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -248,7 +248,8 @@ export const ConnectorIndicator: FC = () => {
|
|||
onBack={handleBackFromEdit}
|
||||
onQuickIndex={
|
||||
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
|
||||
? () => handleQuickIndexConnector(editingConnector.id)
|
||||
? () =>
|
||||
handleQuickIndexConnector(editingConnector.id, editingConnector.connector_type)
|
||||
: undefined
|
||||
}
|
||||
onConfigChange={setConnectorConfig}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import { Info } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type { ConnectorConfigProps } from "../index";
|
||||
|
||||
export interface TeamsConfigProps extends ConnectorConfigProps {
|
||||
onNameChange?: (name: string) => void;
|
||||
}
|
||||
|
||||
export const TeamsConfig: FC<TeamsConfigProps> = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 shrink-0 mt-0.5">
|
||||
<Info className="size-4" />
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm">
|
||||
<p className="font-medium text-xs sm:text-sm">Microsoft Teams Access</p>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-sm">
|
||||
SurfSense will index messages from Teams channels that you have access to. The app can
|
||||
only read messages from teams and channels where you are a member. Make sure you're a
|
||||
member of the teams you want to index before connecting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -17,6 +17,7 @@ import { LumaConfig } from "./components/luma-config";
|
|||
import { SearxngConfig } from "./components/searxng-config";
|
||||
import { SlackConfig } from "./components/slack-config";
|
||||
import { TavilyApiConfig } from "./components/tavily-api-config";
|
||||
import { TeamsConfig } from "./components/teams-config";
|
||||
import { WebcrawlerConfig } from "./components/webcrawler-config";
|
||||
|
||||
export interface ConnectorConfigProps {
|
||||
|
|
@ -52,6 +53,8 @@ export function getConnectorConfigComponent(
|
|||
return SlackConfig;
|
||||
case "DISCORD_CONNECTOR":
|
||||
return DiscordConfig;
|
||||
case "TEAMS_CONNECTOR":
|
||||
return TeamsConfig;
|
||||
case "CONFLUENCE_CONNECTOR":
|
||||
return ConfluenceConfig;
|
||||
case "BOOKSTACK_CONNECTOR":
|
||||
|
|
|
|||
|
|
@ -51,6 +51,13 @@ export const OAUTH_CONNECTORS = [
|
|||
connectorType: EnumConnectorName.SLACK_CONNECTOR,
|
||||
authEndpoint: "/api/v1/auth/slack/connector/add/",
|
||||
},
|
||||
{
|
||||
id: "teams-connector",
|
||||
title: "Microsoft Teams",
|
||||
description: "Search Teams messages",
|
||||
connectorType: EnumConnectorName.TEAMS_CONNECTOR,
|
||||
authEndpoint: "/api/v1/auth/teams/connector/add/",
|
||||
},
|
||||
{
|
||||
id: "discord-connector",
|
||||
title: "Discord",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ import { EnumConnectorName } from "@/contracts/enums/connector";
|
|||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { searchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import {
|
||||
trackConnectorConnected,
|
||||
trackConnectorDeleted,
|
||||
trackIndexWithDateRangeOpened,
|
||||
trackIndexWithDateRangeStarted,
|
||||
trackPeriodicIndexingStarted,
|
||||
trackQuickIndexClicked,
|
||||
} from "@/lib/posthog/events";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { queryClient } from "@/lib/query-client/client";
|
||||
import type { IndexingConfigState } from "../constants/connector-constants";
|
||||
|
|
@ -309,6 +317,13 @@ export const useConnectorDialog = () => {
|
|||
if (newConnector) {
|
||||
const connectorValidation = searchSourceConnector.safeParse(newConnector);
|
||||
if (connectorValidation.success) {
|
||||
// Track connector connected event for OAuth connectors
|
||||
trackConnectorConnected(
|
||||
Number(searchSpaceId),
|
||||
oauthConnector.connectorType,
|
||||
newConnector.id
|
||||
);
|
||||
|
||||
const config = validateIndexingConfigState({
|
||||
connectorType: oauthConnector.connectorType,
|
||||
connectorId: newConnector.id,
|
||||
|
|
@ -419,6 +434,13 @@ export const useConnectorDialog = () => {
|
|||
if (connector) {
|
||||
const connectorValidation = searchSourceConnector.safeParse(connector);
|
||||
if (connectorValidation.success) {
|
||||
// Track webcrawler connector connected
|
||||
trackConnectorConnected(
|
||||
Number(searchSpaceId),
|
||||
EnumConnectorName.WEBCRAWLER_CONNECTOR,
|
||||
connector.id
|
||||
);
|
||||
|
||||
const config = validateIndexingConfigState({
|
||||
connectorType: EnumConnectorName.WEBCRAWLER_CONNECTOR,
|
||||
connectorId: connector.id,
|
||||
|
|
@ -514,6 +536,9 @@ export const useConnectorDialog = () => {
|
|||
// Store connectingConnectorType before clearing it
|
||||
const currentConnectorType = connectingConnectorType;
|
||||
|
||||
// Track connector connected event for non-OAuth connectors
|
||||
trackConnectorConnected(Number(searchSpaceId), currentConnectorType, connector.id);
|
||||
|
||||
// Find connector title from constants
|
||||
const connectorInfo = OTHER_CONNECTORS.find(
|
||||
(c) => c.connectorType === currentConnectorType
|
||||
|
|
@ -848,6 +873,27 @@ export const useConnectorDialog = () => {
|
|||
});
|
||||
}
|
||||
|
||||
// Track index with date range started event
|
||||
trackIndexWithDateRangeStarted(
|
||||
Number(searchSpaceId),
|
||||
indexingConfig.connectorType,
|
||||
indexingConfig.connectorId,
|
||||
{
|
||||
hasStartDate: !!startDate,
|
||||
hasEndDate: !!endDate,
|
||||
}
|
||||
);
|
||||
|
||||
// Track periodic indexing started if enabled
|
||||
if (periodicEnabled && indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR") {
|
||||
trackPeriodicIndexingStarted(
|
||||
Number(searchSpaceId),
|
||||
indexingConfig.connectorType,
|
||||
indexingConfig.connectorId,
|
||||
parseInt(frequencyMinutes, 10)
|
||||
);
|
||||
}
|
||||
|
||||
toast.success(`${indexingConfig.connectorTitle} indexing started`, {
|
||||
description: periodicEnabled
|
||||
? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutes)}.`
|
||||
|
|
@ -914,6 +960,15 @@ export const useConnectorDialog = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Track index with date range opened event
|
||||
if (connector.is_indexable) {
|
||||
trackIndexWithDateRangeOpened(
|
||||
Number(searchSpaceId),
|
||||
connector.connector_type,
|
||||
connector.id
|
||||
);
|
||||
}
|
||||
|
||||
setEditingConnector(connector);
|
||||
setConnectorName(connector.name);
|
||||
// Load existing periodic sync settings (disabled for Google Drive and non-indexable connectors)
|
||||
|
|
@ -1049,6 +1104,36 @@ export const useConnectorDialog = () => {
|
|||
indexingDescription = "Re-indexing started with new date range.";
|
||||
}
|
||||
|
||||
// Track indexing started if re-indexing was performed
|
||||
if (
|
||||
editingConnector.is_indexable &&
|
||||
(indexingDescription.includes("Re-indexing") || indexingDescription.includes("indexing"))
|
||||
) {
|
||||
trackIndexWithDateRangeStarted(
|
||||
Number(searchSpaceId),
|
||||
editingConnector.connector_type,
|
||||
editingConnector.id,
|
||||
{
|
||||
hasStartDate: !!startDateStr,
|
||||
hasEndDate: !!endDateStr,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Track periodic indexing if enabled (for non-Google Drive connectors)
|
||||
if (
|
||||
periodicEnabled &&
|
||||
editingConnector.is_indexable &&
|
||||
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
|
||||
) {
|
||||
trackPeriodicIndexingStarted(
|
||||
Number(searchSpaceId),
|
||||
editingConnector.connector_type,
|
||||
editingConnector.id,
|
||||
frequency || parseInt(frequencyMinutes, 10)
|
||||
);
|
||||
}
|
||||
|
||||
toast.success(`${editingConnector.name} updated successfully`, {
|
||||
description: periodicEnabled
|
||||
? `Periodic sync ${frequency ? `enabled every ${getFrequencyLabel(frequencyMinutes)}` : "enabled"}. ${indexingDescription}`
|
||||
|
|
@ -1101,6 +1186,13 @@ export const useConnectorDialog = () => {
|
|||
id: editingConnector.id,
|
||||
});
|
||||
|
||||
// Track connector deleted event
|
||||
trackConnectorDeleted(
|
||||
Number(searchSpaceId),
|
||||
editingConnector.connector_type,
|
||||
editingConnector.id
|
||||
);
|
||||
|
||||
toast.success(`${editingConnector.name} disconnected successfully`);
|
||||
|
||||
// Update URL - the effect will handle closing the modal and clearing state
|
||||
|
|
@ -1127,9 +1219,14 @@ export const useConnectorDialog = () => {
|
|||
|
||||
// Handle quick index (index without date picker, uses backend defaults)
|
||||
const handleQuickIndexConnector = useCallback(
|
||||
async (connectorId: number) => {
|
||||
async (connectorId: number, connectorType?: string) => {
|
||||
if (!searchSpaceId) return;
|
||||
|
||||
// Track quick index clicked event
|
||||
if (connectorType) {
|
||||
trackQuickIndexClicked(Number(searchSpaceId), connectorType, connectorId);
|
||||
}
|
||||
|
||||
try {
|
||||
await indexConnector({
|
||||
connector_id: connectorId,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
|
||||
// Direct mappings (connector type matches document type)
|
||||
SLACK_CONNECTOR: "SLACK_CONNECTOR",
|
||||
TEAMS_CONNECTOR: "TEAMS_CONNECTOR",
|
||||
NOTION_CONNECTOR: "NOTION_CONNECTOR",
|
||||
GITHUB_CONNECTOR: "GITHUB_CONNECTOR",
|
||||
LINEAR_CONNECTOR: "LINEAR_CONNECTOR",
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const DesktopNav = ({ navItems, isScrolled }: any) => {
|
|||
: "bg-transparent border border-transparent"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-1 flex-row items-center gap-2">
|
||||
<div className="flex flex-1 flex-row items-center gap-0.5">
|
||||
<Logo className="h-8 w-8 rounded-md" />
|
||||
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"discord",
|
||||
"jira",
|
||||
"linear",
|
||||
"microsoft-teams",
|
||||
"confluence",
|
||||
"airtable",
|
||||
"clickup",
|
||||
|
|
|
|||
101
surfsense_web/content/docs/connectors/microsoft-teams.mdx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
title: Microsoft Teams
|
||||
description: Connect your Microsoft Teams to SurfSense
|
||||
---
|
||||
|
||||
# Microsoft Teams OAuth Integration Setup Guide
|
||||
|
||||
This guide walks you through setting up a Microsoft Teams OAuth integration for SurfSense using Azure App Registration.
|
||||
|
||||
## Step 1: Access Azure App Registrations
|
||||
|
||||
1. Navigate to [portal.azure.com](https://portal.azure.com)
|
||||
2. In the search bar, type **"app reg"**
|
||||
3. Select **"App registrations"** from the Services results
|
||||
|
||||

|
||||
|
||||
## Step 2: Create New Registration
|
||||
|
||||
1. On the **App registrations** page, click **"+ New registration"**
|
||||
|
||||

|
||||
|
||||
## Step 3: Register the Application
|
||||
|
||||
Fill in the application details:
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Name** | `SurfSense` |
|
||||
| **Supported account types** | Select **"Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts"** |
|
||||
| **Redirect URI** | Platform: `Web`, URI: `http://localhost:8000/api/v1/auth/teams/connector/callback` |
|
||||
|
||||
Click **"Register"**
|
||||
|
||||

|
||||
|
||||
## Step 4: Get Application (Client) ID
|
||||
|
||||
After registration, you'll be taken to the app's **Overview** page. Here you'll find:
|
||||
|
||||
1. Copy the **Application (client) ID** - this is your Client ID
|
||||
2. Note the **Directory (tenant) ID** if needed
|
||||
|
||||

|
||||
|
||||
## Step 5: Create Client Secret
|
||||
|
||||
1. In the left sidebar under **Manage**, click **"Certificates & secrets"**
|
||||
2. Select the **"Client secrets"** tab
|
||||
3. Click **"+ New client secret"**
|
||||
4. Enter a description (e.g., `SurfSense`) and select an expiration period
|
||||
5. Click **"Add"**
|
||||
|
||||

|
||||
|
||||
6. **Important**: Copy the secret **Value** immediately - it won't be shown again!
|
||||
|
||||

|
||||
|
||||
> ⚠️ Never share your client secret publicly or include it in code repositories.
|
||||
|
||||
## Step 6: Configure API Permissions
|
||||
|
||||
1. In the left sidebar under **Manage**, click **"API permissions"**
|
||||
2. Click **"+ Add a permission"**
|
||||
3. Select **"Microsoft Graph"**
|
||||
4. Select **"Delegated permissions"**
|
||||
5. Add the following permissions:
|
||||
|
||||
| Permission | Type | Description | Admin Consent |
|
||||
|------------|------|-------------|---------------|
|
||||
| `Channel.ReadBasic.All` | Delegated | Read the names and descriptions of channels | No |
|
||||
| `ChannelMessage.Read.All` | Delegated | Read user channel messages | Yes |
|
||||
| `offline_access` | Delegated | Maintain access to data you have given it access to | No |
|
||||
| `Team.ReadBasic.All` | Delegated | Read the names and descriptions of teams | No |
|
||||
| `User.Read` | Delegated | Sign in and read user profile | No |
|
||||
|
||||
6. Click **"Add permissions"**
|
||||
|
||||
> ⚠️ The `ChannelMessage.Read.All` permission requires admin consent. An admin will need to click **"Grant admin consent for [Directory]"** for full functionality.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Running SurfSense with Microsoft Teams Connector
|
||||
|
||||
Add the Microsoft Teams environment variables to your Docker run command:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:3000 -p 8000:8000 \
|
||||
-v surfsense-data:/data \
|
||||
# Microsoft Teams Connector
|
||||
-e TEAMS_CLIENT_ID=your_microsoft_client_id \
|
||||
-e TEAMS_CLIENT_SECRET=your_microsoft_client_secret \
|
||||
-e TEAMS_REDIRECT_URI=http://localhost:8000/api/v1/auth/teams/connector/callback \
|
||||
--name surfsense \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/modsetter/surfsense:latest
|
||||
```
|
||||
|
|
@ -4,6 +4,7 @@ export enum EnumConnectorName {
|
|||
LINKUP_API = "LINKUP_API",
|
||||
BAIDU_SEARCH_API = "BAIDU_SEARCH_API",
|
||||
SLACK_CONNECTOR = "SLACK_CONNECTOR",
|
||||
TEAMS_CONNECTOR = "TEAMS_CONNECTOR",
|
||||
NOTION_CONNECTOR = "NOTION_CONNECTOR",
|
||||
GITHUB_CONNECTOR = "GITHUB_CONNECTOR",
|
||||
LINEAR_CONNECTOR = "LINEAR_CONNECTOR",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
|
|||
return <Image src="/connectors/baidu-search.svg" alt="Baidu" {...imgProps} />;
|
||||
case EnumConnectorName.SLACK_CONNECTOR:
|
||||
return <Image src="/connectors/slack.svg" alt="Slack" {...imgProps} />;
|
||||
case EnumConnectorName.TEAMS_CONNECTOR:
|
||||
return <Image src="/connectors/microsoft-teams.svg" alt="Microsoft Teams" {...imgProps} />;
|
||||
case EnumConnectorName.NOTION_CONNECTOR:
|
||||
return <Image src="/connectors/notion.svg" alt="Notion" {...imgProps} />;
|
||||
case EnumConnectorName.DISCORD_CONNECTOR:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const searchSourceConnectorTypeEnum = z.enum([
|
|||
"LINKUP_API",
|
||||
"BAIDU_SEARCH_API",
|
||||
"SLACK_CONNECTOR",
|
||||
"TEAMS_CONNECTOR",
|
||||
"NOTION_CONNECTOR",
|
||||
"GITHUB_CONNECTOR",
|
||||
"LINEAR_CONNECTOR",
|
||||
|
|
|
|||
|
|
@ -271,6 +271,156 @@ export function trackSourcesTabViewed(searchSpaceId: number, tab: string) {
|
|||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SEARCH SPACE INVITE EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackSearchSpaceInviteSent(
|
||||
searchSpaceId: number,
|
||||
options?: {
|
||||
roleName?: string;
|
||||
hasExpiry?: boolean;
|
||||
hasMaxUses?: boolean;
|
||||
}
|
||||
) {
|
||||
posthog.capture("search_space_invite_sent", {
|
||||
search_space_id: searchSpaceId,
|
||||
role_name: options?.roleName,
|
||||
has_expiry: options?.hasExpiry ?? false,
|
||||
has_max_uses: options?.hasMaxUses ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackSearchSpaceInviteAccepted(
|
||||
searchSpaceId: number,
|
||||
searchSpaceName: string,
|
||||
roleName?: string | null
|
||||
) {
|
||||
posthog.capture("search_space_invite_accepted", {
|
||||
search_space_id: searchSpaceId,
|
||||
search_space_name: searchSpaceName,
|
||||
role_name: roleName,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackSearchSpaceInviteDeclined(searchSpaceName?: string) {
|
||||
posthog.capture("search_space_invite_declined", {
|
||||
search_space_name: searchSpaceName,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackSearchSpaceUserAdded(
|
||||
searchSpaceId: number,
|
||||
searchSpaceName: string,
|
||||
roleName?: string | null
|
||||
) {
|
||||
posthog.capture("search_space_user_added", {
|
||||
search_space_id: searchSpaceId,
|
||||
search_space_name: searchSpaceName,
|
||||
role_name: roleName,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackSearchSpaceUsersViewed(
|
||||
searchSpaceId: number,
|
||||
userCount: number,
|
||||
ownerCount: number
|
||||
) {
|
||||
posthog.capture("search_space_users_viewed", {
|
||||
search_space_id: searchSpaceId,
|
||||
user_count: userCount,
|
||||
owner_count: ownerCount,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CONNECTOR CONNECTION EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackConnectorConnected(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
connectorId?: number
|
||||
) {
|
||||
posthog.capture("connector_connected", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
connector_id: connectorId,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// INDEXING EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackIndexWithDateRangeOpened(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
connectorId: number
|
||||
) {
|
||||
posthog.capture("index_with_date_range_opened", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
connector_id: connectorId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackIndexWithDateRangeStarted(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
connectorId: number,
|
||||
options?: {
|
||||
hasStartDate?: boolean;
|
||||
hasEndDate?: boolean;
|
||||
}
|
||||
) {
|
||||
posthog.capture("index_with_date_range_started", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
connector_id: connectorId,
|
||||
has_start_date: options?.hasStartDate ?? false,
|
||||
has_end_date: options?.hasEndDate ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackQuickIndexClicked(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
connectorId: number
|
||||
) {
|
||||
posthog.capture("quick_index_clicked", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
connector_id: connectorId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackConfigurePeriodicIndexingOpened(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
connectorId: number
|
||||
) {
|
||||
posthog.capture("configure_periodic_indexing_opened", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
connector_id: connectorId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackPeriodicIndexingStarted(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
connectorId: number,
|
||||
frequencyMinutes: number
|
||||
) {
|
||||
posthog.capture("periodic_indexing_started", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
connector_id: connectorId,
|
||||
frequency_minutes: frequencyMinutes,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// USER IDENTIFICATION
|
||||
// ============================================
|
||||
|
|
|
|||
BIN
surfsense_web/public/changelog/0.0.11/header.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
9
surfsense_web/public/icon-128.svg
Normal file
|
After Width: | Height: | Size: 123 KiB |