mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
chore(web): Remove deprecated connection creation/edit UI (#515)
* remove connections settings page * fix styling and remove additional components * add changelog
This commit is contained in:
parent
4a449da7d8
commit
db6e5d4841
15 changed files with 14 additions and 1239 deletions
|
|
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Fixed
|
||||
- Fixed "At least one project, user, or group must be specified" for GitLab configs with `all` in web configurator. [#512](https://github.com/sourcebot-dev/sourcebot/pull/512)
|
||||
- Fixed zoekt indexing failing with pipe in branch/tag names [#506](https://github.com/sourcebot-dev/sourcebot/pull/506)
|
||||
- Removed deprecated connection creation/edit UI [#515](https://github.com/sourcebot-dev/sourcebot/pull/515)
|
||||
|
||||
## [4.6.8] - 2025-09-15
|
||||
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import SharedConnectionCreationForm from "./sharedConnectionCreationForm";
|
||||
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
||||
import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema";
|
||||
import { bitbucketCloudQuickActions } from "../../connections/quickActions";
|
||||
|
||||
interface BitbucketCloudConnectionCreationFormProps {
|
||||
onCreated?: (id: number) => void;
|
||||
}
|
||||
|
||||
const additionalConfigValidation = (config: BitbucketConnectionConfig): { message: string, isValid: boolean } => {
|
||||
const hasProjects = config.projects && config.projects.length > 0 && config.projects.some(p => p.trim().length > 0);
|
||||
const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0);
|
||||
const hasWorkspaces = config.workspaces && config.workspaces.length > 0 && config.workspaces.some(w => w.trim().length > 0);
|
||||
|
||||
if (!hasProjects && !hasRepos && !hasWorkspaces) {
|
||||
return {
|
||||
message: "At least one project, repository, or workspace must be specified",
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Valid",
|
||||
isValid: true,
|
||||
}
|
||||
};
|
||||
|
||||
export const BitbucketCloudConnectionCreationForm = ({ onCreated }: BitbucketCloudConnectionCreationFormProps) => {
|
||||
const defaultConfig: BitbucketConnectionConfig = {
|
||||
type: 'bitbucket',
|
||||
deploymentType: 'cloud',
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedConnectionCreationForm<BitbucketConnectionConfig>
|
||||
type="bitbucket-cloud"
|
||||
title="Create a Bitbucket Cloud connection"
|
||||
defaultValues={{
|
||||
config: JSON.stringify(defaultConfig, null, 2),
|
||||
}}
|
||||
schema={bitbucketSchema}
|
||||
additionalConfigValidation={additionalConfigValidation}
|
||||
quickActions={bitbucketCloudQuickActions}
|
||||
onCreated={onCreated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import SharedConnectionCreationForm from "./sharedConnectionCreationForm";
|
||||
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
||||
import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema";
|
||||
import { bitbucketDataCenterQuickActions } from "../../connections/quickActions";
|
||||
|
||||
interface BitbucketDataCenterConnectionCreationFormProps {
|
||||
onCreated?: (id: number) => void;
|
||||
}
|
||||
|
||||
const additionalConfigValidation = (config: BitbucketConnectionConfig): { message: string, isValid: boolean } => {
|
||||
const hasProjects = config.projects && config.projects.length > 0 && config.projects.some(p => p.trim().length > 0);
|
||||
const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0);
|
||||
|
||||
if (!hasProjects && !hasRepos) {
|
||||
return {
|
||||
message: "At least one project or repository must be specified",
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Valid",
|
||||
isValid: true,
|
||||
}
|
||||
};
|
||||
|
||||
export const BitbucketDataCenterConnectionCreationForm = ({ onCreated }: BitbucketDataCenterConnectionCreationFormProps) => {
|
||||
const defaultConfig: BitbucketConnectionConfig = {
|
||||
type: 'bitbucket',
|
||||
deploymentType: 'server',
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedConnectionCreationForm<BitbucketConnectionConfig>
|
||||
type="bitbucket-server"
|
||||
title="Create a Bitbucket Data Center connection"
|
||||
defaultValues={{
|
||||
config: JSON.stringify(defaultConfig, null, 2),
|
||||
}}
|
||||
schema={bitbucketSchema}
|
||||
additionalConfigValidation={additionalConfigValidation}
|
||||
quickActions={bitbucketDataCenterQuickActions}
|
||||
onCreated={onCreated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type";
|
||||
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
|
||||
import { gerritQuickActions } from "../../connections/quickActions";
|
||||
import SharedConnectionCreationForm from "./sharedConnectionCreationForm";
|
||||
|
||||
interface GerritConnectionCreationFormProps {
|
||||
onCreated?: (id: number) => void;
|
||||
}
|
||||
|
||||
const additionalConfigValidation = (config: GerritConnectionConfig): { message: string, isValid: boolean } => {
|
||||
const hasProjects = config.projects && config.projects.length > 0;
|
||||
|
||||
if (!hasProjects) {
|
||||
return {
|
||||
message: "At least one project must be specified",
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Valid",
|
||||
isValid: true,
|
||||
}
|
||||
}
|
||||
|
||||
export const GerritConnectionCreationForm = ({ onCreated }: GerritConnectionCreationFormProps) => {
|
||||
const defaultConfig: GerritConnectionConfig = {
|
||||
type: 'gerrit',
|
||||
url: "https://gerrit.example.com"
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedConnectionCreationForm<GerritConnectionConfig>
|
||||
type="gerrit"
|
||||
title="Create a Gerrit connection"
|
||||
defaultValues={{
|
||||
config: JSON.stringify(defaultConfig, null, 2),
|
||||
}}
|
||||
schema={gerritSchema}
|
||||
quickActions={gerritQuickActions}
|
||||
additionalConfigValidation={additionalConfigValidation}
|
||||
onCreated={onCreated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
||||
import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
|
||||
import { giteaQuickActions } from "../../connections/quickActions";
|
||||
import SharedConnectionCreationForm from "./sharedConnectionCreationForm";
|
||||
|
||||
interface GiteaConnectionCreationFormProps {
|
||||
onCreated?: (id: number) => void;
|
||||
}
|
||||
|
||||
const additionalConfigValidation = (config: GiteaConnectionConfig): { message: string, isValid: boolean } => {
|
||||
const hasOrgs = config.orgs && config.orgs.length > 0 && config.orgs.some(o => o.trim().length > 0);
|
||||
const hasUsers = config.users && config.users.length > 0 && config.users.some(u => u.trim().length > 0);
|
||||
const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0);
|
||||
|
||||
if (!hasOrgs && !hasUsers && !hasRepos) {
|
||||
return {
|
||||
message: "At least one organization, user, or repository must be specified",
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Valid",
|
||||
isValid: true,
|
||||
}
|
||||
}
|
||||
|
||||
export const GiteaConnectionCreationForm = ({ onCreated }: GiteaConnectionCreationFormProps) => {
|
||||
const defaultConfig: GiteaConnectionConfig = {
|
||||
type: 'gitea',
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedConnectionCreationForm<GiteaConnectionConfig>
|
||||
type="gitea"
|
||||
title="Create a Gitea connection"
|
||||
defaultValues={{
|
||||
config: JSON.stringify(defaultConfig, null, 2),
|
||||
}}
|
||||
schema={giteaSchema}
|
||||
quickActions={giteaQuickActions}
|
||||
additionalConfigValidation={additionalConfigValidation}
|
||||
onCreated={onCreated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
|
||||
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
|
||||
import { githubQuickActions } from "../../connections/quickActions";
|
||||
import SharedConnectionCreationForm from "./sharedConnectionCreationForm";
|
||||
|
||||
interface GitHubConnectionCreationFormProps {
|
||||
onCreated?: (id: number) => void;
|
||||
}
|
||||
|
||||
const additionalConfigValidation = (config: GithubConnectionConfig): { message: string, isValid: boolean } => {
|
||||
const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0);
|
||||
const hasOrgs = config.orgs && config.orgs.length > 0 && config.orgs.some(o => o.trim().length > 0);
|
||||
const hasUsers = config.users && config.users.length > 0 && config.users.some(u => u.trim().length > 0);
|
||||
|
||||
if (!hasRepos && !hasOrgs && !hasUsers) {
|
||||
return {
|
||||
message: "At least one repository, organization, or user must be specified",
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Valid",
|
||||
isValid: true,
|
||||
}
|
||||
};
|
||||
|
||||
export const GitHubConnectionCreationForm = ({ onCreated }: GitHubConnectionCreationFormProps) => {
|
||||
const defaultConfig: GithubConnectionConfig = {
|
||||
type: 'github',
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedConnectionCreationForm<GithubConnectionConfig>
|
||||
type="github"
|
||||
title="Create a GitHub connection"
|
||||
defaultValues={{
|
||||
config: JSON.stringify(defaultConfig, null, 2),
|
||||
}}
|
||||
schema={githubSchema}
|
||||
additionalConfigValidation={additionalConfigValidation}
|
||||
quickActions={githubQuickActions}
|
||||
onCreated={onCreated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
|
||||
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
|
||||
import { gitlabQuickActions } from "../../connections/quickActions";
|
||||
import SharedConnectionCreationForm from "./sharedConnectionCreationForm";
|
||||
|
||||
interface GitLabConnectionCreationFormProps {
|
||||
onCreated?: (id: number) => void;
|
||||
}
|
||||
|
||||
const additionalConfigValidation = (config: GitlabConnectionConfig): { message: string, isValid: boolean } => {
|
||||
const hasProjects = config.projects && config.projects.length > 0 && config.projects.some(p => p.trim().length > 0);
|
||||
const hasUsers = config.users && config.users.length > 0 && config.users.some(u => u.trim().length > 0);
|
||||
const hasGroups = config.groups && config.groups.length > 0 && config.groups.some(g => g.trim().length > 0);
|
||||
const hasAll = config.all;
|
||||
|
||||
if (!hasProjects && !hasUsers && !hasGroups && !hasAll) {
|
||||
return {
|
||||
message: "At least one project, user, or group must be specified",
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Valid",
|
||||
isValid: true,
|
||||
}
|
||||
}
|
||||
|
||||
export const GitLabConnectionCreationForm = ({ onCreated }: GitLabConnectionCreationFormProps) => {
|
||||
const defaultConfig: GitlabConnectionConfig = {
|
||||
type: 'gitlab',
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedConnectionCreationForm<GitlabConnectionConfig>
|
||||
type="gitlab"
|
||||
title="Create a GitLab connection"
|
||||
defaultValues={{
|
||||
config: JSON.stringify(defaultConfig, null, 2),
|
||||
}}
|
||||
schema={gitlabSchema}
|
||||
quickActions={gitlabQuickActions}
|
||||
additionalConfigValidation={additionalConfigValidation}
|
||||
onCreated={onCreated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export { GitHubConnectionCreationForm } from "./githubConnectionCreationForm";
|
||||
export { GitLabConnectionCreationForm } from "./gitlabConnectionCreationForm";
|
||||
export { GiteaConnectionCreationForm } from "./giteaConnectionCreationForm";
|
||||
export { GerritConnectionCreationForm } from "./gerritConnectionCreationForm";
|
||||
export { BitbucketCloudConnectionCreationForm } from "./bitbucketCloudConnectionCreationForm";
|
||||
export { BitbucketDataCenterConnectionCreationForm } from "./bitbucketDataCenterConnectionCreationForm";
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { getSecrets } from "@/actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { cn, CodeHostType, isDefined, isServiceError, unwrapServiceError } from "@/lib/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Check, ChevronsUpDown, Loader2, PlusCircleIcon, TriangleAlert } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { ImportSecretDialog } from "../importSecretDialog";
|
||||
|
||||
interface SecretComboBoxProps {
|
||||
isDisabled: boolean;
|
||||
codeHostType: CodeHostType;
|
||||
secretKey?: string;
|
||||
onSecretChange: (secretKey: string) => void;
|
||||
}
|
||||
|
||||
export const SecretCombobox = ({
|
||||
isDisabled,
|
||||
codeHostType,
|
||||
secretKey,
|
||||
onSecretChange,
|
||||
}: SecretComboBoxProps) => {
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const domain = useDomain();
|
||||
const [isCreateSecretDialogOpen, setIsCreateSecretDialogOpen] = useState(false);
|
||||
const captureEvent = useCaptureEvent();
|
||||
|
||||
const { data: secrets, isPending, isError, refetch } = useQuery({
|
||||
queryKey: ["secrets", domain],
|
||||
queryFn: () => unwrapServiceError(getSecrets(domain)),
|
||||
});
|
||||
|
||||
const onSecretCreated = useCallback((key: string) => {
|
||||
onSecretChange(key);
|
||||
refetch();
|
||||
}, [onSecretChange, refetch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-[300px] overflow-hidden",
|
||||
!secretKey && "text-muted-foreground"
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{!(isPending || isError) && isDefined(secretKey) && !secrets.some(({ key }) => key === secretKey) && (
|
||||
<TooltipProvider>
|
||||
|
||||
<Tooltip
|
||||
delayDuration={100}
|
||||
>
|
||||
<TooltipTrigger
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<TriangleAlert className="h-4 w-4 text-yellow-700 dark:text-yellow-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>The secret you selected does not exist.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<span className="truncate">{isDefined(secretKey) ? secretKey : "Select secret"}</span>
|
||||
<ChevronsUpDown className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0.5">
|
||||
{isPending ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : isError ? (
|
||||
<p className="p-2 text-sm text-destructive">Failed to load secrets</p>
|
||||
) : secrets.length > 0 && (
|
||||
<>
|
||||
<Command className="mb-2">
|
||||
<CommandInput
|
||||
placeholder="Search secrets..."
|
||||
value={searchFilter}
|
||||
onValueChange={(value) => setSearchFilter(value)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<p className="text-sm">No secrets found</p>
|
||||
<p className="text-sm text-muted-foreground">{`Your search term "${searchFilter}" did not match any secrets.`}</p>
|
||||
</CommandEmpty>
|
||||
<CommandGroup
|
||||
heading="Secrets"
|
||||
>
|
||||
{secrets.map(({ key }) => (
|
||||
<CommandItem
|
||||
className="cursor-pointer"
|
||||
value={key}
|
||||
key={key}
|
||||
onSelect={() => {
|
||||
onSecretChange(key);
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
key === secretKey
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
<Separator className="mt-2" />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsCreateSecretDialogOpen(true);
|
||||
captureEvent('wa_secret_combobox_import_secret_pressed', {
|
||||
type: codeHostType,
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"w-full justify-start gap-1.5 p-2",
|
||||
secrets && !isServiceError(secrets) && secrets.length > 0 && "my-2"
|
||||
)}
|
||||
>
|
||||
<PlusCircleIcon className="h-5 w-5 text-muted-foreground mr-1" />
|
||||
Import a secret
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<ImportSecretDialog
|
||||
open={isCreateSecretDialogOpen}
|
||||
onOpenChange={setIsCreateSecretDialogOpen}
|
||||
onSecretCreated={onSecretCreated}
|
||||
codeHostType={codeHostType}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
|
||||
'use client';
|
||||
|
||||
import { checkIfSecretExists, createConnection } from "@/actions";
|
||||
import { ConnectionIcon } from "@/app/[domain]/connections/components/connectionIcon";
|
||||
import { createZodConnectionConfigValidator } from "@/app/[domain]/connections/utils";
|
||||
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 { CodeHostType, isServiceError, isAuthSupportedForCodeHost } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Schema } from "ajv";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import ConfigEditor, { isConfigValidJson, onQuickAction, QuickActionFn } from "../configEditor";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { InfoIcon, Loader2 } from "lucide-react";
|
||||
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||||
import { SecretCombobox } from "./secretCombobox";
|
||||
import strings from "@/lib/strings";
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
|
||||
interface SharedConnectionCreationFormProps<T> {
|
||||
type: CodeHostType;
|
||||
defaultValues: {
|
||||
name?: string;
|
||||
config: string;
|
||||
};
|
||||
title: string;
|
||||
schema: Schema;
|
||||
quickActions?: {
|
||||
name: string;
|
||||
fn: QuickActionFn<T>;
|
||||
}[],
|
||||
className?: string;
|
||||
onCreated?: (id: number) => void;
|
||||
additionalConfigValidation?: (config: T) => { message: string, isValid: boolean };
|
||||
}
|
||||
|
||||
|
||||
export default function SharedConnectionCreationForm<T>({
|
||||
type,
|
||||
defaultValues,
|
||||
title,
|
||||
schema,
|
||||
quickActions,
|
||||
className,
|
||||
onCreated,
|
||||
additionalConfigValidation
|
||||
}: SharedConnectionCreationFormProps<T>) {
|
||||
const { toast } = useToast();
|
||||
const domain = useDomain();
|
||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||
const captureEvent = useCaptureEvent();
|
||||
const formSchema = useMemo(() => {
|
||||
return z.object({
|
||||
name: z.string().min(1),
|
||||
config: createZodConnectionConfigValidator(schema, additionalConfigValidation),
|
||||
secretKey: z.string().optional().refine(async (secretKey) => {
|
||||
if (!secretKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return checkIfSecretExists(secretKey, domain);
|
||||
}, { message: "Secret not found" }),
|
||||
});
|
||||
}, [schema, domain, additionalConfigValidation]);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: defaultValues,
|
||||
});
|
||||
const { isSubmitting } = form.formState;
|
||||
|
||||
const onSubmit = useCallback(async (data: z.infer<typeof formSchema>) => {
|
||||
const response = await createConnection(data.name, type, data.config, domain);
|
||||
if (isServiceError(response)) {
|
||||
toast({
|
||||
description: `❌ Failed to create connection. Reason: ${response.message}`
|
||||
});
|
||||
captureEvent('wa_create_connection_fail', {
|
||||
type: type,
|
||||
error: response.message,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
description: `✅ Connection created successfully.`
|
||||
});
|
||||
captureEvent('wa_create_connection_success', {
|
||||
type: type,
|
||||
});
|
||||
onCreated?.(response.id);
|
||||
}
|
||||
}, [domain, toast, type, onCreated, captureEvent]);
|
||||
|
||||
const onConfigChange = useCallback((value: string) => {
|
||||
form.setValue("config", value);
|
||||
const isValid = isConfigValidJson(value);
|
||||
setIsSecretsDisabled(!isValid);
|
||||
if (isValid) {
|
||||
const configJson = JSON.parse(value);
|
||||
if (configJson.token?.secret !== undefined) {
|
||||
form.setValue("secretKey", configJson.token.secret);
|
||||
} else {
|
||||
form.setValue("secretKey", undefined);
|
||||
}
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
// Run onConfigChange on mount to set the initial secret key
|
||||
useEffect(() => {
|
||||
onConfigChange(defaultValues.config);
|
||||
}, [defaultValues, onConfigChange]);
|
||||
|
||||
const [isSecretsDisabled, setIsSecretsDisabled] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col max-w-3xl mx-auto bg-background border rounded-lg p-6", className)}>
|
||||
<div className="flex flex-col gap-4 mb-8">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<ConnectionIcon
|
||||
type={type}
|
||||
className="w-7 h-7"
|
||||
/>
|
||||
<h1 className="text-3xl">{title}</h1>
|
||||
</div>
|
||||
<span className="flex flex-row items-center">
|
||||
<InfoIcon className="w-4 h-4 mr-2" />Connections are used to specify what repositories you want Sourcebot to sync.
|
||||
</span>
|
||||
</div>
|
||||
<Form
|
||||
{...form}
|
||||
>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Display Name</FormLabel>
|
||||
<FormDescription>This is the {`connection's`} display name within Sourcebot.</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
spellCheck={false}
|
||||
autoFocus={true}
|
||||
placeholder="my-connection"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isAuthSupportedForCodeHost(type) && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secretKey"
|
||||
render={({ field: { value } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Secret (optional)</FormLabel>
|
||||
<FormDescription>{strings.createSecretDescription}</FormDescription>
|
||||
<FormControl>
|
||||
<SecretCombobox
|
||||
isDisabled={isSecretsDisabled}
|
||||
secretKey={value}
|
||||
codeHostType={type}
|
||||
onSecretChange={(secretKey) => {
|
||||
const view = editorRef.current?.view;
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQuickAction(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(previous: any) => {
|
||||
return {
|
||||
...previous,
|
||||
token: {
|
||||
secret: secretKey,
|
||||
}
|
||||
}
|
||||
},
|
||||
form.getValues("config"),
|
||||
view,
|
||||
{
|
||||
focusEditor: false
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config"
|
||||
render={({ field: { value } }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Configuration</FormLabel>
|
||||
<FormDescription>{strings.connectionConfigDescription}</FormDescription>
|
||||
<FormControl>
|
||||
<ConfigEditor<T>
|
||||
ref={editorRef}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onConfigChange}
|
||||
actions={quickActions ?? []}
|
||||
schema={schema}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button
|
||||
className="mt-5"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting && <Loader2 className="animate-spin w-4 h-4 mr-2" />}
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import ConfigEditor, { isConfigValidJson, onQuickAction, QuickAction } from "../../../components/configEditor";
|
||||
import { createZodConnectionConfigValidator } from "../../utils";
|
||||
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
|
||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
||||
import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type";
|
||||
import { githubQuickActions, gitlabQuickActions, giteaQuickActions, gerritQuickActions, bitbucketCloudQuickActions, bitbucketDataCenterQuickActions } from "../../quickActions";
|
||||
import { Schema } from "ajv";
|
||||
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
|
||||
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
|
||||
import { checkIfSecretExists, updateConnectionConfigAndScheduleSync } from "@/actions";
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import { isServiceError, CodeHostType, isAuthSupportedForCodeHost } from "@/lib/utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
|
||||
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { SecretCombobox } from "@/app/[domain]/components/connectionCreationForms/secretCombobox";
|
||||
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||||
import strings from "@/lib/strings";
|
||||
import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema";
|
||||
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type";
|
||||
|
||||
interface ConfigSettingProps {
|
||||
connectionId: number;
|
||||
config: string;
|
||||
type: CodeHostType;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ConfigSetting = (props: ConfigSettingProps) => {
|
||||
const { type } = props;
|
||||
|
||||
if (type === 'github') {
|
||||
return <ConfigSettingInternal<GithubConnectionConfig>
|
||||
{...props}
|
||||
type="github"
|
||||
quickActions={githubQuickActions}
|
||||
schema={githubSchema}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (type === 'gitlab') {
|
||||
return <ConfigSettingInternal<GitlabConnectionConfig>
|
||||
{...props}
|
||||
type="gitlab"
|
||||
quickActions={gitlabQuickActions}
|
||||
schema={gitlabSchema}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (type === 'bitbucket-cloud') {
|
||||
return <ConfigSettingInternal<BitbucketConnectionConfig>
|
||||
{...props}
|
||||
type="bitbucket-cloud"
|
||||
quickActions={bitbucketCloudQuickActions}
|
||||
schema={bitbucketSchema}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (type === 'bitbucket-server') {
|
||||
return <ConfigSettingInternal<BitbucketConnectionConfig>
|
||||
{...props}
|
||||
type="bitbucket-server"
|
||||
quickActions={bitbucketDataCenterQuickActions}
|
||||
schema={bitbucketSchema}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (type === 'gitea') {
|
||||
return <ConfigSettingInternal<GiteaConnectionConfig>
|
||||
{...props}
|
||||
type="gitea"
|
||||
quickActions={giteaQuickActions}
|
||||
schema={giteaSchema}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (type === 'gerrit') {
|
||||
return <ConfigSettingInternal<GerritConnectionConfig>
|
||||
{...props}
|
||||
type="gerrit"
|
||||
quickActions={gerritQuickActions}
|
||||
schema={gerritSchema}
|
||||
/>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function ConfigSettingInternal<T>({
|
||||
connectionId,
|
||||
config,
|
||||
quickActions,
|
||||
schema,
|
||||
type,
|
||||
disabled,
|
||||
}: ConfigSettingProps & {
|
||||
quickActions?: QuickAction<T>[],
|
||||
schema: Schema,
|
||||
type: CodeHostType,
|
||||
disabled?: boolean,
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const domain = useDomain();
|
||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||
const [isSecretsDisabled, setIsSecretsDisabled] = useState(false);
|
||||
|
||||
const formSchema = useMemo(() => {
|
||||
return z.object({
|
||||
config: createZodConnectionConfigValidator(schema),
|
||||
secretKey: z.string().optional().refine(async (secretKey) => {
|
||||
if (!secretKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return checkIfSecretExists(secretKey, domain);
|
||||
}, { message: "Secret not found" })
|
||||
});
|
||||
}, [schema, domain]);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
config,
|
||||
},
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const onSubmit = useCallback((data: z.infer<typeof formSchema>) => {
|
||||
setIsLoading(true);
|
||||
updateConnectionConfigAndScheduleSync(connectionId, data.config, domain)
|
||||
.then((response) => {
|
||||
if (isServiceError(response)) {
|
||||
toast({
|
||||
description: `❌ Failed to update connection. Reason: ${response.message}`
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
description: `✅ Connection config updated successfully.`
|
||||
});
|
||||
router.push(`?tab=overview`);
|
||||
router.refresh();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
})
|
||||
}, [connectionId, domain, router, toast]);
|
||||
|
||||
const onConfigChange = useCallback((value: string) => {
|
||||
form.setValue("config", value);
|
||||
const isValid = isConfigValidJson(value);
|
||||
setIsSecretsDisabled(!isValid);
|
||||
if (isValid) {
|
||||
const configJson = JSON.parse(value);
|
||||
if (configJson.token?.secret !== undefined) {
|
||||
form.setValue("secretKey", configJson.token.secret);
|
||||
} else {
|
||||
form.setValue("secretKey", undefined);
|
||||
}
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
onConfigChange(config);
|
||||
}, [config, onConfigChange]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full bg-background border rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-2">Configuration</h3>
|
||||
<Form
|
||||
{...form}
|
||||
>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-6"
|
||||
>
|
||||
{isAuthSupportedForCodeHost(type) && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secretKey"
|
||||
render={({ field: { value } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Secret (optional)</FormLabel>
|
||||
<FormDescription>{strings.createSecretDescription}</FormDescription>
|
||||
<FormControl>
|
||||
<SecretCombobox
|
||||
isDisabled={isSecretsDisabled}
|
||||
secretKey={value}
|
||||
codeHostType={type}
|
||||
onSecretChange={(secretKey) => {
|
||||
const view = editorRef.current?.view;
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQuickAction(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(previous: any) => {
|
||||
return {
|
||||
...previous,
|
||||
token: {
|
||||
secret: secretKey,
|
||||
}
|
||||
}
|
||||
},
|
||||
form.getValues("config"),
|
||||
view,
|
||||
{
|
||||
focusEditor: false
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config"
|
||||
render={({ field: { value } }) => (
|
||||
<FormItem>
|
||||
<FormItem>
|
||||
{isAuthSupportedForCodeHost(type) && (
|
||||
<FormLabel>Configuration</FormLabel>
|
||||
)}
|
||||
<FormDescription>{strings.connectionConfigDescription}</FormDescription>
|
||||
<FormControl>
|
||||
<ConfigEditor<T>
|
||||
ref={editorRef}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onConfigChange}
|
||||
schema={schema}
|
||||
actions={quickActions ?? []}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
disabled={isLoading || disabled}
|
||||
>
|
||||
{isLoading && <Loader2 className="animate-spin mr-2" />}
|
||||
{isLoading ? 'Syncing...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,53 +7,29 @@ import {
|
|||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb"
|
||||
import { TabSwitcher } from "@/components/ui/tab-switcher"
|
||||
import { Tabs, TabsContent } from "@/components/ui/tabs"
|
||||
import { ConnectionIcon } from "../components/connectionIcon"
|
||||
import { Header } from "../../components/header"
|
||||
import { ConfigSetting } from "./components/configSetting"
|
||||
import { DeleteConnectionSetting } from "./components/deleteConnectionSetting"
|
||||
import { DisplayNameSetting } from "./components/displayNameSetting"
|
||||
import { RepoList } from "./components/repoList"
|
||||
import { getConnectionByDomain } from "@/data/connection"
|
||||
import { Overview } from "./components/overview"
|
||||
import { getOrgMembership } from "@/actions"
|
||||
import { isServiceError } from "@/lib/utils"
|
||||
import { notFound } from "next/navigation"
|
||||
import { OrgRole } from "@sourcebot/db"
|
||||
import { CodeHostType } from "@/lib/utils"
|
||||
import { env } from "@/env.mjs"
|
||||
|
||||
interface ConnectionManagementPageProps {
|
||||
params: Promise<{
|
||||
domain: string
|
||||
id: string
|
||||
}>,
|
||||
searchParams: Promise<{
|
||||
tab: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function ConnectionManagementPage(props: ConnectionManagementPageProps) {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const connection = await getConnectionByDomain(Number(params.id), params.domain);
|
||||
if (!connection) {
|
||||
return <NotFound className="flex w-full h-full items-center justify-center" message="Connection not found" />
|
||||
}
|
||||
|
||||
const membership = await getOrgMembership(params.domain);
|
||||
if (isServiceError(membership)) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const isOwner = membership.role === OrgRole.OWNER;
|
||||
const isDisabled = !isOwner || env.CONFIG_PATH !== undefined;
|
||||
const currentTab = searchParams.tab || "overview";
|
||||
|
||||
return (
|
||||
<Tabs value={currentTab} className="w-full">
|
||||
<Header className="mb-6" withTopMargin={false}>
|
||||
<div>
|
||||
<Header>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
|
|
@ -65,46 +41,23 @@ export default async function ConnectionManagementPage(props: ConnectionManageme
|
|||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="mt-6 flex flex-row items-center gap-4 w-full">
|
||||
<div className="mt-6 flex items-center gap-3">
|
||||
<ConnectionIcon type={connection.connectionType} />
|
||||
<h1 className="text-lg font-semibold">{connection.name}</h1>
|
||||
<h1 className="text-3xl font-semibold">{connection.name}</h1>
|
||||
</div>
|
||||
<TabSwitcher
|
||||
className="h-auto p-0 bg-transparent border-b border-border mt-6"
|
||||
tabs={[
|
||||
{ label: "Overview", value: "overview" },
|
||||
{ label: "Settings", value: "settings" },
|
||||
]}
|
||||
currentTab={currentTab}
|
||||
/>
|
||||
</Header>
|
||||
<TabsContent
|
||||
value="overview"
|
||||
className="space-y-8"
|
||||
>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="font-semibold text-lg mb-4">Overview</h1>
|
||||
<h2 className="text-lg font-medium mb-4">Overview</h2>
|
||||
<Overview connectionId={connection.id} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 className="font-semibold text-lg mb-4">Linked Repositories</h1>
|
||||
<h2 className="text-lg font-medium mb-4">Linked Repositories</h2>
|
||||
<RepoList connectionId={connection.id} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="settings"
|
||||
className="flex flex-col gap-6"
|
||||
>
|
||||
<DisplayNameSetting connectionId={connection.id} name={connection.name} disabled={isDisabled} />
|
||||
<ConfigSetting
|
||||
connectionId={connection.id}
|
||||
type={connection.connectionType as CodeHostType}
|
||||
config={JSON.stringify(connection.config, null, 2)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<DeleteConnectionSetting connectionId={connection.id} disabled={isDisabled} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,148 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { cn, type CodeHostType, getCodeHostIcon } from "@/lib/utils"
|
||||
import placeholderLogo from "@/public/placeholder_avatar.png"
|
||||
import { BlocksIcon, LockIcon } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { useMemo } from "react"
|
||||
import { OrgRole } from "@sourcebot/db"
|
||||
|
||||
interface NewConnectionCardProps {
|
||||
className?: string
|
||||
role: OrgRole
|
||||
configPathProvided: boolean
|
||||
}
|
||||
|
||||
export const NewConnectionCard = ({ className, role, configPathProvided }: NewConnectionCardProps) => {
|
||||
const isOwner = role === OrgRole.OWNER
|
||||
const isDisabled = !isOwner || configPathProvided
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col border rounded-lg p-4 h-fit relative",
|
||||
isDisabled && "bg-muted/10 border-muted cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isDisabled && (
|
||||
<div className="absolute right-3 top-3">
|
||||
<LockIcon className="w-4 h-4 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<BlocksIcon className={cn("mx-auto w-7 h-7", isDisabled && "text-muted-foreground")} />
|
||||
<h2 className={cn("mx-auto mt-4 font-medium text-lg", isDisabled && "text-muted-foreground")}>
|
||||
Connect to a Code Host
|
||||
</h2>
|
||||
<p className="mx-auto text-center text-sm text-muted-foreground font-light">
|
||||
Create a connection to import repos from a code host.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<Card
|
||||
type="github"
|
||||
title="GitHub"
|
||||
subtitle="Cloud or Enterprise supported."
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<Card
|
||||
type="gitlab"
|
||||
title="GitLab"
|
||||
subtitle="Cloud and Self-Hosted supported."
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<Card
|
||||
type="bitbucket-cloud"
|
||||
title="Bitbucket Cloud"
|
||||
subtitle="Fetch repos from Bitbucket Cloud."
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<Card
|
||||
type="bitbucket-server"
|
||||
title="Bitbucket Data Center"
|
||||
subtitle="Fetch repos from a Bitbucket DC instance."
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<Card
|
||||
type="gitea"
|
||||
title="Gitea"
|
||||
subtitle="Cloud and Self-Hosted supported."
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<Card
|
||||
type="gerrit"
|
||||
title="Gerrit"
|
||||
subtitle="Cloud and Self-Hosted supported."
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
{isDisabled && (
|
||||
<p className="mt-4 text-xs text-center text-muted-foreground">
|
||||
{configPathProvided
|
||||
? "Connections are managed through the configuration file."
|
||||
: "Only organization owners can manage connections."}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface CardProps {
|
||||
type: string
|
||||
title: string
|
||||
subtitle: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const Card = ({ type, title, subtitle, disabled = false }: CardProps) => {
|
||||
const Icon = useMemo(() => {
|
||||
const iconInfo = getCodeHostIcon(type as CodeHostType)
|
||||
if (iconInfo) {
|
||||
const { src, className } = iconInfo
|
||||
return (
|
||||
<Image
|
||||
src={src || "/placeholder.svg"}
|
||||
className={cn("rounded-full w-7 h-7 mb-1", className, disabled && "opacity-50")}
|
||||
alt={`${type} logo`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={placeholderLogo || "/placeholder.svg"}
|
||||
alt={`${type} logo`}
|
||||
className={cn("rounded-full w-7 h-7 mb-1", disabled && "opacity-50")}
|
||||
/>
|
||||
)
|
||||
}, [type, disabled])
|
||||
|
||||
const CardContent = (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-row justify-between items-center p-2",
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer",
|
||||
disabled && "opacity-70",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{Icon}
|
||||
<div>
|
||||
<p className={cn("font-medium", disabled && "text-muted-foreground")}>{title}</p>
|
||||
<p className="text-sm text-muted-foreground font-light">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (disabled) {
|
||||
return CardContent
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="flex flex-row justify-between items-center cursor-pointer" href={`connections/new/${type}`}>
|
||||
{CardContent}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
GitHubConnectionCreationForm,
|
||||
GitLabConnectionCreationForm,
|
||||
GiteaConnectionCreationForm,
|
||||
GerritConnectionCreationForm,
|
||||
BitbucketCloudConnectionCreationForm,
|
||||
BitbucketDataCenterConnectionCreationForm
|
||||
} from "@/app/[domain]/components/connectionCreationForms";
|
||||
import { useCallback, use } from "react";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
|
||||
export default function NewConnectionPage(props: { params: Promise<{ type: string }> }) {
|
||||
const params = use(props.params);
|
||||
const { type } = params;
|
||||
const router = useRouter();
|
||||
const domain = useDomain();
|
||||
|
||||
const onCreated = useCallback(() => {
|
||||
router.push(`/${domain}/connections`);
|
||||
}, [domain, router]);
|
||||
|
||||
if (type === 'github') {
|
||||
return <GitHubConnectionCreationForm onCreated={onCreated} />;
|
||||
}
|
||||
|
||||
if (type === 'gitlab') {
|
||||
return <GitLabConnectionCreationForm onCreated={onCreated} />;
|
||||
}
|
||||
|
||||
if (type === 'gitea') {
|
||||
return <GiteaConnectionCreationForm onCreated={onCreated} />;
|
||||
}
|
||||
|
||||
if (type === 'gerrit') {
|
||||
return <GerritConnectionCreationForm onCreated={onCreated} />;
|
||||
}
|
||||
|
||||
if (type === 'bitbucket-cloud') {
|
||||
return <BitbucketCloudConnectionCreationForm onCreated={onCreated} />;
|
||||
}
|
||||
|
||||
if (type === 'bitbucket-server') {
|
||||
return <BitbucketDataCenterConnectionCreationForm onCreated={onCreated} />;
|
||||
}
|
||||
|
||||
|
||||
router.push(`/${domain}/connections`);
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import { ConnectionList } from "./components/connectionList";
|
||||
import { Header } from "../components/header";
|
||||
import { NewConnectionCard } from "./components/newConnectionCard";
|
||||
import { getConnections, getOrgMembership } from "@/actions";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { notFound, ServiceErrorException } from "@/lib/serviceError";
|
||||
import { OrgRole } from "@sourcebot/db";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export default async function ConnectionsPage(props: { params: Promise<{ domain: string }> }) {
|
||||
const params = await props.params;
|
||||
|
|
@ -29,17 +27,9 @@ export default async function ConnectionsPage(props: { params: Promise<{ domain:
|
|||
<Header>
|
||||
<h1 className="text-3xl">Connections</h1>
|
||||
</Header>
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<ConnectionList
|
||||
className="md:w-3/4"
|
||||
isDisabled={membership.role !== OrgRole.OWNER}
|
||||
/>
|
||||
<NewConnectionCard
|
||||
className="md:w-1/4"
|
||||
role={membership.role}
|
||||
configPathProvided={env.CONFIG_PATH !== undefined}
|
||||
/>
|
||||
</div>
|
||||
<ConnectionList
|
||||
isDisabled={membership.role !== OrgRole.OWNER}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue