diff --git a/packages/web/src/app/[domain]/connections/[id]/components/connectionError.tsx b/packages/web/src/app/[domain]/connections/[id]/components/connectionError.tsx deleted file mode 100644 index 928cdca7..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/components/connectionError.tsx +++ /dev/null @@ -1,110 +0,0 @@ -"use client" - -import { BackendError } from "@sourcebot/error"; -import { Prisma } from "@sourcebot/db"; - -export function DisplayConnectionError({ syncStatusMetadata, onSecretsClick }: { syncStatusMetadata: Prisma.JsonValue, onSecretsClick: () => void }) { - const errorCode = syncStatusMetadata && typeof syncStatusMetadata === 'object' && 'error' in syncStatusMetadata - ? (syncStatusMetadata.error as string) - : undefined; - - switch (errorCode) { - case BackendError.CONNECTION_SYNC_INVALID_TOKEN: - return - case BackendError.CONNECTION_SYNC_SECRET_DNE: - return - case BackendError.CONNECTION_SYNC_SYSTEM_ERROR: - return - case BackendError.CONNECTION_SYNC_FAILED_TO_FETCH_GERRIT_PROJECTS: - return - default: - return - } -} - -function SecretNotFoundError({ syncStatusMetadata, onSecretsClick }: { syncStatusMetadata: Prisma.JsonValue, onSecretsClick: () => void }) { - const secretKey = syncStatusMetadata && typeof syncStatusMetadata === 'object' && 'secretKey' in syncStatusMetadata - ? (syncStatusMetadata.secretKey as string) - : undefined; - - return ( -
-

Secret Not Found

-

- The secret key provided for this connection was not found. Please ensure your config is referencing a secret - that exists in your{" "} - - , and try again. -

- {secretKey && ( -

- Secret Key: {secretKey} -

- )} -
- ); -} - -function InvalidTokenError({ syncStatusMetadata, onSecretsClick }: { syncStatusMetadata: Prisma.JsonValue, onSecretsClick: () => void }) { - const secretKey = syncStatusMetadata && typeof syncStatusMetadata === 'object' && 'secretKey' in syncStatusMetadata - ? (syncStatusMetadata.secretKey as string) - : undefined; - - return ( -
-

Invalid Authentication Token

-

- The authentication token provided for this connection is invalid. Please update your config with a valid token and try again. -

- {secretKey && ( -

- Secret Key: -

- )} -
- ); -} - -function SystemError() { - return ( -
-

System Error

-

- An error occurred while syncing this connection. Please try again later. -

-
- ) -} - -function FailedToFetchGerritProjects({ syncStatusMetadata }: { syncStatusMetadata: Prisma.JsonValue}) { - const status = syncStatusMetadata && typeof syncStatusMetadata === 'object' && 'status' in syncStatusMetadata - ? (syncStatusMetadata.status as number) - : undefined; - - return ( -
-

Failed to Fetch Gerrit Projects

-

- An error occurred while syncing this connection. Please try again later. -

- {status && ( -

- Status: {status} -

- )} -
- ) -} - -function UnknownError() { - return ( -
-

Unknown Error

-

- An unknown error occurred while syncing this connection. Please try again later. -

-
- ) -} diff --git a/packages/web/src/app/[domain]/connections/[id]/components/deleteConnectionSetting.tsx b/packages/web/src/app/[domain]/connections/[id]/components/deleteConnectionSetting.tsx deleted file mode 100644 index 1eb2488d..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/components/deleteConnectionSetting.tsx +++ /dev/null @@ -1,100 +0,0 @@ -'use client'; - -import { Button } from "@/components/ui/button"; -import { useCallback, useState } from "react"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, - } from "@/components/ui/alert-dialog"; -import { deleteConnection } from "@/actions"; -import { Loader2 } from "lucide-react"; -import { isServiceError } from "@/lib/utils"; -import { useToast } from "@/components/hooks/use-toast"; -import { useRouter } from "next/navigation"; -import { useDomain } from "@/hooks/useDomain"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; - -interface DeleteConnectionSettingProps { - connectionId: number; - disabled?: boolean; -} - -export const DeleteConnectionSetting = ({ - connectionId, - disabled, -}: DeleteConnectionSettingProps) => { - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const domain = useDomain(); - const { toast } = useToast(); - const router = useRouter(); - const captureEvent = useCaptureEvent(); - - const handleDelete = useCallback(() => { - setIsDialogOpen(false); - setIsLoading(true); - deleteConnection(connectionId, domain) - .then((response) => { - if (isServiceError(response)) { - toast({ - description: `❌ Failed to delete connection. Reason: ${response.message}` - }); - captureEvent('wa_connection_delete_fail', { - error: response.errorCode, - }); - } else { - toast({ - description: `✅ Connection deleted successfully.` - }); - captureEvent('wa_connection_delete_success', {}); - router.replace(`/${domain}/connections`); - router.refresh(); - } - }) - .finally(() => { - setIsLoading(false); - }); - }, [connectionId, domain, router, toast, captureEvent]); - - return ( -
-

Delete Connection

-

- Permanently delete this connection from Sourcebot. All linked repositories that are not linked to any other connection will also be deleted. -

-
- - - - - - - Are you sure? - - This action cannot be undone. - - - - Cancel - Yes, delete connection - - - -
-
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/[id]/components/displayNameSetting.tsx b/packages/web/src/app/[domain]/connections/[id]/components/displayNameSetting.tsx deleted file mode 100644 index e7f795e8..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/components/displayNameSetting.tsx +++ /dev/null @@ -1,100 +0,0 @@ -'use client'; - -import { updateConnectionDisplayName } from "@/actions"; -import { useToast } from "@/components/hooks/use-toast"; -import { Button } from "@/components/ui/button"; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { useDomain } from "@/hooks/useDomain"; -import { isServiceError } from "@/lib/utils"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Loader2 } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useCallback, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -const formSchema = z.object({ - name: z.string().min(1), -}); - -interface DisplayNameSettingProps { - connectionId: number; - name: string; - disabled?: boolean; -} - -export const DisplayNameSetting = ({ - connectionId, - name, - disabled, -}: DisplayNameSettingProps) => { - const { toast } = useToast(); - const router = useRouter(); - const domain = useDomain(); - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - name, - }, - }); - - const [isLoading, setIsLoading] = useState(false); - const onSubmit = useCallback((data: z.infer) => { - setIsLoading(true); - updateConnectionDisplayName(connectionId, data.name, domain) - .then((response) => { - if (isServiceError(response)) { - toast({ - description: `❌ Failed to rename connection. Reason: ${response.message}` - }); - } else { - toast({ - description: `✅ Connection renamed successfully.` - }); - router.refresh(); - } - }).finally(() => { - setIsLoading(false); - }); - }, [connectionId, domain, router, toast]); - - return ( -
-
- - ( - - Display Name - {/* @todo : refactor this description into a shared file */} - This is the {`connection's`} display name within Sourcebot. Examples: public-github, self-hosted-gitlab, gerrit-other, etc. - - - - - - )} - /> -
- -
- - -
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/[id]/components/notFoundWarning.tsx b/packages/web/src/app/[domain]/connections/[id]/components/notFoundWarning.tsx deleted file mode 100644 index c65d4f10..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/components/notFoundWarning.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client'; - -import { AlertTriangle } from "lucide-react" -import { Prisma, ConnectionSyncStatus } from "@sourcebot/db" -import { SyncStatusMetadataSchema } from "@/lib/syncStatusMetadataSchema" -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { ReloadIcon } from "@radix-ui/react-icons"; -import { Button } from "@/components/ui/button"; - -interface NotFoundWarningProps { - syncStatus: ConnectionSyncStatus - syncStatusMetadata: Prisma.JsonValue - onSecretsClick: () => void - connectionType: string - onRetrySync: () => void -} - -export const NotFoundWarning = ({ syncStatus, syncStatusMetadata, onSecretsClick, connectionType, onRetrySync }: NotFoundWarningProps) => { - const captureEvent = useCaptureEvent(); - - const parseResult = SyncStatusMetadataSchema.safeParse(syncStatusMetadata); - if (syncStatus !== ConnectionSyncStatus.SYNCED_WITH_WARNINGS || !parseResult.success || !parseResult.data.notFound) { - return null; - } - - const { notFound } = parseResult.data; - - if (notFound.users.length === 0 && notFound.orgs.length === 0 && notFound.repos.length === 0) { - return null; - } else { - captureEvent('wa_connection_not_found_warning_displayed', {}); - } - - return ( -
-
- -

Unable to fetch all references

-
-

- Some requested references couldn't be found. Please ensure you've provided the information listed below correctly, and that you've provided a{" "} - {" "} - to access them if they're private. -

-
    - {notFound.users.length > 0 && ( -
  • - Users: - {notFound.users.join(', ')} -
  • - )} - {notFound.orgs.length > 0 && ( -
  • - {connectionType === "gitlab" ? "Groups" : "Organizations"}: - {notFound.orgs.join(', ')} -
  • - )} - {notFound.repos.length > 0 && ( -
  • - {connectionType === "gitlab" ? "Projects" : "Repositories"}: - {notFound.repos.join(', ')} -
  • - )} -
-
- -
-
- ) -} diff --git a/packages/web/src/app/[domain]/connections/[id]/components/overview.tsx b/packages/web/src/app/[domain]/connections/[id]/components/overview.tsx deleted file mode 100644 index 6b7dbbaf..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/components/overview.tsx +++ /dev/null @@ -1,159 +0,0 @@ -'use client'; - -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" -import { DisplayConnectionError } from "./connectionError" -import { NotFoundWarning } from "./notFoundWarning" -import { useDomain } from "@/hooks/useDomain"; -import { useRouter } from "next/navigation"; -import { useCallback } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { flagConnectionForSync, getConnectionInfo } from "@/actions"; -import { isServiceError, unwrapServiceError } from "@/lib/utils"; -import { env } from "@/env.mjs"; -import { ConnectionSyncStatus } from "@sourcebot/db"; -import { FiLoader } from "react-icons/fi"; -import { CircleCheckIcon, AlertTriangle, CircleXIcon } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { ReloadIcon } from "@radix-ui/react-icons"; -import { toast } from "@/components/hooks/use-toast"; - -interface OverviewProps { - connectionId: number; -} - -export const Overview = ({ connectionId }: OverviewProps) => { - const captureEvent = useCaptureEvent(); - const domain = useDomain(); - const router = useRouter(); - - const { data: connection, isPending, error, refetch } = useQuery({ - queryKey: ['connection', domain, connectionId], - queryFn: () => unwrapServiceError(getConnectionInfo(connectionId, domain)), - refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, - }); - - const handleSecretsNavigation = useCallback(() => { - captureEvent('wa_connection_secrets_navigation_pressed', {}); - router.push(`/${domain}/secrets`); - }, [captureEvent, domain, router]); - - const onRetrySync = useCallback(async () => { - const result = await flagConnectionForSync(connectionId, domain); - if (isServiceError(result)) { - toast({ - description: `❌ Failed to flag connection for sync.`, - }); - captureEvent('wa_connection_retry_sync_fail', { - error: result.errorCode, - }); - } else { - toast({ - description: "✅ Connection flagged for sync.", - }); - captureEvent('wa_connection_retry_sync_success', {}); - refetch(); - } - }, [connectionId, domain, captureEvent, refetch]); - - - if (error) { - return
- {`Error loading connection. Reason: ${error.message}`} -
- } - - if (isPending) { - return ( -
- {Array.from({ length: 4 }).map((_, i) => ( -
-
-
-
- ))} -
- ) - } - - return ( -
-
-
-

Connection Type

-

{connection.connectionType}

-
-
-

Last Synced At

-

- {connection.syncedAt ? new Date(connection.syncedAt).toLocaleDateString() : "never"} -

-
-
-

Linked Repositories

-

{connection.numLinkedRepos}

-
-
-

Status

-
- {connection.syncStatus === "FAILED" ? ( - - captureEvent('wa_connection_failed_status_hover', {})}> - - - - - - - ) : ( - - )} - {connection.syncStatus === "FAILED" && ( - - )} -
-
-
- -
- ) -} - -const SyncStatusBadge = ({ status }: { status: ConnectionSyncStatus }) => { - return ( - - {status === ConnectionSyncStatus.SYNC_NEEDED || status === ConnectionSyncStatus.IN_SYNC_QUEUE ? ( - <> Sync queued - ) : status === ConnectionSyncStatus.SYNCING ? ( - <> Syncing - ) : status === ConnectionSyncStatus.SYNCED ? ( - Synced - ) : status === ConnectionSyncStatus.SYNCED_WITH_WARNINGS ? ( - Synced with warnings - ) : status === ConnectionSyncStatus.FAILED ? ( - <> Sync failed - ) : null} - - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/[id]/components/repoList.tsx b/packages/web/src/app/[domain]/connections/[id]/components/repoList.tsx deleted file mode 100644 index cab5c4b1..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/components/repoList.tsx +++ /dev/null @@ -1,229 +0,0 @@ -'use client'; - -import { useDomain } from "@/hooks/useDomain"; -import { useQuery } from "@tanstack/react-query"; -import { flagReposForIndex, getConnectionInfo, getRepos } from "@/actions"; -import { RepoListItem } from "./repoListItem"; -import { isServiceError, unwrapServiceError } from "@/lib/utils"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { ConnectionSyncStatus, RepoIndexingStatus } from "@sourcebot/db"; -import { Search, Loader2 } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { useCallback, useMemo, useState } from "react"; -import { RepoListItemSkeleton } from "./repoListItemSkeleton"; -import { env } from "@/env.mjs"; -import { Button } from "@/components/ui/button"; -import { useRouter } from "next/navigation"; -import { MultiSelect } from "@/components/ui/multi-select"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { useToast } from "@/components/hooks/use-toast"; - -interface RepoListProps { - connectionId: number; -} - -const getPriority = (status: RepoIndexingStatus) => { - switch (status) { - case RepoIndexingStatus.FAILED: - return 0 - case RepoIndexingStatus.IN_INDEX_QUEUE: - case RepoIndexingStatus.INDEXING: - return 1 - case RepoIndexingStatus.INDEXED: - return 2 - default: - return 3 - } -} - -const convertIndexingStatus = (status: RepoIndexingStatus) => { - switch (status) { - case RepoIndexingStatus.FAILED: - return 'failed'; - case RepoIndexingStatus.NEW: - return 'waiting'; - case RepoIndexingStatus.IN_INDEX_QUEUE: - case RepoIndexingStatus.INDEXING: - return 'running'; - case RepoIndexingStatus.INDEXED: - return 'succeeded'; - default: - return 'unknown'; - } -} - -export const RepoList = ({ connectionId }: RepoListProps) => { - const domain = useDomain(); - const router = useRouter(); - const { toast } = useToast(); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedStatuses, setSelectedStatuses] = useState([]); - const captureEvent = useCaptureEvent(); - const [isRetryAllFailedReposLoading, setIsRetryAllFailedReposLoading] = useState(false); - - const { data: unfilteredRepos, isPending: isReposPending, error: reposError, refetch: refetchRepos } = useQuery({ - queryKey: ['repos', domain, connectionId], - queryFn: async () => { - const repos = await unwrapServiceError(getRepos()); - return repos.sort((a, b) => { - const priorityA = getPriority(a.repoIndexingStatus); - const priorityB = getPriority(b.repoIndexingStatus); - - // First sort by priority - if (priorityA !== priorityB) { - return priorityA - priorityB; - } - - // If same priority, sort by indexedAt - return new Date(a.indexedAt ?? new Date()).getTime() - new Date(b.indexedAt ?? new Date()).getTime(); - }); - }, - refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, - }); - - const { data: connection, isPending: isConnectionPending, error: isConnectionError } = useQuery({ - queryKey: ['connection', domain, connectionId], - queryFn: () => unwrapServiceError(getConnectionInfo(connectionId, domain)), - }) - - - const failedRepos = useMemo(() => { - return unfilteredRepos?.filter((repo) => repo.repoIndexingStatus === RepoIndexingStatus.FAILED) ?? []; - }, [unfilteredRepos]); - - - const onRetryAllFailedRepos = useCallback(() => { - if (failedRepos.length === 0) { - return; - } - - setIsRetryAllFailedReposLoading(true); - flagReposForIndex(failedRepos.map((repo) => repo.repoId)) - .then((response) => { - if (isServiceError(response)) { - captureEvent('wa_connection_retry_all_failed_repos_fail', {}); - toast({ - description: `❌ Failed to flag repositories for indexing. Reason: ${response.message}`, - }); - } else { - captureEvent('wa_connection_retry_all_failed_repos_success', {}); - toast({ - description: `✅ ${failedRepos.length} repositories flagged for indexing.`, - }); - } - }) - .then(() => { refetchRepos() }) - .finally(() => { - setIsRetryAllFailedReposLoading(false); - }); - }, [captureEvent, failedRepos, refetchRepos, toast]); - - const filteredRepos = useMemo(() => { - if (isServiceError(unfilteredRepos)) { - return unfilteredRepos; - } - - const searchLower = searchQuery.toLowerCase(); - return unfilteredRepos?.filter((repo) => { - return repo.repoName.toLowerCase().includes(searchLower); - }).filter((repo) => { - if (selectedStatuses.length === 0) { - return true; - } - - return selectedStatuses.includes(convertIndexingStatus(repo.repoIndexingStatus)); - }); - }, [unfilteredRepos, searchQuery, selectedStatuses]); - - if (reposError) { - return
- {`Error loading repositories. Reason: ${reposError.message}`} -
- } - - return ( -
-
-
- - setSearchQuery(e.target.value)} - /> -
- - setSelectedStatuses(value)} - defaultValue={[]} - placeholder="Filter by status" - maxCount={2} - animation={0} - /> - - {failedRepos.length > 0 && ( - - )} -
- - {isReposPending ? ( -
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
- ) : (!filteredRepos || filteredRepos.length === 0) ? ( -
-

No Repositories Found

-

- { - searchQuery.length > 0 ? ( - No repositories found matching your filters. - ) : (!isConnectionError && !isConnectionPending && (connection.syncStatus === ConnectionSyncStatus.IN_SYNC_QUEUE || connection.syncStatus === ConnectionSyncStatus.SYNCING || connection.syncStatus === ConnectionSyncStatus.SYNC_NEEDED)) ? ( - Repositories are being synced. Please check back soon. - ) : ( - - )} -

-
- ) : ( -
- {filteredRepos?.map((repo) => ( - - ))} -
- )} -
-
- ) -} diff --git a/packages/web/src/app/[domain]/connections/[id]/components/repoListItem.tsx b/packages/web/src/app/[domain]/connections/[id]/components/repoListItem.tsx deleted file mode 100644 index fd491376..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/components/repoListItem.tsx +++ /dev/null @@ -1,113 +0,0 @@ -'use client'; - -import { getDisplayTime, getRepoImageSrc } from "@/lib/utils"; -import Image from "next/image"; -import { StatusIcon } from "../../components/statusIcon"; -import { RepoIndexingStatus } from "@sourcebot/db"; -import { useMemo } from "react"; -import { RetryRepoIndexButton } from "./repoRetryIndexButton"; - - -interface RepoListItemProps { - name: string; - status: RepoIndexingStatus; - imageUrl?: string; - indexedAt?: Date; - repoId: number; - domain: string; -} - -export const RepoListItem = ({ - imageUrl, - name, - indexedAt, - status, - repoId, - domain, -}: RepoListItemProps) => { - const statusDisplayName = useMemo(() => { - switch (status) { - case RepoIndexingStatus.NEW: - return 'Waiting...'; - case RepoIndexingStatus.IN_INDEX_QUEUE: - return 'In index queue...'; - case RepoIndexingStatus.INDEXING: - return 'Indexing...'; - case RepoIndexingStatus.INDEXED: - return 'Indexed'; - case RepoIndexingStatus.FAILED: - return 'Index failed'; - case RepoIndexingStatus.IN_GC_QUEUE: - return 'In garbage collection queue...'; - case RepoIndexingStatus.GARBAGE_COLLECTING: - return 'Garbage collecting...'; - case RepoIndexingStatus.GARBAGE_COLLECTION_FAILED: - return 'Garbage collection failed'; - } - }, [status]); - - const imageSrc = getRepoImageSrc(imageUrl, repoId, domain); - - return ( -
-
- {imageSrc ? ( - {name} - ) : ( -
- {name.charAt(0)} -
- )} -

{name}

-
-
- {status === RepoIndexingStatus.FAILED && ( - - )} -
- -

- {statusDisplayName} - { - ( - status === RepoIndexingStatus.INDEXED || - status === RepoIndexingStatus.FAILED - ) && indexedAt && ( - {` ${getDisplayTime(indexedAt)}`} - ) - } -

-
-
-
- ) -} - -const convertIndexingStatus = (status: RepoIndexingStatus) => { - switch (status) { - case RepoIndexingStatus.NEW: - return 'waiting'; - case RepoIndexingStatus.IN_INDEX_QUEUE: - case RepoIndexingStatus.INDEXING: - return 'running'; - case RepoIndexingStatus.IN_GC_QUEUE: - case RepoIndexingStatus.GARBAGE_COLLECTING: - return "garbage-collecting" - case RepoIndexingStatus.INDEXED: - return 'succeeded'; - case RepoIndexingStatus.GARBAGE_COLLECTION_FAILED: - case RepoIndexingStatus.FAILED: - return 'failed'; - } -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/[id]/components/repoListItemSkeleton.tsx b/packages/web/src/app/[domain]/connections/[id]/components/repoListItemSkeleton.tsx deleted file mode 100644 index 2eab9a38..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/components/repoListItemSkeleton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Skeleton } from "@/components/ui/skeleton" - -export const RepoListItemSkeleton = () => { - return ( -
-
- - -
-
- -
-
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/[id]/components/repoRetryIndexButton.tsx b/packages/web/src/app/[domain]/connections/[id]/components/repoRetryIndexButton.tsx deleted file mode 100644 index ad44c60e..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/components/repoRetryIndexButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { ReloadIcon } from "@radix-ui/react-icons" -import { toast } from "@/components/hooks/use-toast"; -import { flagReposForIndex } from "@/actions"; -import { isServiceError } from "@/lib/utils"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; - -interface RetryRepoIndexButtonProps { - repoId: number; -} - -export const RetryRepoIndexButton = ({ repoId }: RetryRepoIndexButtonProps) => { - const captureEvent = useCaptureEvent(); - - return ( - - ); -}; diff --git a/packages/web/src/app/[domain]/connections/[id]/page.tsx b/packages/web/src/app/[domain]/connections/[id]/page.tsx deleted file mode 100644 index 0e0a91d2..00000000 --- a/packages/web/src/app/[domain]/connections/[id]/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { NotFound } from "@/app/[domain]/components/notFound" -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb" -import { ConnectionIcon } from "../components/connectionIcon" -import { Header } from "../../components/header" -import { RepoList } from "./components/repoList" -import { getConnectionByDomain } from "@/data/connection" -import { Overview } from "./components/overview" - -interface ConnectionManagementPageProps { - params: Promise<{ - domain: string - id: string - }>, -} - -export default async function ConnectionManagementPage(props: ConnectionManagementPageProps) { - const params = await props.params; - const connection = await getConnectionByDomain(Number(params.id), params.domain); - if (!connection) { - return - } - - return ( -
-
- - - - Connections - - - - {connection.name} - - - -
- -

{connection.name}

-
-
- -
-
-

Overview

- -
- -
-

Linked Repositories

- -
-
-
- ) -} diff --git a/packages/web/src/app/[domain]/connections/components/connectionIcon.tsx b/packages/web/src/app/[domain]/connections/components/connectionIcon.tsx deleted file mode 100644 index 2c08121a..00000000 --- a/packages/web/src/app/[domain]/connections/components/connectionIcon.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client'; - -import { cn, CodeHostType, getCodeHostIcon } from "@/lib/utils"; -import { useMemo } from "react"; -import Image from "next/image"; -import placeholderLogo from "@/public/placeholder_avatar.png"; - -interface ConnectionIconProps { - type: string; - className?: string; -} - -export const ConnectionIcon = ({ - type, - className, -}: ConnectionIconProps) => { - const Icon = useMemo(() => { - const iconInfo = getCodeHostIcon(type as CodeHostType); - if (iconInfo) { - return ( - {`${type} - ) - } - - return {''} - - }, [className, type]); - - return Icon; -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItem.tsx b/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItem.tsx deleted file mode 100644 index 02c41139..00000000 --- a/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItem.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { getDisplayTime } from "@/lib/utils"; -import { useMemo } from "react"; -import { ConnectionIcon } from "../connectionIcon"; -import { ConnectionSyncStatus, Prisma } from "@sourcebot/db"; -import { StatusIcon } from "../statusIcon"; -import { ConnectionListItemErrorIndicator } from "./connectionListItemErrorIndicator"; -import { ConnectionListItemWarningIndicator } from "./connectionListItemWarningIndicator"; -import { ConnectionListItemManageButton } from "./connectionListItemManageButton"; - -const convertSyncStatus = (status: ConnectionSyncStatus) => { - switch (status) { - case ConnectionSyncStatus.SYNC_NEEDED: - return 'waiting'; - case ConnectionSyncStatus.IN_SYNC_QUEUE: - case ConnectionSyncStatus.SYNCING: - return 'running'; - case ConnectionSyncStatus.SYNCED: - return 'succeeded'; - case ConnectionSyncStatus.SYNCED_WITH_WARNINGS: - return 'succeeded-with-warnings'; - case ConnectionSyncStatus.FAILED: - return 'failed'; - } -} - -interface ConnectionListItemProps { - id: string; - name: string; - type: string; - status: ConnectionSyncStatus; - syncStatusMetadata: Prisma.JsonValue; - editedAt: Date; - syncedAt?: Date; - failedRepos?: { repoId: number, repoName: string }[]; - disabled: boolean; -} - -export const ConnectionListItem = ({ - id, - name, - type, - status, - syncStatusMetadata, - editedAt, - syncedAt, - failedRepos, - disabled, -}: ConnectionListItemProps) => { - const statusDisplayName = useMemo(() => { - switch (status) { - case ConnectionSyncStatus.SYNC_NEEDED: - return 'Waiting...'; - case ConnectionSyncStatus.IN_SYNC_QUEUE: - case ConnectionSyncStatus.SYNCING: - return 'Syncing...'; - case ConnectionSyncStatus.SYNCED: - return 'Synced'; - case ConnectionSyncStatus.FAILED: - return 'Sync failed'; - case ConnectionSyncStatus.SYNCED_WITH_WARNINGS: - return null; - } - }, [status]); - - const { notFoundData, displayNotFoundWarning } = useMemo(() => { - if (!syncStatusMetadata || typeof syncStatusMetadata !== 'object' || !('notFound' in syncStatusMetadata)) { - return { notFoundData: null, displayNotFoundWarning: false }; - } - - const notFoundData = syncStatusMetadata.notFound as { - users: string[], - orgs: string[], - repos: string[], - } - - return { notFoundData, displayNotFoundWarning: notFoundData.users.length > 0 || notFoundData.orgs.length > 0 || notFoundData.repos.length > 0 }; - }, [syncStatusMetadata]); - - return ( -
-
- -
-

{name}

- {`Edited ${getDisplayTime(editedAt)}`} -
- - -
-
- -

- {statusDisplayName} - { - ( - status === ConnectionSyncStatus.SYNCED || - status === ConnectionSyncStatus.FAILED - ) && syncedAt && ( - {` ${getDisplayTime(syncedAt)}`} - ) - } -

- -
-
- ) -} diff --git a/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItemErrorIndicator.tsx b/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItemErrorIndicator.tsx deleted file mode 100644 index 26c9ee21..00000000 --- a/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItemErrorIndicator.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client' - -import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; -import { CircleX } from "lucide-react"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; - -interface ConnectionListItemErrorIndicatorProps { - failedRepos: { repoId: number; repoName: string; }[] | undefined; - connectionId: string; -} - -export const ConnectionListItemErrorIndicator = ({ - failedRepos, - connectionId -}: ConnectionListItemErrorIndicatorProps) => { - const captureEvent = useCaptureEvent() - - if (!failedRepos || failedRepos.length === 0) return null; - - return ( - - - { - captureEvent('wa_connection_list_item_error_pressed', {}) - window.location.href = `connections/${connectionId}` - }} - onMouseEnter={() => captureEvent('wa_connection_list_item_error_hover', {})} - /> - - -
-
- -

Failed to Index Repositories

-
-
-

- {failedRepos.length} {failedRepos.length === 1 ? 'repository' : 'repositories'} failed to index. This is likely due to temporary server load. -

-
-
- {failedRepos.slice(0, 10).map(repo => ( - {repo.repoName} - ))} - {failedRepos.length > 10 && ( - - And {failedRepos.length - 10} more... - - )} -
-
-

- Navigate to the connection for more details and to retry indexing. -

-
-
-
-
- ); -}; diff --git a/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItemManageButton.tsx b/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItemManageButton.tsx deleted file mode 100644 index 00b7db27..00000000 --- a/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItemManageButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client' - -import { Button } from "@/components/ui/button"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { useRouter } from "next/navigation"; -import { useDomain } from "@/hooks/useDomain"; - -interface ConnectionListItemManageButtonProps { - id: string; - disabled: boolean; -} - -export const ConnectionListItemManageButton = ({ - id, - disabled, -}: ConnectionListItemManageButtonProps) => { - const captureEvent = useCaptureEvent() - const router = useRouter(); - const domain = useDomain(); - - return ( - - ); -}; diff --git a/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItemWarningIndicator.tsx b/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItemWarningIndicator.tsx deleted file mode 100644 index 690c9b18..00000000 --- a/packages/web/src/app/[domain]/connections/components/connectionList/connectionListItemWarningIndicator.tsx +++ /dev/null @@ -1,78 +0,0 @@ -'use client' - -import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; -import { AlertTriangle } from "lucide-react"; -import { NotFoundData } from "@/lib/syncStatusMetadataSchema"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; - - -interface ConnectionListItemWarningIndicatorProps { - notFoundData: NotFoundData | null; - connectionId: string; - type: string; - displayWarning: boolean; -} - -export const ConnectionListItemWarningIndicator = ({ - notFoundData, - connectionId, - type, - displayWarning -}: ConnectionListItemWarningIndicatorProps) => { - const captureEvent = useCaptureEvent() - - if (!notFoundData || !displayWarning) return null; - - return ( - - - { - captureEvent('wa_connection_list_item_warning_pressed', {}) - window.location.href = `connections/${connectionId}` - }} - onMouseEnter={() => captureEvent('wa_connection_list_item_warning_hover', {})} - /> - - -
-
- -

Unable to fetch all references

-
-

- Some requested references couldn't be found. Verify the details below and ensure your connection is using a {" "} - {" "} - that has access to any private references. -

-
    - {notFoundData.users.length > 0 && ( -
  • - Users: - {notFoundData.users.join(', ')} -
  • - )} - {notFoundData.orgs.length > 0 && ( -
  • - {type === "gitlab" ? "Groups" : "Organizations"}: - {notFoundData.orgs.join(', ')} -
  • - )} - {notFoundData.repos.length > 0 && ( -
  • - {type === "gitlab" ? "Projects" : "Repositories"}: - {notFoundData.repos.join(', ')} -
  • - )} -
-
-
-
- ); -}; \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/components/connectionList/index.tsx b/packages/web/src/app/[domain]/connections/components/connectionList/index.tsx deleted file mode 100644 index 94b4f8be..00000000 --- a/packages/web/src/app/[domain]/connections/components/connectionList/index.tsx +++ /dev/null @@ -1,144 +0,0 @@ -"use client"; -import { useDomain } from "@/hooks/useDomain"; -import { ConnectionListItem } from "./connectionListItem"; -import { cn, unwrapServiceError } from "@/lib/utils"; -import { InfoCircledIcon } from "@radix-ui/react-icons"; -import { getConnections } from "@/actions"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useQuery } from "@tanstack/react-query"; -import { env } from "@/env.mjs"; -import { RepoIndexingStatus, ConnectionSyncStatus } from "@sourcebot/db"; -import { Search } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { useMemo, useState } from "react"; -import { MultiSelect } from "@/components/ui/multi-select"; - -interface ConnectionListProps { - className?: string; - isDisabled: boolean; -} - -const convertSyncStatus = (status: ConnectionSyncStatus) => { - switch (status) { - case ConnectionSyncStatus.SYNC_NEEDED: - return 'waiting'; - case ConnectionSyncStatus.SYNCING: - return 'running'; - case ConnectionSyncStatus.SYNCED: - return 'succeeded'; - case ConnectionSyncStatus.SYNCED_WITH_WARNINGS: - return 'synced-with-warnings'; - case ConnectionSyncStatus.FAILED: - return 'failed'; - default: - return 'unknown'; - } -} - -export const ConnectionList = ({ - className, - isDisabled, -}: ConnectionListProps) => { - const domain = useDomain(); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedStatuses, setSelectedStatuses] = useState([]); - - const { data: unfilteredConnections, isPending, error } = useQuery({ - queryKey: ['connections', domain], - queryFn: () => unwrapServiceError(getConnections(domain)), - refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, - }); - - const connections = useMemo(() => { - return unfilteredConnections - ?.filter((connection) => connection.name.toLowerCase().includes(searchQuery.toLowerCase())) - .filter((connection) => { - if (selectedStatuses.length === 0) { - return true; - } - - return selectedStatuses.includes(convertSyncStatus(connection.syncStatus)); - }) - .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()) ?? []; - }, [unfilteredConnections, searchQuery, selectedStatuses]); - - if (error) { - return
-

Error loading connections: {error.message}

-
- } - - return ( -
-
-
- - setSearchQuery(e.target.value)} - /> -
- - setSelectedStatuses(value)} - defaultValue={[]} - placeholder="Filter by status" - maxCount={2} - animation={0} - /> - -
- - {isPending ? ( - // Skeleton for loading state -
- {Array.from({ length: 3 }).map((_, i) => ( -
- -
- - -
- -
- ))} -
- ) : connections.length > 0 ? ( - connections - .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()) - .map((connection) => ( - repo.repoIndexingStatus === RepoIndexingStatus.FAILED).map((repo) => ({ - repoId: repo.id, - repoName: repo.name, - }))} - disabled={isDisabled} - /> - )) - ) : ( -
- -

No connections

-
- )} -
- ); -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/components/statusIcon.tsx b/packages/web/src/app/[domain]/connections/components/statusIcon.tsx deleted file mode 100644 index 4aad8eb6..00000000 --- a/packages/web/src/app/[domain]/connections/components/statusIcon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { cn } from "@/lib/utils"; -import { CircleCheckIcon, CircleXIcon } from "lucide-react"; -import { useMemo } from "react"; -import { FiLoader } from "react-icons/fi"; - -export type Status = 'waiting' | 'running' | 'succeeded' | 'succeeded-with-warnings' | 'garbage-collecting' | 'failed'; - -export const StatusIcon = ({ - status, - className, -}: { status: Status, className?: string }) => { - const Icon = useMemo(() => { - switch (status) { - case 'waiting': - case 'garbage-collecting': - case 'running': - return ; - case 'succeeded': - return ; - case 'failed': - return ; - case 'succeeded-with-warnings': - default: - return null; - } - }, [className, status]); - - return Icon; -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/layout.tsx b/packages/web/src/app/[domain]/connections/layout.tsx deleted file mode 100644 index 9aeb3541..00000000 --- a/packages/web/src/app/[domain]/connections/layout.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { auth } from "@/auth"; -import { NavigationMenu } from "../components/navigationMenu"; -import { redirect } from "next/navigation"; - -interface LayoutProps { - children: React.ReactNode; - params: Promise<{ domain: string }>; -} - -export default async function Layout( - props: LayoutProps -) { - const params = await props.params; - - const { - domain - } = params; - - const { - children - } = props; - - const session = await auth(); - if (!session) { - return redirect(`/${domain}`); - } - - return ( -
- -
-
{children}
-
-
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/connections/page.tsx b/packages/web/src/app/[domain]/connections/page.tsx deleted file mode 100644 index fdf3f32c..00000000 --- a/packages/web/src/app/[domain]/connections/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { ConnectionList } from "./components/connectionList"; -import { Header } from "../components/header"; -import { getConnections, getOrgMembership } from "@/actions"; -import { isServiceError } from "@/lib/utils"; -import { notFound, ServiceErrorException } from "@/lib/serviceError"; -import { OrgRole } from "@sourcebot/db"; - -export default async function ConnectionsPage(props: { params: Promise<{ domain: string }> }) { - const params = await props.params; - - const { - domain - } = params; - - const connections = await getConnections(domain); - if (isServiceError(connections)) { - throw new ServiceErrorException(connections); - } - - const membership = await getOrgMembership(domain); - if (isServiceError(membership)) { - throw new ServiceErrorException(notFound()); - } - - return ( -
-
-

Connections

-
- -
- ); -} diff --git a/packages/web/src/app/[domain]/connections/quickActions.tsx b/packages/web/src/app/[domain]/connections/quickActions.tsx deleted file mode 100644 index 5f8008b0..00000000 --- a/packages/web/src/app/[domain]/connections/quickActions.tsx +++ /dev/null @@ -1,575 +0,0 @@ -import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type" -import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"; -import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type"; -import { QuickAction } from "../components/configEditor"; -import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type"; -import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type"; -import { CodeSnippet } from "@/app/components/codeSnippet"; - -export const githubQuickActions: QuickAction[] = [ - { - fn: (previous: GithubConnectionConfig) => ({ - ...previous, - repos: [ - ...(previous.repos ?? []), - "/" - ] - }), - name: "Add a single repo", - selectionText: "/", - description: ( -
- Add a individual repository to sync with. Ensure the repository is visible to the provided token (if any). - Examples: -
- {[ - "sourcebot/sourcebot", - "vercel/next.js", - "torvalds/linux" - ].map((repo) => ( - {repo} - ))} -
-
- ) - }, - { - fn: (previous: GithubConnectionConfig) => ({ - ...previous, - orgs: [ - ...(previous.orgs ?? []), - "" - ] - }), - name: "Add an organization", - selectionText: "", - description: ( -
- Add an organization to sync with. All repositories in the organization visible to the provided token (if any) will be synced. - Examples: -
- {[ - "commaai", - "sourcebot", - "vercel" - ].map((org) => ( - {org} - ))} -
-
- ) - }, - { - fn: (previous: GithubConnectionConfig) => ({ - ...previous, - users: [ - ...(previous.users ?? []), - "" - ] - }), - name: "Add a user", - selectionText: "", - description: ( -
- Add a user to sync with. All repositories that the user owns visible to the provided token (if any) will be synced. - Examples: -
- {[ - "jane-doe", - "torvalds", - "octocat" - ].map((org) => ( - {org} - ))} -
-
- ) - }, - { - fn: (previous: GithubConnectionConfig) => ({ - ...previous, - url: previous.url ?? "https://github.example.com", - }), - name: "Set url to GitHub instance", - selectionText: "https://github.example.com", - description: Set a custom GitHub host. Defaults to https://github.com. - }, - { - fn: (previous: GithubConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - repos: [ - ...(previous.exclude?.repos ?? []), - "" - ] - } - }), - name: "Exclude by repo name", - selectionText: "", - description: ( -
- Exclude repositories from syncing by name. Glob patterns are supported. - Examples: -
- {[ - "my-org/docs*", - "my-org/test*" - ].map((repo) => ( - {repo} - ))} -
-
- ) - }, - { - fn: (previous: GithubConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - topics: [ - ...(previous.exclude?.topics ?? []), - "" - ] - } - }), - name: "Exclude by topic", - selectionText: "", - description: ( -
- Exclude topics from syncing. Only repos that do not match any of the provided topics will be synced. Glob patterns are supported. - Examples: -
- {[ - "docs", - "ci" - ].map((repo) => ( - {repo} - ))} -
-
- ) - }, - { - fn: (previous: GithubConnectionConfig) => ({ - ...previous, - topics: [ - ...(previous.topics ?? []), - "" - ] - }), - name: "Include by topic", - selectionText: "", - description: ( -
- Include repositories by topic. Only repos that match at least one of the provided topics will be synced. Glob patterns are supported. - Examples: -
- {[ - "docs", - "ci" - ].map((repo) => ( - {repo} - ))} -
-
- ) - }, - { - fn: (previous: GithubConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - archived: true, - } - }), - name: "Exclude archived repos", - description: Exclude archived repositories from syncing. - }, - { - fn: (previous: GithubConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - forks: true, - } - }), - name: "Exclude forked repos", - description: Exclude forked repositories from syncing. - } -]; - -export const gitlabQuickActions: QuickAction[] = [ - { - fn: (previous: GitlabConnectionConfig) => ({ - ...previous, - projects: [ - ...previous.projects ?? [], - "" - ] - }), - name: "Add a project", - selectionText: "", - description: ( -
- Add a individual project to sync with. Ensure the project is visible to the provided token (if any). - Examples: -
- {[ - "gitlab-org/gitlab", - "corp/team-project", - ].map((repo) => ( - {repo} - ))} -
-
- ) - }, - { - fn: (previous: GitlabConnectionConfig) => ({ - ...previous, - users: [ - ...previous.users ?? [], - "" - ] - }), - name: "Add a user", - selectionText: "", - description: ( -
- Add a user to sync with. All projects that the user owns visible to the provided token (if any) will be synced. - Examples: -
- {[ - "jane-doe", - "torvalds" - ].map((org) => ( - {org} - ))} -
-
- ) - }, - { - fn: (previous: GitlabConnectionConfig) => ({ - ...previous, - groups: [ - ...previous.groups ?? [], - "" - ] - }), - name: "Add a group", - selectionText: "", - description: ( -
- Add a group to sync with. All projects in the group (and recursive subgroups) visible to the provided token (if any) will be synced. - Examples: -
- {[ - "my-group", - "path/to/subgroup" - ].map((org) => ( - {org} - ))} -
-
- ) - }, - { - fn: (previous: GitlabConnectionConfig) => ({ - ...previous, - url: previous.url ?? "https://gitlab.example.com", - }), - name: "Set url to GitLab instance", - selectionText: "https://gitlab.example.com", - description: Set a custom GitLab host. Defaults to https://gitlab.com. - }, - { - fn: (previous: GitlabConnectionConfig) => ({ - ...previous, - all: true, - }), - name: "Sync all projects", - description: Sync all projects visible to the provided token (if any). Only available when using a self-hosted GitLab instance. - }, - { - fn: (previous: GitlabConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - projects: [ - ...(previous.exclude?.projects ?? []), - "" - ] - } - }), - name: "Exclude a project", - selectionText: "", - description: ( -
- List of projects to exclude from syncing. Glob patterns are supported. - Examples: -
- {[ - "docs/**", - "**/tests/**", - ].map((repo) => ( - {repo} - ))} -
-
- ) - } -] - -export const giteaQuickActions: QuickAction[] = [ - { - fn: (previous: GiteaConnectionConfig) => ({ - ...previous, - orgs: [ - ...(previous.orgs ?? []), - "" - ] - }), - name: "Add an organization", - selectionText: "", - }, - { - fn: (previous: GiteaConnectionConfig) => ({ - ...previous, - repos: [ - ...(previous.repos ?? []), - "/" - ] - }), - name: "Add a repo", - selectionText: "/", - }, - { - fn: (previous: GiteaConnectionConfig) => ({ - ...previous, - url: previous.url ?? "https://gitea.example.com", - }), - name: "Set url to Gitea instance", - selectionText: "https://gitea.example.com", - } -] - -export const gerritQuickActions: QuickAction[] = [ - { - fn: (previous: GerritConnectionConfig) => ({ - ...previous, - projects: [ - ...(previous.projects ?? []), - "" - ] - }), - name: "Add a project", - }, - { - fn: (previous: GerritConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - projects: [ - ...(previous.exclude?.projects ?? []), - "" - ] - } - }), - name: "Exclude a project", - } -] - -export const bitbucketCloudQuickActions: QuickAction[] = [ - { - // add user - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - user: previous.user ?? "username" - }), - name: "Add username", - selectionText: "username", - description: ( -
- Username to use for authentication. This is only required if you're using an App Password (stored in token) for authentication. -
- ) - }, - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - workspaces: [ - ...(previous.workspaces ?? []), - "myWorkspace" - ] - }), - name: "Add a workspace", - selectionText: "myWorkspace", - description: ( -
- Add a workspace to sync with. Ensure the workspace is visible to the provided token (if any). -
- ) - }, - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - repos: [ - ...(previous.repos ?? []), - "myWorkspace/myRepo" - ] - }), - name: "Add a repo", - selectionText: "myWorkspace/myRepo", - description: ( -
- Add an individual repository to sync with. Ensure the repository is visible to the provided token (if any). -
- ) - }, - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - projects: [ - ...(previous.projects ?? []), - "myProject" - ] - }), - name: "Add a project", - selectionText: "myProject", - description: ( -
- Add a project to sync with. Ensure the project is visible to the provided token (if any). -
- ) - }, - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - repos: [...(previous.exclude?.repos ?? []), "myWorkspace/myExcludedRepo"] - } - }), - name: "Exclude a repo", - selectionText: "myWorkspace/myExcludedRepo", - description: ( -
- Exclude a repository from syncing. Glob patterns are supported. -
- ) - }, - // exclude forked - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - forks: true - } - }), - name: "Exclude forked repos", - description: Exclude forked repositories from syncing. - } -] - -export const bitbucketDataCenterQuickActions: QuickAction[] = [ - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - url: previous.url ?? "https://bitbucket.example.com", - }), - name: "Set url to Bitbucket DC instance", - selectionText: "https://bitbucket.example.com", - }, - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - repos: [ - ...(previous.repos ?? []), - "myProject/myRepo" - ] - }), - name: "Add a repo", - selectionText: "myProject/myRepo", - description: ( -
- Add a individual repository to sync with. Ensure the repository is visible to the provided token (if any). - Examples: -
- {[ - "PROJ/repo-name", - "MYPROJ/api" - ].map((repo) => ( - {repo} - ))} -
-
- ) - }, - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - projects: [ - ...(previous.projects ?? []), - "myProject" - ] - }), - name: "Add a project", - selectionText: "myProject", - description: ( -
- Add a project to sync with. Ensure the project is visible to the provided token (if any). -
- ) - }, - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - repos: [...(previous.exclude?.repos ?? []), "myProject/myExcludedRepo"] - } - }), - name: "Exclude a repo", - selectionText: "myProject/myExcludedRepo", - description: ( -
- Exclude a repository from syncing. Glob patterns are supported. - Examples: -
- {[ - "myProject/myExcludedRepo", - "myProject2/*" - ].map((repo) => ( - {repo} - ))} -
-
- ) - }, - // exclude archived - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - archived: true - } - }), - name: "Exclude archived repos", - }, - // exclude forked - { - fn: (previous: BitbucketConnectionConfig) => ({ - ...previous, - exclude: { - ...previous.exclude, - forks: true - } - }), - name: "Exclude forked repos", - } -] - diff --git a/packages/web/src/app/[domain]/connections/utils.ts b/packages/web/src/app/[domain]/connections/utils.ts deleted file mode 100644 index cd728092..00000000 --- a/packages/web/src/app/[domain]/connections/utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Ajv, { Schema } from "ajv"; -import { z } from "zod"; - -export const createZodConnectionConfigValidator = (jsonSchema: Schema, additionalConfigValidation?: (config: T) => { message: string, isValid: boolean }) => { - const ajv = new Ajv({ - validateFormats: false, - }); - const validate = ajv.compile(jsonSchema); - - return z - .string() - .superRefine((data, ctx) => { - const addIssue = (message: string) => { - return ctx.addIssue({ - code: "custom", - message: `Schema validation error: ${message}` - }); - } - - let parsed; - try { - parsed = JSON.parse(data); - } catch { - addIssue("Invalid JSON"); - return; - } - - const valid = validate(parsed); - if (!valid) { - addIssue(ajv.errorsText(validate.errors)); - } - - if (additionalConfigValidation) { - const result = additionalConfigValidation(parsed as T); - if (!result.isValid) { - addIssue(result.message); - } - } - }); -} \ No newline at end of file