Feat: God Mode UI Updates and More Config Settings (#2877)

* feat: Images in Plane config screen.
* feat: Enable/ Disable Magic Login config toggle.
* style: UX copy and design updates across all screens.
* style: SSO and OAuth Screen revamp.
* style: Enter God Mode button for Profile Settings sidebar.
* fix: update input type to password for password fields.
This commit is contained in:
Prateek Shourya 2023-11-25 21:23:50 +05:30 committed by sriram veeraghanta
parent bf060cc8eb
commit 398f35d36d
19 changed files with 672 additions and 360 deletions

View File

@ -9,17 +9,16 @@ import useToast from "hooks/use-toast";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
export interface IInstanceOpenAIForm { export interface IInstanceAIForm {
config: IFormattedInstanceConfiguration; config: IFormattedInstanceConfiguration;
} }
export interface OpenAIFormValues { export interface AIFormValues {
OPENAI_API_BASE: string;
OPENAI_API_KEY: string; OPENAI_API_KEY: string;
GPT_ENGINE: string; GPT_ENGINE: string;
} }
export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => { export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
const { config } = props; const { config } = props;
// store // store
const { instance: instanceStore } = useMobxStore(); const { instance: instanceStore } = useMobxStore();
@ -30,16 +29,15 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
handleSubmit, handleSubmit,
control, control,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<OpenAIFormValues>({ } = useForm<AIFormValues>({
defaultValues: { defaultValues: {
OPENAI_API_BASE: config["OPENAI_API_BASE"],
OPENAI_API_KEY: config["OPENAI_API_KEY"], OPENAI_API_KEY: config["OPENAI_API_KEY"],
GPT_ENGINE: config["GPT_ENGINE"], GPT_ENGINE: config["GPT_ENGINE"],
}, },
}); });
const onSubmit = async (formData: OpenAIFormValues) => { const onSubmit = async (formData: AIFormValues) => {
const payload: Partial<OpenAIFormValues> = { ...formData }; const payload: Partial<AIFormValues> = { ...formData };
await instanceStore await instanceStore
.updateInstanceConfigurations(payload) .updateInstanceConfigurations(payload)
@ -47,64 +45,15 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
setToastAlert({ setToastAlert({
title: "Success", title: "Success",
type: "success", type: "success",
message: "Open AI Settings updated successfully", message: "AI Settings updated successfully",
}) })
) )
.catch((err) => console.error(err)); .catch((err) => console.error(err));
}; };
return ( return (
<div className="flex flex-col gap-8 m-8 w-4/5"> <>
<div className="pb-2 mb-2 border-b border-custom-border-100"> <div className="grid grid-col grid-cols-1 lg:grid-cols-3 items-center justify-between gap-x-16 gap-y-8 w-full">
<div className="text-custom-text-100 font-medium text-lg">OpenAI</div>
<div className="text-custom-text-300 font-normal text-sm">
AI is everywhere make use it as much as you can! <a href="#" className="text-custom-primary-100">Learn more.</a>
</div>
</div>
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
<div className="flex flex-col gap-1">
<h4 className="text-sm">OpenAI API Base</h4>
<Controller
control={control}
name="OPENAI_API_BASE"
render={({ field: { value, onChange, ref } }) => (
<Input
id="OPENAI_API_BASE"
name="OPENAI_API_BASE"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.OPENAI_API_BASE)}
placeholder="OpenAI API Base"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">OpenAI API Key</h4>
<Controller
control={control}
name="OPENAI_API_KEY"
render={({ field: { value, onChange, ref } }) => (
<Input
id="OPENAI_API_KEY"
name="OPENAI_API_KEY"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.OPENAI_API_KEY)}
placeholder="OpenAI API Key"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
</div>
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">GPT Engine</h4> <h4 className="text-sm">GPT Engine</h4>
<Controller <Controller
@ -119,11 +68,54 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.GPT_ENGINE)} hasError={Boolean(errors.GPT_ENGINE)}
placeholder="GPT Engine" placeholder="gpt-3.5-turbo"
className="rounded-md font-medium w-full" className="rounded-md font-medium w-full"
/> />
)} )}
/> />
<p className="text-xs text-custom-text-400">
Choose an OpenAI engine.{" "}
<a
href="https://platform.openai.com/docs/models/overview"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more
</a>
</p>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">API Key</h4>
<Controller
control={control}
name="OPENAI_API_KEY"
render={({ field: { value, onChange, ref } }) => (
<Input
id="OPENAI_API_KEY"
name="OPENAI_API_KEY"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.OPENAI_API_KEY)}
placeholder="sk-asddassdfasdefqsdfasd23das3dasdcasd"
className="rounded-md font-medium w-full"
/>
)}
/>
<p className="text-xs text-custom-text-400">
You will find your API key{" "}
<a
href="https://platform.openai.com/api-keys"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
</a>
</p>
</div> </div>
</div> </div>
@ -132,6 +124,6 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
{isSubmitting ? "Saving..." : "Save Changes"} {isSubmitting ? "Saving..." : "Save Changes"}
</Button> </Button>
</div> </div>
</div> </>
); );
}; };

View File

@ -31,6 +31,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
// form data // form data
const { const {
handleSubmit, handleSubmit,
watch,
control, control,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<EmailFormValues>({ } = useForm<EmailFormValues>({
@ -60,11 +61,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
}; };
return ( return (
<div className="flex flex-col gap-8 m-8 w-4/5"> <>
<div className="pb-2 mb-2 border-b border-custom-border-100">
<div className="text-custom-text-100 font-medium text-lg">Email</div>
<div className="text-custom-text-300 font-normal text-sm">Email related settings.</div>
</div>
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full"> <div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">Host</h4> <h4 className="text-sm">Host</h4>
@ -80,7 +77,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.EMAIL_HOST)} hasError={Boolean(errors.EMAIL_HOST)}
placeholder="Email Host" placeholder="email.google.com"
className="rounded-md font-medium w-full" className="rounded-md font-medium w-full"
/> />
)} )}
@ -101,7 +98,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.EMAIL_PORT)} hasError={Boolean(errors.EMAIL_PORT)}
placeholder="Email Port" placeholder="8080"
className="rounded-md font-medium w-full" className="rounded-md font-medium w-full"
/> />
)} )}
@ -123,7 +120,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.EMAIL_HOST_USER)} hasError={Boolean(errors.EMAIL_HOST_USER)}
placeholder="Username" placeholder="getitdone@projectplane.so"
className="rounded-md font-medium w-full" className="rounded-md font-medium w-full"
/> />
)} )}
@ -139,7 +136,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
<Input <Input
id="EMAIL_HOST_PASSWORD" id="EMAIL_HOST_PASSWORD"
name="EMAIL_HOST_PASSWORD" name="EMAIL_HOST_PASSWORD"
type="text" type="password"
value={value} value={value}
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
@ -152,11 +149,15 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
</div> </div>
</div> </div>
<div className="flex items-center gap-8 pt-4"> <div className="w-full lg:w-1/2 flex flex-col px-1 gap-y-8">
<div> <div className="flex items-center gap-8 pt-4 mr-8">
<div className="text-custom-text-100 font-medium text-sm">Enable TLS</div> <div className="grow">
<div className="text-custom-text-100 font-medium text-sm">
Turn TLS {Boolean(parseInt(watch("EMAIL_USE_TLS"))) ? "off" : "on"}
</div> </div>
<div> <div className="text-custom-text-300 font-normal text-xs">Use this if your email domain supports TLS.</div>
</div>
<div className="shrink-0">
<Controller <Controller
control={control} control={control}
name="EMAIL_USE_TLS" name="EMAIL_USE_TLS"
@ -173,11 +174,16 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
</div> </div>
</div> </div>
<div className="flex items-center gap-8 pt-4"> <div className="flex items-center gap-8 pt-4 mr-8">
<div> <div className="grow">
<div className="text-custom-text-100 font-medium text-sm">Enable SSL</div> <div className="text-custom-text-100 font-medium text-sm">
Turn SSL {Boolean(parseInt(watch("EMAIL_USE_SSL"))) ? "off" : "on"}
</div> </div>
<div> <div className="text-custom-text-300 font-normal text-xs">
Most email domains support SSL. Use this to secure comms between this instance and your users.
</div>
</div>
<div className="shrink-0">
<Controller <Controller
control={control} control={control}
name="EMAIL_USE_SSL" name="EMAIL_USE_SSL"
@ -193,12 +199,13 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
/> />
</div> </div>
</div> </div>
</div>
<div className="flex items-center py-1"> <div className="flex items-center py-1">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}> <Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"} {isSubmitting ? "Saving..." : "Save Changes"}
</Button> </Button>
</div> </div>
</div> </>
); );
}; };

View File

@ -14,7 +14,7 @@ export interface IInstanceGeneralForm {
export interface GeneralFormValues { export interface GeneralFormValues {
instance_name: string; instance_name: string;
is_telemetry_enabled: boolean; // is_telemetry_enabled: boolean;
} }
export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => { export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
@ -31,7 +31,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
} = useForm<GeneralFormValues>({ } = useForm<GeneralFormValues>({
defaultValues: { defaultValues: {
instance_name: instance.instance_name, instance_name: instance.instance_name,
is_telemetry_enabled: instance.is_telemetry_enabled, // is_telemetry_enabled: instance.is_telemetry_enabled,
}, },
}); });
@ -51,13 +51,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
}; };
return ( return (
<div className="flex flex-col gap-8 m-8"> <>
<div className="pb-2 mb-2 border-b border-custom-border-100">
<div className="text-custom-text-100 font-medium text-lg">General</div>
<div className="text-custom-text-300 font-normal text-sm">
The usual things like your mail, name of instance and other stuff.
</div>
</div>
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-8 w-full"> <div className="grid grid-col grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-8 w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">Name of instance</h4> <h4 className="text-sm">Name of instance</h4>
@ -106,7 +100,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
</div> </div>
</div> </div>
<div className="flex items-center gap-8 pt-4"> {/* <div className="flex items-center gap-12 pt-4">
<div> <div>
<div className="text-custom-text-100 font-medium text-sm">Share anonymous usage instance</div> <div className="text-custom-text-100 font-medium text-sm">Share anonymous usage instance</div>
<div className="text-custom-text-300 font-normal text-xs"> <div className="text-custom-text-300 font-normal text-xs">
@ -120,13 +114,13 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
render={({ field: { value, onChange } }) => <ToggleSwitch value={value} onChange={onChange} size="sm" />} render={({ field: { value, onChange } }) => <ToggleSwitch value={value} onChange={onChange} size="sm" />}
/> />
</div> </div>
</div> </div> */}
<div className="flex items-center py-1"> <div className="flex items-center py-1">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}> <Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"} {isSubmitting ? "Saving..." : "Save Changes"}
</Button> </Button>
</div> </div>
</div> </>
); );
}; };

View File

@ -56,8 +56,8 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
const originURL = typeof window !== "undefined" ? window.location.origin : ""; const originURL = typeof window !== "undefined" ? window.location.origin : "";
return ( return (
<> <div className="flex flex-col gap-8">
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full"> <div className="grid grid-col grid-cols-1 lg:grid-cols-3 justify-between gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">Client ID</h4> <h4 className="text-sm">Client ID</h4>
<Controller <Controller
@ -72,11 +72,22 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.GITHUB_CLIENT_ID)} hasError={Boolean(errors.GITHUB_CLIENT_ID)}
placeholder="Github Client ID" placeholder="70a44354520df8bd9bcd"
className="rounded-md font-medium w-full" className="rounded-md font-medium w-full"
/> />
)} )}
/> />
<p className="text-xs text-custom-text-400">
You will get this from your{" "}
<a
href="https://github.com/settings/applications/new"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitHub OAuth application settings.
</a>
</p>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">Client Secret</h4> <h4 className="text-sm">Client Secret</h4>
@ -92,14 +103,23 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.GITHUB_CLIENT_SECRET)} hasError={Boolean(errors.GITHUB_CLIENT_SECRET)}
placeholder="Github Client Secret" placeholder="9b0050f94ec1b744e32ce79ea4ffacd40d4119cb"
className="rounded-md font-medium w-full" className="rounded-md font-medium w-full"
/> />
)} )}
/> />
<p className="text-xs text-custom-text-400">
Your client secret is also found in your{" "}
<a
href="https://github.com/settings/applications/new"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitHub OAuth application settings.
</a>
</p>
</div> </div>
</div>
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">Origin URL</h4> <h4 className="text-sm">Origin URL</h4>
<Button <Button
@ -117,16 +137,26 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
<p className="font-medium text-sm">{originURL}</p> <p className="font-medium text-sm">{originURL}</p>
<Copy size={18} color="#B9B9B9" /> <Copy size={18} color="#B9B9B9" />
</Button> </Button>
<p className="text-xs text-custom-text-400/60">*paste this URL in your Github console.</p> <p className="text-xs text-custom-text-400">
We will auto-generate this. Paste this into the Authorization callback URL field{" "}
<a
href="https://github.com/settings/applications/new"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
</a>
</p>
</div>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center p-2"> <div className="flex items-center">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}> <Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"} {isSubmitting ? "Saving..." : "Save Changes"}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</>
); );
}; };

View File

@ -56,8 +56,8 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
const originURL = typeof window !== "undefined" ? window.location.origin : ""; const originURL = typeof window !== "undefined" ? window.location.origin : "";
return ( return (
<> <div className="flex flex-col gap-8">
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full"> <div className="grid grid-col grid-cols-1 lg:grid-cols-3 justify-between gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">Client ID</h4> <h4 className="text-sm">Client ID</h4>
<Controller <Controller
@ -72,11 +72,22 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.GOOGLE_CLIENT_ID)} hasError={Boolean(errors.GOOGLE_CLIENT_ID)}
placeholder="Google Client ID" placeholder="840195096245-0p2tstej9j5nc4l8o1ah2dqondscqc1g.apps.googleusercontent.com"
className="rounded-md font-medium w-full" className="rounded-md font-medium w-full"
/> />
)} )}
/> />
<p className="text-xs text-custom-text-400">
Your client ID lives in your Google API Console.{" "}
<a
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more
</a>
</p>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">Client Secret</h4> <h4 className="text-sm">Client Secret</h4>
@ -92,14 +103,23 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.GOOGLE_CLIENT_SECRET)} hasError={Boolean(errors.GOOGLE_CLIENT_SECRET)}
placeholder="Google Client Secret" placeholder="GOCShX-ADp4cI0kPqav1gGCBg5bE02E"
className="rounded-md font-medium w-full" className="rounded-md font-medium w-full"
/> />
)} )}
/> />
<p className="text-xs text-custom-text-400">
Your client secret should also be in your Google API Console.{" "}
<a
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more
</a>
</p>
</div> </div>
</div>
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">Origin URL</h4> <h4 className="text-sm">Origin URL</h4>
<Button <Button
@ -117,16 +137,26 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
<p className="font-medium text-sm">{originURL}</p> <p className="font-medium text-sm">{originURL}</p>
<Copy size={18} color="#B9B9B9" /> <Copy size={18} color="#B9B9B9" />
</Button> </Button>
<p className="text-xs text-custom-text-400/60">*paste this URL in your Google developer console.</p> <p className="text-xs text-custom-text-400">
We will auto-generate this. Paste this into your Authorized JavaScript origins field. For this OAuth client{" "}
<a
href="https://console.cloud.google.com/apis/credentials/oauthclient"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
</a>
</p>
</div>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center p-2"> <div className="flex items-center">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}> <Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"} {isSubmitting ? "Saving..." : "Save Changes"}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</>
); );
}; };

View File

@ -0,0 +1,95 @@
import { FC } from "react";
import { Controller, useForm } from "react-hook-form";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IFormattedInstanceConfiguration } from "types/instance";
// hooks
import useToast from "hooks/use-toast";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
export interface IInstanceImageConfigForm {
config: IFormattedInstanceConfiguration;
}
export interface ImageConfigFormValues {
UNSPLASH_ACCESS_KEY: string;
}
export const InstanceImageConfigForm: FC<IInstanceImageConfigForm> = (props) => {
const { config } = props;
// store
const { instance: instanceStore } = useMobxStore();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<ImageConfigFormValues>({
defaultValues: {
UNSPLASH_ACCESS_KEY: config["UNSPLASH_ACCESS_KEY"],
},
});
const onSubmit = async (formData: ImageConfigFormValues) => {
const payload: Partial<ImageConfigFormValues> = { ...formData };
await instanceStore
.updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Image Configuration Settings updated successfully",
})
)
.catch((err) => console.error(err));
};
return (
<>
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Access key from your Unsplash account</h4>
<Controller
control={control}
name="UNSPLASH_ACCESS_KEY"
render={({ field: { value, onChange, ref } }) => (
<Input
id="UNSPLASH_ACCESS_KEY"
name="UNSPLASH_ACCESS_KEY"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.UNSPLASH_ACCESS_KEY)}
placeholder="oXgq-sdfadsaeweqasdfasdf3234234rassd"
className="rounded-md font-medium w-full"
/>
)}
/>
<p className="text-xs text-custom-text-400">
You will find your access key in your Unsplash developer console.{" "}
<a
href="https://unsplash.com/documentation#creating-a-developer-account"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more.
</a>
</p>
</div>
</div>
<div className="flex items-center py-1">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"}
</Button>
</div>
</>
);
};

View File

@ -3,9 +3,11 @@ import { useRouter } from "next/router";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
import { Menu, Transition } from "@headlessui/react";
import { Cog, LogIn, LogOut, Settings } from "lucide-react";
import { mutate } from "swr"; import { mutate } from "swr";
// components
import { Menu, Transition } from "@headlessui/react";
// icons
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
@ -64,28 +66,21 @@ export const InstanceSidebarDropdown = observer(() => {
}; };
return ( return (
<div className="flex items-center gap-x-3 gap-y-2 px-4 py-4"> <div className="flex items-center gap-x-2 gap-y-2 px-4 pt-3 pb-2 mb-2 border border-custom-sidebar-border-200">
<div className="w-full h-full truncate"> <div className="w-full h-full truncate">
<div <div
className={`flex flex-grow items-center gap-x-2 rounded p-1 truncate ${ className={`flex flex-grow items-center gap-x-2 rounded p-1 truncate ${
sidebarCollapsed ? "justify-center" : "" sidebarCollapsed ? "justify-center" : ""
}`} }`}
> >
<div <div className={`flex-shrink-0 flex items-center justify-center h-7 w-7 rounded bg-custom-sidebar-background-80`}>
className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`} <UserCog2 className="h-6 w-6 text-custom-text-200" />
>
<Cog className="h-5 w-5 text-custom-text-200" />
</div>
{!sidebarCollapsed && <h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>}
</div>
</div> </div>
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<Menu as="div" className="relative flex-shrink-0"> <div className="flex w-full gap-2">
<Menu.Button className="flex gap-4 place-items-center outline-none"> <h4 className="grow text-custom-text-200 font-medium text-base truncate">God Mode</h4>
{!sidebarCollapsed && ( <Tooltip position="bottom-left" tooltipContent="Exit God Mode">
<Tooltip position="bottom-left" tooltipContent="Go back to your workspace">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Link href={`/${redirectWorkspaceSlug}`}> <Link href={`/${redirectWorkspaceSlug}`}>
<a> <a>
@ -94,7 +89,14 @@ export const InstanceSidebarDropdown = observer(() => {
</Link> </Link>
</div> </div>
</Tooltip> </Tooltip>
</div>
)} )}
</div>
</div>
{!sidebarCollapsed && (
<Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="grid place-items-center outline-none">
<Avatar <Avatar
name={currentUser?.display_name} name={currentUser?.display_name}
src={currentUser?.avatar} src={currentUser?.avatar}
@ -114,8 +116,8 @@ export const InstanceSidebarDropdown = observer(() => {
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items <Menu.Items
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 origin-top-left rounded-md className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 rounded-md
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none" border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-100 shadow-lg text-xs outline-none"
> >
<div className="flex flex-col gap-2.5 pb-2"> <div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span> <span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
@ -145,8 +147,8 @@ export const InstanceSidebarDropdown = observer(() => {
<div className="p-2 pb-0"> <div className="p-2 pb-0">
<Menu.Item as="button" type="button" className="w-full"> <Menu.Item as="button" type="button" className="w-full">
<Link href={`/${redirectWorkspaceSlug}`}> <Link href={`/${redirectWorkspaceSlug}`}>
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-10 hover:bg-custom-primary-20"> <a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-100/20 hover:bg-custom-primary-100/30">
Normal Mode Exit God Mode
</a> </a>
</Link> </Link>
</Menu.Item> </Menu.Item>

View File

@ -1,7 +1,7 @@
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// icons // icons
import { BrainCog, Cog, Lock, Mail } from "lucide-react"; import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// ui // ui
@ -11,26 +11,32 @@ const INSTANCE_ADMIN_LINKS = [
{ {
Icon: Cog, Icon: Cog,
name: "General", name: "General",
description: "General settings here", description: "Identify your instances and get key details",
href: `/god-mode`, href: `/god-mode`,
}, },
{ {
Icon: Mail, Icon: Mail,
name: "Email", name: "Email",
description: "Email related settings will go here", description: "Set up emails to your users",
href: `/god-mode/email`, href: `/god-mode/email`,
}, },
{ {
Icon: Lock, Icon: Lock,
name: "Authorization", name: "SSO and OAuth",
description: "Autorization", description: "Configure your Google and GitHub SSOs",
href: `/god-mode/authorization`, href: `/god-mode/authorization`,
}, },
{ {
Icon: BrainCog, Icon: BrainCog,
name: "OpenAI", name: "Artificial intelligence",
description: "OpenAI configurations", description: "Configure your OpenAI creds",
href: `/god-mode/openai`, href: `/god-mode/ai`,
},
{
Icon: Image,
name: "Images in Plane",
description: "Allow third-party image libraries",
href: `/god-mode/image`,
}, },
]; ];
@ -68,7 +74,9 @@ export const InstanceAdminSidebarMenu = () => {
{item.name} {item.name}
</span> </span>
<span <span
className={`text-xs ${isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-300"}`} className={`text-[10px] ${
isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-300"
}`}
> >
{item.description} {item.description}
</span> </span>

View File

@ -303,8 +303,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
<div className="p-2 pb-0"> <div className="p-2 pb-0">
<Menu.Item as="button" type="button" className="w-full"> <Menu.Item as="button" type="button" className="w-full">
<Link href="/god-mode"> <Link href="/god-mode">
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-10 hover:bg-custom-primary-20"> <a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-100/20 hover:bg-custom-primary-100/30">
God Mode Enter God Mode
</a> </a>
</Link> </Link>
</Menu.Item> </Menu.Item>

View File

@ -7,15 +7,16 @@ import { Breadcrumbs } from "@plane/ui";
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
export interface IInstanceAdminHeader { export interface IInstanceAdminHeader {
title: string; title?: string;
} }
export const InstanceAdminHeader: FC<IInstanceAdminHeader> = observer((props) => { export const InstanceAdminHeader: FC<IInstanceAdminHeader> = observer((props) => {
const { title } = props; const { title } = props;
return ( return (
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.4rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis"> <div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
{title && (
<div> <div>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
@ -27,6 +28,7 @@ export const InstanceAdminHeader: FC<IInstanceAdminHeader> = observer((props) =>
<Breadcrumbs.BreadcrumbItem type="text" label={title} /> <Breadcrumbs.BreadcrumbItem type="text" label={title} />
</Breadcrumbs> </Breadcrumbs>
</div> </div>
)}
</div> </div>
</div> </div>
); );

View File

@ -3,14 +3,14 @@ import { FC, ReactNode } from "react";
import { AdminAuthWrapper, UserAuthWrapper } from "layouts/auth-layout"; import { AdminAuthWrapper, UserAuthWrapper } from "layouts/auth-layout";
// components // components
import { InstanceAdminSidebar } from "./sidebar"; import { InstanceAdminSidebar } from "./sidebar";
import { InstanceAdminHeader } from "./header";
export interface IInstanceAdminLayout { export interface IInstanceAdminLayout {
children: ReactNode; children: ReactNode;
header: ReactNode;
} }
export const InstanceAdminLayout: FC<IInstanceAdminLayout> = (props) => { export const InstanceAdminLayout: FC<IInstanceAdminLayout> = (props) => {
const { children, header } = props; const { children } = props;
return ( return (
<> <>
@ -19,7 +19,7 @@ export const InstanceAdminLayout: FC<IInstanceAdminLayout> = (props) => {
<div className="relative flex h-screen w-full overflow-hidden"> <div className="relative flex h-screen w-full overflow-hidden">
<InstanceAdminSidebar /> <InstanceAdminSidebar />
<main className="relative flex flex-col h-full w-full overflow-hidden bg-custom-background-100"> <main className="relative flex flex-col h-full w-full overflow-hidden bg-custom-background-100">
{header} <InstanceAdminHeader />
<div className="h-full w-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll"> <div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
<>{children}</> <>{children}</>

View File

@ -33,18 +33,56 @@ export const AdminAuthWrapper: FC<IAdminAuthWrapper> = observer(({ children }) =
// if user does not have admin access to the instance // if user does not have admin access to the instance
if (isUserInstanceAdmin !== undefined && isUserInstanceAdmin === false) { if (isUserInstanceAdmin !== undefined && isUserInstanceAdmin === false) {
return ( return (
<div className={`h-screen w-full flex items-center justify-center overflow-hidden`}> <div className={`my-8 w-full flex flex-col gap-4 items-center justify-center overflow-hidden`}>
<div className="w-3/5 h-2/3 bg-custom-background-90"> <div className="w-3/5 bg-custom-background-90">
<div className="grid h-full place-items-center p-4"> <div className="grid h-full place-items-center p-2 pb-0">
<div className="space-y-8 text-center"> <div className="text-center">
<div className="space-y-2"> <Image src={AccessDeniedImg} height="250" width="550" alt="AccessDeniedImg" />
<Image src={AccessDeniedImg} height="220" width="550" alt="AccessDeniedImg" /> <h3 className="text-3xl font-semibold">God mode needs a god role</h3>
<h3 className="text-3xl font-semibold">Access denied!</h3> <p className="text-base text-custom-text-300">Doesnt look like you have that role.</p>
<div className="mx-auto text-base text-custom-text-100"> </div>
<p>Sorry, but you do not have permission to view this page.</p> <div className="flex flex-col gap-2 my-8 text-center">
<p> <div>
If you think there{""}s a mistake contact <span className="font-semibold">support.</span> <p className="font-medium text-xs text-custom-text-400 tracking-tight">Do we have a god role?</p>
<p className="text-custom-text-300 text-sm">Yes.</p>
</div>
<div>
<p className="font-medium text-xs text-custom-text-400 tracking-tight">Do we call it god role?</p>
<p className="text-custom-text-300 text-sm">No. Obviously not.</p>
</div>
<div>
<p className="font-medium text-xs text-custom-text-400 tracking-tight">Can you get it?</p>
<p className="text-custom-text-300 text-sm">Maybe. Ask your god.</p>
</div>
<div>
<p className="font-medium text-xs text-custom-text-400 tracking-tight">
Are we being intentionally cryptic?
</p> </p>
<p className="text-custom-text-300 text-sm">Yes.</p>
</div>
<div>
<p className="font-medium text-xs text-custom-text-400 tracking-tight">
Is this for the security of your workspaces?
</p>
<p className="text-custom-text-300 text-sm">Absolutely!</p>
</div>
<div>
<p className="font-medium text-xs text-custom-text-400 tracking-tight">
Are you the god here and still seeing this?
</p>
<p className="text-custom-text-300 text-sm">
Sorry, God.{" "}
<a
href="https://discord.com/channels/1031547764020084846/1094927053867995176"
target="_blank"
className="text-custom-primary-100 font-medium hover:underline"
rel="noreferrer"
>
Talk to us.
</a>
</p>
</div>
</div>
</div> </div>
</div> </div>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
@ -52,15 +90,12 @@ export const AdminAuthWrapper: FC<IAdminAuthWrapper> = observer(({ children }) =
<a> <a>
<Button variant="primary" size="sm"> <Button variant="primary" size="sm">
<LayoutGrid width={16} height={16} /> <LayoutGrid width={16} height={16} />
Back to Dashboard To the workspace
</Button> </Button>
</a> </a>
</Link> </Link>
</div> </div>
</div> </div>
</div>
</div>
</div>
); );
} }

View File

@ -41,7 +41,7 @@ export const ProfileLayoutSidebar = observer(() => {
const { const {
theme: { sidebarCollapsed, toggleSidebar }, theme: { sidebarCollapsed, toggleSidebar },
workspace: { workspaces }, workspace: { workspaces },
user: { currentUser, currentUserSettings }, user: { currentUser, currentUserSettings, isUserInstanceAdmin },
} = useMobxStore(); } = useMobxStore();
// redirect url for normal mode // redirect url for normal mode
@ -135,6 +135,17 @@ export const ProfileLayoutSidebar = observer(() => {
<LogOut className="h-4 w-4 stroke-[1.5]" /> <LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out Sign out
</Menu.Item> </Menu.Item>
{isUserInstanceAdmin && (
<div className="p-2 pb-0 border-t border-custom-border-100">
<Menu.Item as="button" type="button" className="w-full">
<Link href="/god-mode">
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-100/20 hover:bg-custom-primary-100/30">
Enter God Mode
</a>
</Link>
</Menu.Item>
</div>
)}
</Menu.Items> </Menu.Items>
</Transition> </Transition>
</Menu> </Menu>

62
web/pages/god-mode/ai.tsx Normal file
View File

@ -0,0 +1,62 @@
import { ReactElement } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
import { InstanceAdminLayout } from "layouts/admin-layout";
// types
import { NextPageWithLayout } from "types/app";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { Loader } from "@plane/ui";
// icons
import { Lightbulb } from "lucide-react";
// components
import { InstanceAIForm } from "components/instance/ai-form";
const InstanceAdminAIPage: NextPageWithLayout = observer(() => {
// store
const {
instance: { fetchInstanceConfigurations, formattedConfig },
} = useMobxStore();
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return (
<div className="flex flex-col gap-8 my-8 mx-12">
<div className="pb-3 mb-2 border-b border-custom-border-100">
<div className="text-custom-text-100 font-medium text-xl pb-1">AI features for all your workspaces</div>
<div className="text-custom-text-300 font-normal text-sm">
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
</div>
</div>
{formattedConfig ? (
<>
<div>
<div className="text-custom-text-100 font-medium text-xl pb-1">OpenAI</div>
<div className="text-custom-text-300 font-normal text-sm">If you use ChatGPT, this is for you.</div>
</div>
<InstanceAIForm config={formattedConfig} />
<div className="flex my-2">
<div className="flex items-center gap-2 px-4 py-2 text-xs text-custom-primary-200 bg-custom-primary-100/10 border border-custom-primary-100/20 rounded">
<Lightbulb height="14" width="14" />
<div>If you have a preferred AI models vendor, please get in touch with us.</div>
</div>
</div>
</>
) : (
<Loader className="space-y-4">
<Loader.Item height="50px" width="50%" />
<Loader.Item height="50px" width="50%" />
<Loader.Item height="50px" width="25%" />
</Loader>
)}
</div>
);
});
InstanceAdminAIPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
};
export default InstanceAdminAIPage;

View File

@ -1,19 +1,17 @@
import { ReactElement, useState } from "react"; import { ReactElement, useState } from "react";
import Link from "next/link";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// layouts // layouts
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout"; import { InstanceAdminLayout } from "layouts/admin-layout";
// types // types
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
// store // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// icons
import { ChevronDown, ChevronRight } from "lucide-react";
// ui // ui
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch } from "@plane/ui";
import { Disclosure, Transition } from "@headlessui/react";
// components // components
import { InstanceGoogleConfigForm } from "components/instance/google-config-form"; import { InstanceGoogleConfigForm } from "components/instance/google-config-form";
import { InstanceGithubConfigForm } from "components/instance/github-config-form"; import { InstanceGithubConfigForm } from "components/instance/github-config-form";
@ -33,12 +31,17 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const enableSignup = formattedConfig?.ENABLE_SIGNUP ?? "0"; const enableSignup = formattedConfig?.ENABLE_SIGNUP ?? "0";
const enableMagicLogin = formattedConfig?.ENABLE_MAGIC_LINK_LOGIN ?? "0";
// const enableEmailPassword = formattedConfig?.ENABLE_EMAIL_PASSWORD ?? "0";
const updateConfig = async (value: string) => { const updateConfig = async (
key: "ENABLE_SIGNUP" | "ENABLE_MAGIC_LINK_LOGIN" | "ENABLE_EMAIL_PASSWORD",
value: string
) => {
setIsSubmitting(true); setIsSubmitting(true);
const payload = { const payload = {
ENABLE_SIGNUP: value, [key]: value,
}; };
await updateInstanceConfigurations(payload) await updateInstanceConfigurations(payload)
@ -46,7 +49,7 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
setToastAlert({ setToastAlert({
title: "Success", title: "Success",
type: "success", type: "success",
message: "Authorization Settings updated successfully", message: "SSO and OAuth Settings updated successfully",
}); });
setIsSubmitting(false); setIsSubmitting(false);
}) })
@ -55,102 +58,121 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
setToastAlert({ setToastAlert({
title: "Error", title: "Error",
type: "error", type: "error",
message: "Failed to update Authorization Settings", message: "Failed to update SSO and OAuth Settings",
}); });
setIsSubmitting(false); setIsSubmitting(false);
}); });
}; };
return ( return (
<div> <div className="flex flex-col gap-8 my-8 mx-12">
{formattedConfig ? ( <div className="pb-3 mb-2 border-b border-custom-border-100">
<div className="flex flex-col gap-8 m-8 w-4/5"> <div className="text-custom-text-100 font-medium text-xl pb-1">Single sign-on and OAuth</div>
<div className="pb-2 mb-2 border-b border-custom-border-100">
<div className="text-custom-text-100 font-medium text-lg">Authorization</div>
<div className="text-custom-text-300 font-normal text-sm"> <div className="text-custom-text-300 font-normal text-sm">
Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the
settings. settings.
</div> </div>
</div> </div>
<div className="flex items-center gap-8 pb-4 border-b border-custom-border-100"> {formattedConfig ? (
<div> <>
<div className="text-custom-text-100 font-medium text-sm">Enable sign-up</div> <div className="flex flex-col gap-12 w-full lg:w-2/5 pb-8 border-b border-custom-border-100">
<div className="flex items-center gap-14 mr-4">
<div className="grow">
<div className="text-custom-text-100 font-medium text-sm">
Turn Magic Links {Boolean(parseInt(enableMagicLogin)) ? "off" : "on"}
</div>
<div className="text-custom-text-300 font-normal text-xs"> <div className="text-custom-text-300 font-normal text-xs">
Keep the doors open so people can join your workspaces. <p>Slack-like emails for authentication.</p>
You need to have set up email{" "}
<Link href="email">
<a className="text-custom-primary-100 hover:underline">here</a>
</Link>{" "}
to enable this.
</div> </div>
</div> </div>
<div className={isSubmitting ? "opacity-70" : ""}> <div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
<ToggleSwitch <ToggleSwitch
value={Boolean(parseInt(enableSignup))} value={Boolean(parseInt(enableMagicLogin))}
onChange={() => { onChange={() => {
Boolean(parseInt(enableSignup)) === true ? updateConfig("0") : updateConfig("1"); Boolean(parseInt(enableMagicLogin)) === true
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
}} }}
size="sm" size="sm"
disabled={isSubmitting} disabled={isSubmitting}
/> />
</div> </div>
</div> </div>
<div className="flex items-center gap-14 mr-4">
<div className="grow">
<div className="text-custom-text-100 font-medium text-sm">
Let your users log in via the methods below
</div>
<div className="text-custom-text-300 font-normal text-xs">
Toggling this off will disable all previous configs. Users will only be able to login with an e-mail
and password combo.
</div>
</div>
<div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
<ToggleSwitch
value={Boolean(parseInt(enableSignup))}
onChange={() => {
Boolean(parseInt(enableSignup)) === true
? updateConfig("ENABLE_SIGNUP", "0")
: updateConfig("ENABLE_SIGNUP", "1");
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
{/* <div className="flex items-center gap-14 mr-4">
<div className="grow">
<div className="text-custom-text-100 font-medium text-sm">
Turn Email Password {Boolean(parseInt(enableEmailPassword)) ? "off" : "on"}
</div>
<div className="text-custom-text-300 font-normal text-xs">UX Copy Required!</div>
</div>
<div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
<ToggleSwitch
value={Boolean(parseInt(enableEmailPassword))}
onChange={() => {
Boolean(parseInt(enableEmailPassword)) === true
? updateConfig("ENABLE_EMAIL_PASSWORD", "0")
: updateConfig("ENABLE_EMAIL_PASSWORD", "1");
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div> */}
</div>
<div className="flex flex-col gap-y-6 py-2"> <div className="flex flex-col gap-y-6 py-2">
<Disclosure as="div">
{({ open }) => (
<div className="w-full"> <div className="w-full">
<Disclosure.Button <div className="flex items-center justify-between py-2 border-b border-custom-border-100">
as="button"
type="button"
className="flex items-center justify-between w-full py-2 border-b border-custom-border-100"
>
<span className="text-lg font-medium tracking-tight">Google</span> <span className="text-lg font-medium tracking-tight">Google</span>
{open ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />} </div>
</Disclosure.Button> <div className="px-2 py-6">
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel className="flex flex-col gap-8 px-2 py-8">
<InstanceGoogleConfigForm config={formattedConfig} /> <InstanceGoogleConfigForm config={formattedConfig} />
</Disclosure.Panel>
</Transition>
</div> </div>
)} </div>
</Disclosure>
<Disclosure as="div">
{({ open }) => (
<div className="w-full"> <div className="w-full">
<Disclosure.Button <div className="flex items-center justify-between py-2 border-b border-custom-border-100">
as="button"
type="button"
className="flex items-center justify-between w-full py-2 border-b border-custom-border-100"
>
<span className="text-lg font-medium tracking-tight">Github</span> <span className="text-lg font-medium tracking-tight">Github</span>
{open ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />} </div>
</Disclosure.Button> <div className="px-2 py-6">
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel className="flex flex-col gap-8 px-2 py-8">
<InstanceGithubConfigForm config={formattedConfig} /> <InstanceGithubConfigForm config={formattedConfig} />
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div> </div>
</div> </div>
</div>
</>
) : ( ) : (
<Loader className="space-y-4 m-8"> <Loader className="space-y-4">
<Loader.Item height="50px" /> <Loader.Item height="50px" width="50%" />
<Loader.Item height="50px" width="50%" />
<Loader.Item height="50px" width="50%" />
<Loader.Item height="50px" /> <Loader.Item height="50px" />
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" /> <Loader.Item height="50px" />
<Loader.Item height="50px" width="25%" /> <Loader.Item height="50px" width="25%" />
</Loader> </Loader>
@ -160,7 +182,7 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
}); });
InstanceAdminAuthorizationPage.getLayout = function getLayout(page: ReactElement) { InstanceAdminAuthorizationPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout header={<InstanceAdminHeader title="Authorization" />}>{page}</InstanceAdminLayout>; return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
}; };
export default InstanceAdminAuthorizationPage; export default InstanceAdminAuthorizationPage;

View File

@ -2,7 +2,7 @@ import { ReactElement } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// layouts // layouts
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout"; import { InstanceAdminLayout } from "layouts/admin-layout";
// types // types
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
// store // store
@ -21,11 +21,21 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return ( return (
<div> <div className="flex flex-col gap-8 my-8 mx-12 w-4/5">
<div className="pb-3 mb-2 border-b border-custom-border-100">
<div className="text-custom-text-100 font-medium text-xl pb-1">Secure emails from your own instance</div>
<div className="text-custom-text-300 font-normal text-sm">
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
</div>
<div className="text-custom-text-300 font-normal text-sm">
Set it up below and please test your settings before you save them.{" "}
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
</div>
</div>
{formattedConfig ? ( {formattedConfig ? (
<InstanceEmailForm config={formattedConfig} /> <InstanceEmailForm config={formattedConfig} />
) : ( ) : (
<Loader className="space-y-4 m-8"> <Loader className="space-y-4">
<Loader.Item height="50px" /> <Loader.Item height="50px" />
<Loader.Item height="50px" /> <Loader.Item height="50px" />
<Loader.Item height="50px" width="25%" /> <Loader.Item height="50px" width="25%" />
@ -38,7 +48,7 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
}); });
InstanceAdminEmailPage.getLayout = function getLayout(page: ReactElement) { InstanceAdminEmailPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout header={<InstanceAdminHeader title="Email" />}>{page}</InstanceAdminLayout>; return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
}; };
export default InstanceAdminEmailPage; export default InstanceAdminEmailPage;

View File

@ -0,0 +1,47 @@
import { ReactElement } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
import { InstanceAdminLayout } from "layouts/admin-layout";
// types
import { NextPageWithLayout } from "types/app";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { Loader } from "@plane/ui";
// components
import { InstanceImageConfigForm } from "components/instance/image-config-form";
const InstanceAdminImagePage: NextPageWithLayout = observer(() => {
// store
const {
instance: { fetchInstanceConfigurations, formattedConfig },
} = useMobxStore();
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return (
<div className="flex flex-col gap-8 my-8 mx-12 w-4/5">
<div className="pb-3 mb-2 border-b border-custom-border-100">
<div className="text-custom-text-100 font-medium text-xl pb-1">Third-party image libraries</div>
<div className="text-custom-text-300 font-normal text-sm">
Let your users search and choose images from third-party libraries
</div>
</div>
{formattedConfig ? (
<InstanceImageConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" width="25%" />
</Loader>
)}
</div>
);
});
InstanceAdminImagePage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
};
export default InstanceAdminImagePage;

View File

@ -2,7 +2,7 @@ import { ReactElement } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// layouts // layouts
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout"; import { InstanceAdminLayout } from "layouts/admin-layout";
// types // types
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
// store // store
@ -21,11 +21,18 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_INFO", () => fetchInstanceInfo()); useSWR("INSTANCE_INFO", () => fetchInstanceInfo());
return ( return (
<div> <div className="flex flex-col gap-8 my-8 mx-12">
<div className="pb-3 mb-2 border-b border-custom-border-100">
<div className="text-custom-text-100 font-medium text-xl pb-1">ID your instance easily</div>
<div className="text-custom-text-300 font-normal text-sm">
Change the name of your instance and instance admin e-mail addresses. If you have a paid subscription, you
will find your license key here.
</div>
</div>
{instance ? ( {instance ? (
<InstanceGeneralForm instance={instance} /> <InstanceGeneralForm instance={instance} />
) : ( ) : (
<Loader className="space-y-4 m-8"> <Loader className="space-y-4">
<Loader.Item height="50px" /> <Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" /> <Loader.Item height="50px" width="50%" />
<Loader.Item height="50px" width="25%" /> <Loader.Item height="50px" width="25%" />
@ -36,7 +43,7 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
}); });
InstanceAdminPage.getLayout = function getLayout(page: ReactElement) { InstanceAdminPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout header={<InstanceAdminHeader title="General" />}>{page}</InstanceAdminLayout>; return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
}; };
export default InstanceAdminPage; export default InstanceAdminPage;

View File

@ -1,42 +0,0 @@
import { ReactElement } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout";
// types
import { NextPageWithLayout } from "types/app";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { Loader } from "@plane/ui";
// components
import { InstanceOpenAIForm } from "components/instance/openai-form";
const InstanceAdminOpenAIPage: NextPageWithLayout = observer(() => {
// store
const {
instance: { fetchInstanceConfigurations, formattedConfig },
} = useMobxStore();
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return (
<div>
{formattedConfig ? (
<InstanceOpenAIForm config={formattedConfig} />
) : (
<Loader className="space-y-4 m-8">
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
<Loader.Item height="50px" width="25%" />
</Loader>
)}
</div>
);
});
InstanceAdminOpenAIPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout header={<InstanceAdminHeader title="OpenAI" />}>{page}</InstanceAdminLayout>;
};
export default InstanceAdminOpenAIPage;