mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 20:35:24 +00:00
Icon & link support for self-hosted repositories (#93)
This commit is contained in:
parent
01f4329d3e
commit
d6544086e7
12 changed files with 154 additions and 99 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added file suggestions as a suggestion type. ([#88](https://github.com/sourcebot-dev/sourcebot/pull/88))
|
- Added file suggestions as a suggestion type. ([#88](https://github.com/sourcebot-dev/sourcebot/pull/88))
|
||||||
|
- Added icon and link support for self-hosted repositories. ([#93](https://github.com/sourcebot-dev/sourcebot/pull/93))
|
||||||
|
|
||||||
## [2.5.0] - 2024-11-22
|
## [2.5.0] - 2024-11-22
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,8 @@ interface RepositoryBadgeProps {
|
||||||
const RepositoryBadge = ({
|
const RepositoryBadge = ({
|
||||||
repo
|
repo
|
||||||
}: RepositoryBadgeProps) => {
|
}: RepositoryBadgeProps) => {
|
||||||
const { repoIcon, repoName, repoLink } = (() => {
|
const { repoIcon, displayName, repoLink } = (() => {
|
||||||
const info = getRepoCodeHostInfo(repo.Name);
|
const info = getRepoCodeHostInfo(repo);
|
||||||
|
|
||||||
if (info) {
|
if (info) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -66,14 +66,14 @@ const RepositoryBadge = ({
|
||||||
alt={info.costHostName}
|
alt={info.costHostName}
|
||||||
className={`w-4 h-4 ${info.iconClassName}`}
|
className={`w-4 h-4 ${info.iconClassName}`}
|
||||||
/>,
|
/>,
|
||||||
repoName: info.repoName,
|
displayName: info.displayName,
|
||||||
repoLink: info.repoLink,
|
repoLink: info.repoLink,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
repoIcon: <FileIcon className="w-4 h-4" />,
|
repoIcon: <FileIcon className="w-4 h-4" />,
|
||||||
repoName: repo.Name,
|
displayName: repo.Name,
|
||||||
repoLink: undefined,
|
repoLink: undefined,
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
@ -91,7 +91,7 @@ const RepositoryBadge = ({
|
||||||
>
|
>
|
||||||
{repoIcon}
|
{repoIcon}
|
||||||
<span className="text-sm font-mono">
|
<span className="text-sm font-mono">
|
||||||
{repoName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getRepoCodeHostInfo } from "@/lib/utils";
|
|
||||||
import { Column, ColumnDef } from "@tanstack/react-table"
|
import { Column, ColumnDef } from "@tanstack/react-table"
|
||||||
import { ArrowUpDown } from "lucide-react"
|
import { ArrowUpDown } from "lucide-react"
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
@ -19,6 +18,7 @@ export type RepositoryColumnInfo = {
|
||||||
lastIndexed: string;
|
lastIndexed: string;
|
||||||
latestCommit: string;
|
latestCommit: string;
|
||||||
commitUrlTemplate: string;
|
commitUrlTemplate: string;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const columns: ColumnDef<RepositoryColumnInfo>[] = [
|
export const columns: ColumnDef<RepositoryColumnInfo>[] = [
|
||||||
|
|
@ -27,14 +27,16 @@ export const columns: ColumnDef<RepositoryColumnInfo>[] = [
|
||||||
header: "Name",
|
header: "Name",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const repo = row.original;
|
const repo = row.original;
|
||||||
const info = getRepoCodeHostInfo(repo.name);
|
const url = repo.url;
|
||||||
|
// local repositories will have a url of 0 length
|
||||||
|
const isRemoteRepo = url.length === 0;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={info?.repoLink ? "cursor-pointer text-blue-500 hover:underline": ""}
|
className={!isRemoteRepo ? "cursor-pointer text-blue-500 hover:underline": ""}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (info?.repoLink) {
|
if (!isRemoteRepo) {
|
||||||
window.open(info.repoLink, "_blank");
|
window.open(url, "_blank");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export const RepositoryTable = async () => {
|
||||||
latestCommit: repo.Repository.LatestCommitDate,
|
latestCommit: repo.Repository.LatestCommitDate,
|
||||||
indexedFiles: repo.Stats.Documents,
|
indexedFiles: repo.Stats.Documents,
|
||||||
commitUrlTemplate: repo.Repository.CommitURLTemplate,
|
commitUrlTemplate: repo.Repository.CommitURLTemplate,
|
||||||
|
url: repo.Repository.URL,
|
||||||
}
|
}
|
||||||
}).sort((a, b) => {
|
}).sort((a, b) => {
|
||||||
return new Date(b.lastIndexed).getTime() - new Date(a.lastIndexed).getTime();
|
return new Date(b.lastIndexed).getTime() - new Date(a.lastIndexed).getTime();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { fetchFileSource } from "@/app/api/(client)/client";
|
import { fetchFileSource } from "@/app/api/(client)/client";
|
||||||
import { getCodeHostFilePreviewLink, base64Decode } from "@/lib/utils";
|
import { base64Decode } from "@/lib/utils";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { CodePreview, CodePreviewFile } from "./codePreview";
|
import { CodePreview, CodePreviewFile } from "./codePreview";
|
||||||
import { SearchResultFile } from "@/lib/types";
|
import { SearchResultFile } from "@/lib/types";
|
||||||
|
|
@ -11,6 +11,7 @@ interface CodePreviewPanelProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
selectedMatchIndex: number;
|
selectedMatchIndex: number;
|
||||||
onSelectedMatchIndexChange: (index: number) => void;
|
onSelectedMatchIndexChange: (index: number) => void;
|
||||||
|
repoUrlTemplates: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodePreviewPanel = ({
|
export const CodePreviewPanel = ({
|
||||||
|
|
@ -18,6 +19,7 @@ export const CodePreviewPanel = ({
|
||||||
onClose,
|
onClose,
|
||||||
selectedMatchIndex,
|
selectedMatchIndex,
|
||||||
onSelectedMatchIndexChange,
|
onSelectedMatchIndexChange,
|
||||||
|
repoUrlTemplates,
|
||||||
}: CodePreviewPanelProps) => {
|
}: CodePreviewPanelProps) => {
|
||||||
|
|
||||||
const { data: file } = useQuery({
|
const { data: file } = useQuery({
|
||||||
|
|
@ -37,8 +39,15 @@ export const CodePreviewPanel = ({
|
||||||
branch,
|
branch,
|
||||||
})
|
})
|
||||||
.then(({ source }) => {
|
.then(({ source }) => {
|
||||||
// @todo : refector this to use the templates provided by zoekt.
|
const link = (() => {
|
||||||
const link = getCodeHostFilePreviewLink(fileMatch.Repository, fileMatch.FileName, branch);
|
const template = repoUrlTemplates[fileMatch.Repository];
|
||||||
|
if (!template) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
.replace("{{.Version}}", branch ?? "HEAD")
|
||||||
|
.replace("{{.Path}}", fileMatch.FileName);
|
||||||
|
})();
|
||||||
|
|
||||||
const decodedSource = base64Decode(source);
|
const decodedSource = base64Decode(source);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,13 @@
|
||||||
|
|
||||||
import { QuestionMarkCircledIcon } from "@radix-ui/react-icons";
|
import { QuestionMarkCircledIcon } from "@radix-ui/react-icons";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export type Entry = {
|
export type Entry = {
|
||||||
key: string;
|
key: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
count: number;
|
count: number;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
icon?: string;
|
Icon?: React.ReactNode;
|
||||||
iconAltText?: string;
|
|
||||||
iconClassName?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntryProps {
|
interface EntryProps {
|
||||||
|
|
@ -22,11 +19,9 @@ interface EntryProps {
|
||||||
export const Entry = ({
|
export const Entry = ({
|
||||||
entry: {
|
entry: {
|
||||||
isSelected,
|
isSelected,
|
||||||
icon,
|
|
||||||
iconAltText,
|
|
||||||
iconClassName,
|
|
||||||
displayName,
|
displayName,
|
||||||
count,
|
count,
|
||||||
|
Icon,
|
||||||
},
|
},
|
||||||
onClicked,
|
onClicked,
|
||||||
}: EntryProps) => {
|
}: EntryProps) => {
|
||||||
|
|
@ -42,13 +37,7 @@ export const Entry = ({
|
||||||
onClick={() => onClicked()}
|
onClick={() => onClicked()}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center gap-1">
|
<div className="flex flex-row items-center gap-1">
|
||||||
{icon ? (
|
{Icon ? Icon : (
|
||||||
<Image
|
|
||||||
src={icon}
|
|
||||||
alt={iconAltText ?? ''}
|
|
||||||
className={`w-4 h-4 flex-shrink-0 ${iconClassName}`}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<QuestionMarkCircledIcon className="w-4 h-4 flex-shrink-0" />
|
<QuestionMarkCircledIcon className="w-4 h-4 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
<p className="text-wrap">{displayName}</p>
|
<p className="text-wrap">{displayName}</p>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { SearchResultFile } from "@/lib/types";
|
import { Repository, SearchResultFile } from "@/lib/types";
|
||||||
import { getRepoCodeHostInfo } from "@/lib/utils";
|
import { cn, getRepoCodeHostInfo } from "@/lib/utils";
|
||||||
import { SetStateAction, useCallback, useEffect, useState } from "react";
|
import { SetStateAction, useCallback, useEffect, useState } from "react";
|
||||||
import { Entry } from "./entry";
|
import { Entry } from "./entry";
|
||||||
import { Filter } from "./filter";
|
import { Filter } from "./filter";
|
||||||
import { getLanguageIcon } from "./languageIcons";
|
import { getLanguageIcon } from "./languageIcons";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { LaptopIcon, QuestionMarkCircledIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
interface FilePanelProps {
|
interface FilePanelProps {
|
||||||
matches: SearchResultFile[];
|
matches: SearchResultFile[];
|
||||||
onFilterChanged: (filteredMatches: SearchResultFile[]) => void,
|
onFilterChanged: (filteredMatches: SearchResultFile[]) => void,
|
||||||
|
repoMetadata: Record<string, Repository>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterPanel = ({
|
export const FilterPanel = ({
|
||||||
matches,
|
matches,
|
||||||
onFilterChanged,
|
onFilterChanged,
|
||||||
|
repoMetadata,
|
||||||
}: FilePanelProps) => {
|
}: FilePanelProps) => {
|
||||||
const [repos, setRepos] = useState<Record<string, Entry>>({});
|
const [repos, setRepos] = useState<Record<string, Entry>>({});
|
||||||
const [languages, setLanguages] = useState<Record<string, Entry>>({});
|
const [languages, setLanguages] = useState<Record<string, Entry>>({});
|
||||||
|
|
@ -24,15 +28,24 @@ export const FilterPanel = ({
|
||||||
"Repository",
|
"Repository",
|
||||||
matches,
|
matches,
|
||||||
(key) => {
|
(key) => {
|
||||||
const info = getRepoCodeHostInfo(key);
|
const repo: Repository | undefined = repoMetadata[key];
|
||||||
|
const info = getRepoCodeHostInfo(repo);
|
||||||
|
const Icon = info ? (
|
||||||
|
<Image
|
||||||
|
src={info.icon}
|
||||||
|
alt={info.costHostName}
|
||||||
|
className={cn('w-4 h-4 flex-shrink-0', info.iconClassName)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LaptopIcon className="w-4 h-4 flex-shrink-0" />
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
displayName: info?.repoName ?? key,
|
displayName: info?.displayName ?? key,
|
||||||
count: 0,
|
count: 0,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
icon: info?.icon,
|
Icon,
|
||||||
iconAltText: info?.costHostName,
|
|
||||||
iconClassName: info?.iconClassName,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -45,12 +58,23 @@ export const FilterPanel = ({
|
||||||
"Language",
|
"Language",
|
||||||
matches,
|
matches,
|
||||||
(key) => {
|
(key) => {
|
||||||
|
const iconSrc = getLanguageIcon(key);
|
||||||
|
const Icon = iconSrc ? (
|
||||||
|
<Image
|
||||||
|
src={iconSrc}
|
||||||
|
alt={key}
|
||||||
|
className="w-4 h-4 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<QuestionMarkCircledIcon className="w-4 h-4 flex-shrink-0" />
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
displayName: key,
|
displayName: key,
|
||||||
count: 0,
|
count: 0,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
icon: getLanguageIcon(key),
|
Icon: Icon,
|
||||||
} satisfies Entry;
|
} satisfies Entry;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
import { getRepoCodeHostInfo } from "@/lib/utils";
|
import { getRepoCodeHostInfo } from "@/lib/utils";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { DoubleArrowDownIcon, DoubleArrowUpIcon, FileIcon } from "@radix-ui/react-icons";
|
import { DoubleArrowDownIcon, DoubleArrowUpIcon, LaptopIcon } from "@radix-ui/react-icons";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { SearchResultFile } from "@/lib/types";
|
import { Repository, SearchResultFile } from "@/lib/types";
|
||||||
import { FileMatch } from "./fileMatch";
|
import { FileMatch } from "./fileMatch";
|
||||||
|
|
||||||
export const MAX_MATCHES_TO_PREVIEW = 3;
|
export const MAX_MATCHES_TO_PREVIEW = 3;
|
||||||
|
|
@ -18,6 +18,7 @@ interface FileMatchContainerProps {
|
||||||
showAllMatches: boolean;
|
showAllMatches: boolean;
|
||||||
onShowAllMatchesButtonClicked: () => void;
|
onShowAllMatchesButtonClicked: () => void;
|
||||||
isBranchFilteringEnabled: boolean;
|
isBranchFilteringEnabled: boolean;
|
||||||
|
repoMetadata: Record<string, Repository>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileMatchContainer = ({
|
export const FileMatchContainer = ({
|
||||||
|
|
@ -27,6 +28,7 @@ export const FileMatchContainer = ({
|
||||||
showAllMatches,
|
showAllMatches,
|
||||||
onShowAllMatchesButtonClicked,
|
onShowAllMatchesButtonClicked,
|
||||||
isBranchFilteringEnabled,
|
isBranchFilteringEnabled,
|
||||||
|
repoMetadata,
|
||||||
}: FileMatchContainerProps) => {
|
}: FileMatchContainerProps) => {
|
||||||
|
|
||||||
const matchCount = useMemo(() => {
|
const matchCount = useMemo(() => {
|
||||||
|
|
@ -59,11 +61,13 @@ export const FileMatchContainer = ({
|
||||||
return null;
|
return null;
|
||||||
}, [matches]);
|
}, [matches]);
|
||||||
|
|
||||||
const { repoIcon, repoName, repoLink } = useMemo(() => {
|
const { repoIcon, displayName, repoLink } = useMemo(() => {
|
||||||
const info = getRepoCodeHostInfo(file.Repository);
|
const repo: Repository | undefined = repoMetadata[file.Repository];
|
||||||
|
const info = getRepoCodeHostInfo(repo);
|
||||||
|
|
||||||
if (info) {
|
if (info) {
|
||||||
return {
|
return {
|
||||||
repoName: info.repoName,
|
displayName: info.displayName,
|
||||||
repoLink: info.repoLink,
|
repoLink: info.repoLink,
|
||||||
repoIcon: <Image
|
repoIcon: <Image
|
||||||
src={info.icon}
|
src={info.icon}
|
||||||
|
|
@ -74,11 +78,11 @@ export const FileMatchContainer = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
repoName: file.Repository,
|
displayName: file.Repository,
|
||||||
repoLink: undefined,
|
repoLink: undefined,
|
||||||
repoIcon: <FileIcon className="w-4 h-4" />
|
repoIcon: <LaptopIcon className="w-4 h-4" />
|
||||||
}
|
}
|
||||||
}, [file]);
|
}, [file.Repository, repoMetadata]);
|
||||||
|
|
||||||
const isMoreContentButtonVisible = useMemo(() => {
|
const isMoreContentButtonVisible = useMemo(() => {
|
||||||
return matchCount > MAX_MATCHES_TO_PREVIEW;
|
return matchCount > MAX_MATCHES_TO_PREVIEW;
|
||||||
|
|
@ -122,7 +126,7 @@ export const FileMatchContainer = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{repoName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
{isBranchFilteringEnabled && branches.length > 0 && (
|
{isBranchFilteringEnabled && branches.length > 0 && (
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { SearchResultFile } from "@/lib/types";
|
import { Repository, SearchResultFile } from "@/lib/types";
|
||||||
import { FileMatchContainer, MAX_MATCHES_TO_PREVIEW } from "./fileMatchContainer";
|
import { FileMatchContainer, MAX_MATCHES_TO_PREVIEW } from "./fileMatchContainer";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
|
|
@ -12,6 +12,7 @@ interface SearchResultsPanelProps {
|
||||||
isLoadMoreButtonVisible: boolean;
|
isLoadMoreButtonVisible: boolean;
|
||||||
onLoadMoreButtonClicked: () => void;
|
onLoadMoreButtonClicked: () => void;
|
||||||
isBranchFilteringEnabled: boolean;
|
isBranchFilteringEnabled: boolean;
|
||||||
|
repoMetadata: Record<string, Repository>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ESTIMATED_LINE_HEIGHT_PX = 20;
|
const ESTIMATED_LINE_HEIGHT_PX = 20;
|
||||||
|
|
@ -25,6 +26,7 @@ export const SearchResultsPanel = ({
|
||||||
isLoadMoreButtonVisible,
|
isLoadMoreButtonVisible,
|
||||||
onLoadMoreButtonClicked,
|
onLoadMoreButtonClicked,
|
||||||
isBranchFilteringEnabled,
|
isBranchFilteringEnabled,
|
||||||
|
repoMetadata,
|
||||||
}: SearchResultsPanelProps) => {
|
}: SearchResultsPanelProps) => {
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const [showAllMatchesStates, setShowAllMatchesStates] = useState(Array(fileMatches.length).fill(false));
|
const [showAllMatchesStates, setShowAllMatchesStates] = useState(Array(fileMatches.length).fill(false));
|
||||||
|
|
@ -148,6 +150,7 @@ export const SearchResultsPanel = ({
|
||||||
onShowAllMatchesButtonClicked(virtualRow.index);
|
onShowAllMatchesButtonClicked(virtualRow.index);
|
||||||
}}
|
}}
|
||||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||||
|
repoMetadata={repoMetadata}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||||
import { SearchQueryParams, SearchResultFile } from "@/lib/types";
|
import { Repository, SearchQueryParams, SearchResultFile } from "@/lib/types";
|
||||||
import { createPathWithQueryParams } from "@/lib/utils";
|
import { createPathWithQueryParams } from "@/lib/utils";
|
||||||
import { SymbolIcon } from "@radix-ui/react-icons";
|
import { SymbolIcon } from "@radix-ui/react-icons";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
@ -17,7 +17,7 @@ import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import logoDark from "../../../public/sb_logo_dark.png";
|
import logoDark from "../../../public/sb_logo_dark.png";
|
||||||
import logoLight from "../../../public/sb_logo_light.png";
|
import logoLight from "../../../public/sb_logo_light.png";
|
||||||
import { search } from "../api/(client)/client";
|
import { getRepos, search } from "../api/(client)/client";
|
||||||
import { SearchBar } from "../components/searchBar";
|
import { SearchBar } from "../components/searchBar";
|
||||||
import { SettingsDropdown } from "../components/settingsDropdown";
|
import { SettingsDropdown } from "../components/settingsDropdown";
|
||||||
import { CodePreviewPanel } from "./components/codePreviewPanel";
|
import { CodePreviewPanel } from "./components/codePreviewPanel";
|
||||||
|
|
@ -45,6 +45,26 @@ export default function SearchPage() {
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use the /api/repos endpoint to get a useful list of
|
||||||
|
// repository metadata (like host type, repo name, etc.)
|
||||||
|
// Convert this into a map of repo name to repo metadata
|
||||||
|
// for easy lookup.
|
||||||
|
const { data: repoMetadata } = useQuery({
|
||||||
|
queryKey: ["repos"],
|
||||||
|
queryFn: () => getRepos(),
|
||||||
|
select: (data): Record<string, Repository> =>
|
||||||
|
data.List.Repos
|
||||||
|
.map(r => r.Repository)
|
||||||
|
.reduce(
|
||||||
|
(acc, repo) => ({
|
||||||
|
...acc,
|
||||||
|
[repo.Name]: repo,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchResponse) {
|
if (!searchResponse) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -77,13 +97,14 @@ export default function SearchPage() {
|
||||||
});
|
});
|
||||||
}, [captureEvent, searchResponse]);
|
}, [captureEvent, searchResponse]);
|
||||||
|
|
||||||
const { fileMatches, searchDurationMs, totalMatchCount, isBranchFilteringEnabled } = useMemo(() => {
|
const { fileMatches, searchDurationMs, totalMatchCount, isBranchFilteringEnabled, repoUrlTemplates } = useMemo(() => {
|
||||||
if (!searchResponse) {
|
if (!searchResponse) {
|
||||||
return {
|
return {
|
||||||
fileMatches: [],
|
fileMatches: [],
|
||||||
searchDurationMs: 0,
|
searchDurationMs: 0,
|
||||||
totalMatchCount: 0,
|
totalMatchCount: 0,
|
||||||
isBranchFilteringEnabled: false,
|
isBranchFilteringEnabled: false,
|
||||||
|
repoUrlTemplates: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +129,7 @@ export default function SearchPage() {
|
||||||
searchDurationMs: Math.round(searchResponse.Result.Duration / 1000000),
|
searchDurationMs: Math.round(searchResponse.Result.Duration / 1000000),
|
||||||
totalMatchCount: searchResponse.Result.MatchCount,
|
totalMatchCount: searchResponse.Result.MatchCount,
|
||||||
isBranchFilteringEnabled,
|
isBranchFilteringEnabled,
|
||||||
|
repoUrlTemplates: searchResponse.Result.RepoURLs,
|
||||||
}
|
}
|
||||||
}, [searchResponse]);
|
}, [searchResponse]);
|
||||||
|
|
||||||
|
|
@ -202,6 +224,8 @@ export default function SearchPage() {
|
||||||
isMoreResultsButtonVisible={isMoreResultsButtonVisible}
|
isMoreResultsButtonVisible={isMoreResultsButtonVisible}
|
||||||
onLoadMoreResults={onLoadMoreResults}
|
onLoadMoreResults={onLoadMoreResults}
|
||||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||||
|
repoUrlTemplates={repoUrlTemplates}
|
||||||
|
repoMetadata={repoMetadata ?? {}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -213,6 +237,8 @@ interface PanelGroupProps {
|
||||||
isMoreResultsButtonVisible?: boolean;
|
isMoreResultsButtonVisible?: boolean;
|
||||||
onLoadMoreResults: () => void;
|
onLoadMoreResults: () => void;
|
||||||
isBranchFilteringEnabled: boolean;
|
isBranchFilteringEnabled: boolean;
|
||||||
|
repoUrlTemplates: Record<string, string>;
|
||||||
|
repoMetadata: Record<string, Repository>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PanelGroup = ({
|
const PanelGroup = ({
|
||||||
|
|
@ -220,6 +246,8 @@ const PanelGroup = ({
|
||||||
isMoreResultsButtonVisible,
|
isMoreResultsButtonVisible,
|
||||||
onLoadMoreResults,
|
onLoadMoreResults,
|
||||||
isBranchFilteringEnabled,
|
isBranchFilteringEnabled,
|
||||||
|
repoUrlTemplates,
|
||||||
|
repoMetadata,
|
||||||
}: PanelGroupProps) => {
|
}: PanelGroupProps) => {
|
||||||
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
|
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
|
||||||
const [selectedFile, setSelectedFile] = useState<SearchResultFile | undefined>(undefined);
|
const [selectedFile, setSelectedFile] = useState<SearchResultFile | undefined>(undefined);
|
||||||
|
|
@ -254,6 +282,7 @@ const PanelGroup = ({
|
||||||
<FilterPanel
|
<FilterPanel
|
||||||
matches={fileMatches}
|
matches={fileMatches}
|
||||||
onFilterChanged={onFilterChanged}
|
onFilterChanged={onFilterChanged}
|
||||||
|
repoMetadata={repoMetadata}
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle
|
<ResizableHandle
|
||||||
|
|
@ -278,6 +307,7 @@ const PanelGroup = ({
|
||||||
isLoadMoreButtonVisible={!!isMoreResultsButtonVisible}
|
isLoadMoreButtonVisible={!!isMoreResultsButtonVisible}
|
||||||
onLoadMoreButtonClicked={onLoadMoreResults}
|
onLoadMoreButtonClicked={onLoadMoreResults}
|
||||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||||
|
repoMetadata={repoMetadata}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
|
@ -302,6 +332,7 @@ const PanelGroup = ({
|
||||||
onClose={() => setSelectedFile(undefined)}
|
onClose={() => setSelectedFile(undefined)}
|
||||||
selectedMatchIndex={selectedMatchIndex}
|
selectedMatchIndex={selectedMatchIndex}
|
||||||
onSelectedMatchIndexChange={setSelectedMatchIndex}
|
onSelectedMatchIndexChange={setSelectedMatchIndex}
|
||||||
|
repoUrlTemplates={repoUrlTemplates}
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ export const zoektSearchResponseSchema = z.object({
|
||||||
// Set if `whole` is true.
|
// Set if `whole` is true.
|
||||||
Content: z.string().optional(),
|
Content: z.string().optional(),
|
||||||
})).nullable(),
|
})).nullable(),
|
||||||
|
RepoURLs: z.record(z.string(), z.string()),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import githubLogo from "../../public/github.svg";
|
||||||
import gitlabLogo from "../../public/gitlab.svg";
|
import gitlabLogo from "../../public/gitlab.svg";
|
||||||
import giteaLogo from "../../public/gitea.svg";
|
import giteaLogo from "../../public/gitea.svg";
|
||||||
import { ServiceError } from "./serviceError";
|
import { ServiceError } from "./serviceError";
|
||||||
|
import { Repository } from "./types";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
|
|
@ -31,64 +32,53 @@ export const createPathWithQueryParams = (path: string, ...queryParams: [string,
|
||||||
|
|
||||||
type CodeHostInfo = {
|
type CodeHostInfo = {
|
||||||
type: "github" | "gitlab" | "gitea";
|
type: "github" | "gitlab" | "gitea";
|
||||||
repoName: string;
|
displayName: string;
|
||||||
costHostName: string;
|
costHostName: string;
|
||||||
repoLink: string;
|
repoLink: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
iconClassName?: string;
|
iconClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRepoCodeHostInfo = (repoName: string): CodeHostInfo | undefined => {
|
export const getRepoCodeHostInfo = (repo?: Repository): CodeHostInfo | undefined => {
|
||||||
if (repoName.startsWith("github.com")) {
|
if (!repo) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostType = repo.RawConfig ? repo.RawConfig['web-url-type'] : undefined;
|
||||||
|
if (!hostType) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(repo.URL);
|
||||||
|
const displayName = url.pathname.slice(1);
|
||||||
|
|
||||||
|
switch (hostType) {
|
||||||
|
case 'github':
|
||||||
return {
|
return {
|
||||||
type: "github",
|
type: "github",
|
||||||
repoName: repoName.substring("github.com/".length),
|
displayName: displayName,
|
||||||
costHostName: "GitHub",
|
costHostName: "GitHub",
|
||||||
repoLink: `https://${repoName}`,
|
repoLink: repo.URL,
|
||||||
icon: githubLogo,
|
icon: githubLogo,
|
||||||
iconClassName: "dark:invert",
|
iconClassName: "dark:invert",
|
||||||
}
|
}
|
||||||
}
|
case 'gitlab':
|
||||||
|
|
||||||
if (repoName.startsWith("gitlab.com")) {
|
|
||||||
return {
|
return {
|
||||||
type: "gitlab",
|
type: "gitlab",
|
||||||
repoName: repoName.substring("gitlab.com/".length),
|
displayName: displayName,
|
||||||
costHostName: "GitLab",
|
costHostName: "GitLab",
|
||||||
repoLink: `https://${repoName}`,
|
repoLink: repo.URL,
|
||||||
icon: gitlabLogo,
|
icon: gitlabLogo,
|
||||||
}
|
}
|
||||||
}
|
case 'gitea':
|
||||||
|
|
||||||
if (repoName.startsWith("gitea.com")) {
|
|
||||||
return {
|
return {
|
||||||
type: "gitea",
|
type: "gitea",
|
||||||
repoName: repoName.substring("gitea.com/".length),
|
displayName: displayName,
|
||||||
costHostName: "Gitea",
|
costHostName: "Gitea",
|
||||||
repoLink: `https://${repoName}`,
|
repoLink: repo.URL,
|
||||||
icon: giteaLogo,
|
icon: giteaLogo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCodeHostFilePreviewLink = (repoName: string, filePath: string, branch: string = "HEAD"): string | undefined => {
|
|
||||||
const info = getRepoCodeHostInfo(repoName);
|
|
||||||
|
|
||||||
if (info?.type === "github") {
|
|
||||||
return `${info.repoLink}/blob/${branch}/${filePath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info?.type === "gitlab") {
|
|
||||||
return `${info.repoLink}/-/blob/${branch}/${filePath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info?.type === "gitea") {
|
|
||||||
return `${info.repoLink}/src/branch/${branch}/${filePath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isServiceError = (data: unknown): data is ServiceError => {
|
export const isServiceError = (data: unknown): data is ServiceError => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue