-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(admin,packages,space,web): GitLab OAuth client
- Loading branch information
Showing
25 changed files
with
535 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
"use client"; | ||
|
||
import React from "react"; | ||
import { observer } from "mobx-react-lite"; | ||
import Link from "next/link"; | ||
// icons | ||
import { Settings2 } from "lucide-react"; | ||
// types | ||
import { TInstanceAuthenticationMethodKeys } from "@plane/types"; | ||
// ui | ||
import { ToggleSwitch, getButtonStyling } from "@plane/ui"; | ||
// helpers | ||
import { cn } from "@/helpers/common.helper"; | ||
// hooks | ||
import { useInstance } from "@/hooks/store"; | ||
|
||
type Props = { | ||
disabled: boolean; | ||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void; | ||
}; | ||
|
||
export const GitlabConfiguration: React.FC<Props> = observer((props) => { | ||
const { disabled, updateConfig } = props; | ||
// store | ||
const { formattedConfig } = useInstance(); | ||
// derived values | ||
const enableGitlabConfig = formattedConfig?.IS_GITLAB_ENABLED ?? ""; | ||
const isGitlabConfigured = !!formattedConfig?.GITLAB_CLIENT_ID && !!formattedConfig?.GITLAB_CLIENT_SECRET; | ||
|
||
return ( | ||
<> | ||
{isGitlabConfigured ? ( | ||
<div className="flex items-center gap-4"> | ||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}> | ||
Edit | ||
</Link> | ||
<ToggleSwitch | ||
value={Boolean(parseInt(enableGitlabConfig))} | ||
onChange={() => { | ||
Boolean(parseInt(enableGitlabConfig)) === true | ||
? updateConfig("IS_GITLAB_ENABLED", "0") | ||
: updateConfig("IS_GITLAB_ENABLED", "1"); | ||
}} | ||
size="sm" | ||
disabled={disabled} | ||
/> | ||
</div> | ||
) : ( | ||
<Link | ||
href="/authentication/gitlab" | ||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")} | ||
> | ||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" /> | ||
Configure | ||
</Link> | ||
)} | ||
</> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
export * from "./email-config-switch"; | ||
export * from "./password-config-switch"; | ||
export * from "./authentication-method-card"; | ||
export * from "./gitlab-config"; | ||
export * from "./github-config"; | ||
export * from "./google-config"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import { FC, useState } from "react"; | ||
import isEmpty from "lodash/isEmpty"; | ||
import Link from "next/link"; | ||
import { useForm } from "react-hook-form"; | ||
// types | ||
import { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types"; | ||
// ui | ||
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; | ||
// components | ||
import { | ||
ConfirmDiscardModal, | ||
ControllerInput, | ||
CopyField, | ||
TControllerInputFormField, | ||
TCopyField, | ||
} from "@/components/common"; | ||
// helpers | ||
import { API_BASE_URL, cn } from "@/helpers/common.helper"; | ||
// hooks | ||
import { useInstance } from "@/hooks/store"; | ||
|
||
type Props = { | ||
config: IFormattedInstanceConfiguration; | ||
}; | ||
|
||
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>; | ||
|
||
export const InstanceGitlabConfigForm: FC<Props> = (props) => { | ||
const { config } = props; | ||
// states | ||
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false); | ||
// store hooks | ||
const { updateInstanceConfigurations } = useInstance(); | ||
// form data | ||
const { | ||
handleSubmit, | ||
control, | ||
reset, | ||
formState: { errors, isDirty, isSubmitting }, | ||
} = useForm<GitlabConfigFormValues>({ | ||
defaultValues: { | ||
GITLAB_HOST: config["GITLAB_HOST"], | ||
GITLAB_CLIENT_ID: config["GITLAB_CLIENT_ID"], | ||
GITLAB_CLIENT_SECRET: config["GITLAB_CLIENT_SECRET"], | ||
}, | ||
}); | ||
|
||
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : ""; | ||
|
||
const GITLAB_FORM_FIELDS: TControllerInputFormField[] = [ | ||
{ | ||
key: "GITLAB_HOST", | ||
type: "text", | ||
label: "Host", | ||
description: ( | ||
<> | ||
This is the <b>GitLab host</b> to use for login, <b>including scheme</b>. | ||
</> | ||
), | ||
placeholder: "https://gitlab.com", | ||
error: Boolean(errors.GITLAB_HOST), | ||
required: true, | ||
}, | ||
{ | ||
key: "GITLAB_CLIENT_ID", | ||
type: "text", | ||
label: "Application ID", | ||
description: ( | ||
<> | ||
Get this from your{" "} | ||
<a | ||
tabIndex={-1} | ||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html" | ||
target="_blank" | ||
className="text-custom-primary-100 hover:underline" | ||
rel="noreferrer" | ||
> | ||
GitLab OAuth application settings | ||
</a> | ||
. | ||
</> | ||
), | ||
placeholder: "c2ef2e7fc4e9d15aa7630f5637d59e8e4a27ff01dceebdb26b0d267b9adcf3c3", | ||
error: Boolean(errors.GITLAB_CLIENT_ID), | ||
required: true, | ||
}, | ||
{ | ||
key: "GITLAB_CLIENT_SECRET", | ||
type: "password", | ||
label: "Secret", | ||
description: ( | ||
<> | ||
The client secret is also found in your{" "} | ||
<a | ||
tabIndex={-1} | ||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html" | ||
target="_blank" | ||
className="text-custom-primary-100 hover:underline" | ||
rel="noreferrer" | ||
> | ||
GitLab OAuth application settings | ||
</a> | ||
. | ||
</> | ||
), | ||
placeholder: "gloas-f79cfa9a03c97f6ffab303177a5a6778a53c61e3914ba093412f68a9298a1b28", | ||
error: Boolean(errors.GITLAB_CLIENT_SECRET), | ||
required: true, | ||
}, | ||
]; | ||
|
||
const GITLAB_SERVICE_FIELD: TCopyField[] = [ | ||
{ | ||
key: "Callback_URL", | ||
label: "Callback URL", | ||
url: `${originURL}/auth/gitlab/callback/`, | ||
description: ( | ||
<> | ||
We will auto-generate this. Paste this into the <b>Redirect URI</b> field of your{" "} | ||
<a | ||
tabIndex={-1} | ||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html" | ||
target="_blank" | ||
className="text-custom-primary-100 hover:underline" | ||
rel="noreferrer" | ||
> | ||
GitLab OAuth application | ||
</a> | ||
. | ||
</> | ||
), | ||
}, | ||
]; | ||
|
||
const onSubmit = async (formData: GitlabConfigFormValues) => { | ||
const payload: Partial<GitlabConfigFormValues> = { ...formData }; | ||
|
||
await updateInstanceConfigurations(payload) | ||
.then((response = []) => { | ||
setToast({ | ||
type: TOAST_TYPE.SUCCESS, | ||
title: "Success", | ||
message: "GitLab Configuration Settings updated successfully", | ||
}); | ||
reset({ | ||
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value, | ||
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value, | ||
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value, | ||
}); | ||
}) | ||
.catch((err) => console.error(err)); | ||
}; | ||
|
||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { | ||
if (isDirty) { | ||
e.preventDefault(); | ||
setIsDiscardChangesModalOpen(true); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
<ConfirmDiscardModal | ||
isOpen={isDiscardChangesModalOpen} | ||
onDiscardHref="/authentication" | ||
handleClose={() => setIsDiscardChangesModalOpen(false)} | ||
/> | ||
<div className="flex flex-col gap-8"> | ||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full"> | ||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1"> | ||
<div className="pt-2 text-xl font-medium">Configuration</div> | ||
{GITLAB_FORM_FIELDS.map((field) => ( | ||
<ControllerInput | ||
key={field.key} | ||
control={control} | ||
type={field.type} | ||
name={field.key} | ||
label={field.label} | ||
description={field.description} | ||
placeholder={field.placeholder} | ||
error={field.error} | ||
required={field.required} | ||
/> | ||
))} | ||
<div className="flex flex-col gap-1 pt-4"> | ||
<div className="flex items-center gap-4"> | ||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}> | ||
{isSubmitting ? "Saving..." : "Save changes"} | ||
</Button> | ||
<Link | ||
href="/authentication" | ||
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")} | ||
onClick={handleGoBack} | ||
> | ||
Go back | ||
</Link> | ||
</div> | ||
</div> | ||
</div> | ||
<div className="col-span-2 md:col-span-1"> | ||
<div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg"> | ||
<div className="pt-2 text-xl font-medium">Service provider details</div> | ||
{GITLAB_SERVICE_FIELD.map((field) => ( | ||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} /> | ||
))} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
"use client"; | ||
|
||
import { useState } from "react"; | ||
import { observer } from "mobx-react-lite"; | ||
import Image from "next/image"; | ||
import { useTheme } from "next-themes"; | ||
import useSWR from "swr"; | ||
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui"; | ||
// components | ||
import { PageHeader } from "@/components/core"; | ||
// helpers | ||
import { resolveGeneralTheme } from "@/helpers/common.helper"; | ||
// hooks | ||
import { useInstance } from "@/hooks/store"; | ||
// icons | ||
import GitlabLogo from "@/public/logos/gitlab-logo.svg"; | ||
// local components | ||
import { AuthenticationMethodCard } from "../components"; | ||
import { InstanceGitlabConfigForm } from "./form"; | ||
|
||
const InstanceGitlabAuthenticationPage = observer(() => { | ||
// store | ||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance(); | ||
// state | ||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); | ||
// config | ||
const enableGitlabConfig = formattedConfig?.IS_GITLAB_ENABLED ?? ""; | ||
|
||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); | ||
|
||
const updateConfig = async (key: "IS_GITLAB_ENABLED", value: string) => { | ||
setIsSubmitting(true); | ||
|
||
const payload = { | ||
[key]: value, | ||
}; | ||
|
||
const updateConfigPromise = updateInstanceConfigurations(payload); | ||
|
||
setPromiseToast(updateConfigPromise, { | ||
loading: "Saving Configuration...", | ||
success: { | ||
title: "Configuration saved", | ||
message: () => `GitLab authentication is now ${value ? "active" : "disabled"}.`, | ||
}, | ||
error: { | ||
title: "Error", | ||
message: () => "Failed to save configuration", | ||
}, | ||
}); | ||
|
||
await updateConfigPromise | ||
.then(() => { | ||
setIsSubmitting(false); | ||
}) | ||
.catch((err) => { | ||
console.error(err); | ||
setIsSubmitting(false); | ||
}); | ||
}; | ||
return ( | ||
<> | ||
<PageHeader title="Authentication - God Mode" /> | ||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> | ||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0"> | ||
<AuthenticationMethodCard | ||
name="GitLab" | ||
description="Allow members to login or sign up to plane with their GitLab accounts." | ||
icon={<Image src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />} | ||
config={ | ||
<ToggleSwitch | ||
value={Boolean(parseInt(enableGitlabConfig))} | ||
onChange={() => { | ||
Boolean(parseInt(enableGitlabConfig)) === true | ||
? updateConfig("IS_GITLAB_ENABLED", "0") | ||
: updateConfig("IS_GITLAB_ENABLED", "1"); | ||
}} | ||
size="sm" | ||
disabled={isSubmitting || !formattedConfig} | ||
/> | ||
} | ||
disabled={isSubmitting || !formattedConfig} | ||
withBorder={false} | ||
/> | ||
</div> | ||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4"> | ||
{formattedConfig ? ( | ||
<InstanceGitlabConfigForm config={formattedConfig} /> | ||
) : ( | ||
<Loader className="space-y-8"> | ||
<Loader.Item height="50px" width="25%" /> | ||
<Loader.Item height="50px" /> | ||
<Loader.Item height="50px" /> | ||
<Loader.Item height="50px" /> | ||
<Loader.Item height="50px" width="50%" /> | ||
</Loader> | ||
)} | ||
</div> | ||
</div> | ||
</> | ||
); | ||
}); | ||
|
||
export default InstanceGitlabAuthenticationPage; |
Oops, something went wrong.