forked from github/plane
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:
parent
5c6a59ba35
commit
2980c7b00d
@ -9,17 +9,16 @@ import useToast from "hooks/use-toast";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export interface IInstanceOpenAIForm {
|
||||
export interface IInstanceAIForm {
|
||||
config: IFormattedInstanceConfiguration;
|
||||
}
|
||||
|
||||
export interface OpenAIFormValues {
|
||||
OPENAI_API_BASE: string;
|
||||
export interface AIFormValues {
|
||||
OPENAI_API_KEY: string;
|
||||
GPT_ENGINE: string;
|
||||
}
|
||||
|
||||
export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
|
||||
const { config } = props;
|
||||
// store
|
||||
const { instance: instanceStore } = useMobxStore();
|
||||
@ -30,16 +29,15 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<OpenAIFormValues>({
|
||||
} = useForm<AIFormValues>({
|
||||
defaultValues: {
|
||||
OPENAI_API_BASE: config["OPENAI_API_BASE"],
|
||||
OPENAI_API_KEY: config["OPENAI_API_KEY"],
|
||||
GPT_ENGINE: config["GPT_ENGINE"],
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: OpenAIFormValues) => {
|
||||
const payload: Partial<OpenAIFormValues> = { ...formData };
|
||||
const onSubmit = async (formData: AIFormValues) => {
|
||||
const payload: Partial<AIFormValues> = { ...formData };
|
||||
|
||||
await instanceStore
|
||||
.updateInstanceConfigurations(payload)
|
||||
@ -47,64 +45,15 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Open AI Settings updated successfully",
|
||||
message: "AI Settings updated successfully",
|
||||
})
|
||||
)
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
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">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="grid grid-col grid-cols-1 lg:grid-cols-3 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">GPT Engine</h4>
|
||||
<Controller
|
||||
@ -119,11 +68,54 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GPT_ENGINE)}
|
||||
placeholder="GPT Engine"
|
||||
placeholder="gpt-3.5-turbo"
|
||||
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>
|
||||
|
||||
@ -132,6 +124,6 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -31,6 +31,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
// form data
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<EmailFormValues>({
|
||||
@ -60,11 +61,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
};
|
||||
|
||||
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="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Host</h4>
|
||||
@ -80,7 +77,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.EMAIL_HOST)}
|
||||
placeholder="Email Host"
|
||||
placeholder="email.google.com"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
@ -101,7 +98,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.EMAIL_PORT)}
|
||||
placeholder="Email Port"
|
||||
placeholder="8080"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
@ -123,7 +120,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.EMAIL_HOST_USER)}
|
||||
placeholder="Username"
|
||||
placeholder="getitdone@projectplane.so"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
@ -139,7 +136,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
<Input
|
||||
id="EMAIL_HOST_PASSWORD"
|
||||
name="EMAIL_HOST_PASSWORD"
|
||||
type="text"
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
@ -152,11 +149,15 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8 pt-4">
|
||||
<div>
|
||||
<div className="text-custom-text-100 font-medium text-sm">Enable TLS</div>
|
||||
<div className="w-full lg:w-1/2 flex flex-col px-1 gap-y-8">
|
||||
<div className="flex items-center gap-8 pt-4 mr-8">
|
||||
<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 className="text-custom-text-300 font-normal text-xs">Use this if your email domain supports TLS.</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Controller
|
||||
control={control}
|
||||
name="EMAIL_USE_TLS"
|
||||
@ -173,11 +174,16 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8 pt-4">
|
||||
<div>
|
||||
<div className="text-custom-text-100 font-medium text-sm">Enable SSL</div>
|
||||
<div className="flex items-center gap-8 pt-4 mr-8">
|
||||
<div className="grow">
|
||||
<div className="text-custom-text-100 font-medium text-sm">
|
||||
Turn SSL {Boolean(parseInt(watch("EMAIL_USE_SSL"))) ? "off" : "on"}
|
||||
</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
|
||||
control={control}
|
||||
name="EMAIL_USE_SSL"
|
||||
@ -193,12 +199,13 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center py-1">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ export interface IInstanceGeneralForm {
|
||||
|
||||
export interface GeneralFormValues {
|
||||
instance_name: string;
|
||||
is_telemetry_enabled: boolean;
|
||||
// is_telemetry_enabled: boolean;
|
||||
}
|
||||
|
||||
export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
@ -31,7 +31,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
} = useForm<GeneralFormValues>({
|
||||
defaultValues: {
|
||||
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 (
|
||||
<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="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Name of instance</h4>
|
||||
@ -106,7 +100,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8 pt-4">
|
||||
{/* <div className="flex items-center gap-12 pt-4">
|
||||
<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">
|
||||
@ -120,13 +114,13 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
render={({ field: { value, onChange } }) => <ToggleSwitch value={value} onChange={onChange} size="sm" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="flex items-center py-1">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -56,8 +56,8 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
||||
const originURL = typeof window !== "undefined" ? window.location.origin : "";
|
||||
|
||||
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-8">
|
||||
<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">
|
||||
<h4 className="text-sm">Client ID</h4>
|
||||
<Controller
|
||||
@ -72,11 +72,22 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GITHUB_CLIENT_ID)}
|
||||
placeholder="Github Client ID"
|
||||
placeholder="70a44354520df8bd9bcd"
|
||||
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 className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Client Secret</h4>
|
||||
@ -92,14 +103,23 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GITHUB_CLIENT_SECRET)}
|
||||
placeholder="Github Client Secret"
|
||||
placeholder="9b0050f94ec1b744e32ce79ea4ffacd40d4119cb"
|
||||
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 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">Origin URL</h4>
|
||||
<Button
|
||||
@ -117,16 +137,26 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
||||
<p className="font-medium text-sm">{originURL}</p>
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
</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 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}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -56,8 +56,8 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
||||
const originURL = typeof window !== "undefined" ? window.location.origin : "";
|
||||
|
||||
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-8">
|
||||
<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">
|
||||
<h4 className="text-sm">Client ID</h4>
|
||||
<Controller
|
||||
@ -72,11 +72,22 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GOOGLE_CLIENT_ID)}
|
||||
placeholder="Google Client ID"
|
||||
placeholder="840195096245-0p2tstej9j5nc4l8o1ah2dqondscqc1g.apps.googleusercontent.com"
|
||||
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 className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Client Secret</h4>
|
||||
@ -92,14 +103,23 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GOOGLE_CLIENT_SECRET)}
|
||||
placeholder="Google Client Secret"
|
||||
placeholder="GOCShX-ADp4cI0kPqav1gGCBg5bE02E"
|
||||
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 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">Origin URL</h4>
|
||||
<Button
|
||||
@ -117,16 +137,26 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
||||
<p className="font-medium text-sm">{originURL}</p>
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
</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 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}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
95
web/components/instance/image-config-form.tsx
Normal file
95
web/components/instance/image-config-form.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -3,9 +3,11 @@ import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Cog, LogIn, LogOut, Settings } from "lucide-react";
|
||||
import { mutate } from "swr";
|
||||
// components
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
@ -64,28 +66,21 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
};
|
||||
|
||||
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={`flex flex-grow items-center gap-x-2 rounded p-1 truncate ${
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`}
|
||||
>
|
||||
<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 className={`flex-shrink-0 flex items-center justify-center h-7 w-7 rounded bg-custom-sidebar-background-80`}>
|
||||
<UserCog2 className="h-6 w-6 text-custom-text-200" />
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
<Menu.Button className="flex gap-4 place-items-center outline-none">
|
||||
{!sidebarCollapsed && (
|
||||
<Tooltip position="bottom-left" tooltipContent="Go back to your workspace">
|
||||
<div className="flex w-full gap-2">
|
||||
<h4 className="grow text-custom-text-200 font-medium text-base truncate">God Mode</h4>
|
||||
<Tooltip position="bottom-left" tooltipContent="Exit God Mode">
|
||||
<div className="flex-shrink-0">
|
||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||
<a>
|
||||
@ -94,7 +89,14 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
</Link>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
<Menu.Button className="grid place-items-center outline-none">
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={currentUser?.avatar}
|
||||
@ -114,8 +116,8 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 origin-top-left 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"
|
||||
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-100 shadow-lg text-xs outline-none"
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<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">
|
||||
<Menu.Item as="button" type="button" className="w-full">
|
||||
<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">
|
||||
Normal 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">
|
||||
Exit God Mode
|
||||
</a>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
@ -11,26 +11,32 @@ const INSTANCE_ADMIN_LINKS = [
|
||||
{
|
||||
Icon: Cog,
|
||||
name: "General",
|
||||
description: "General settings here",
|
||||
description: "Identify your instances and get key details",
|
||||
href: `/god-mode`,
|
||||
},
|
||||
{
|
||||
Icon: Mail,
|
||||
name: "Email",
|
||||
description: "Email related settings will go here",
|
||||
description: "Set up emails to your users",
|
||||
href: `/god-mode/email`,
|
||||
},
|
||||
{
|
||||
Icon: Lock,
|
||||
name: "Authorization",
|
||||
description: "Autorization",
|
||||
name: "SSO and OAuth",
|
||||
description: "Configure your Google and GitHub SSOs",
|
||||
href: `/god-mode/authorization`,
|
||||
},
|
||||
{
|
||||
Icon: BrainCog,
|
||||
name: "OpenAI",
|
||||
description: "OpenAI configurations",
|
||||
href: `/god-mode/openai`,
|
||||
name: "Artificial intelligence",
|
||||
description: "Configure your OpenAI creds",
|
||||
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}
|
||||
</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}
|
||||
</span>
|
||||
|
@ -303,8 +303,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
<div className="p-2 pb-0">
|
||||
<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-10 hover:bg-custom-primary-20">
|
||||
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>
|
||||
|
@ -7,15 +7,16 @@ import { Breadcrumbs } from "@plane/ui";
|
||||
import { Settings } from "lucide-react";
|
||||
|
||||
export interface IInstanceAdminHeader {
|
||||
title: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const InstanceAdminHeader: FC<IInstanceAdminHeader> = observer((props) => {
|
||||
const { title } = props;
|
||||
|
||||
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">
|
||||
{title && (
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
@ -27,6 +28,7 @@ export const InstanceAdminHeader: FC<IInstanceAdminHeader> = observer((props) =>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -3,14 +3,14 @@ import { FC, ReactNode } from "react";
|
||||
import { AdminAuthWrapper, UserAuthWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
import { InstanceAdminSidebar } from "./sidebar";
|
||||
import { InstanceAdminHeader } from "./header";
|
||||
|
||||
export interface IInstanceAdminLayout {
|
||||
children: ReactNode;
|
||||
header: ReactNode;
|
||||
}
|
||||
|
||||
export const InstanceAdminLayout: FC<IInstanceAdminLayout> = (props) => {
|
||||
const { children, header } = props;
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -19,7 +19,7 @@ export const InstanceAdminLayout: FC<IInstanceAdminLayout> = (props) => {
|
||||
<div className="relative flex h-screen w-full overflow-hidden">
|
||||
<InstanceAdminSidebar />
|
||||
<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="relative h-full w-full overflow-x-hidden overflow-y-scroll">
|
||||
<>{children}</>
|
||||
|
@ -33,18 +33,56 @@ export const AdminAuthWrapper: FC<IAdminAuthWrapper> = observer(({ children }) =
|
||||
// if user does not have admin access to the instance
|
||||
if (isUserInstanceAdmin !== undefined && isUserInstanceAdmin === false) {
|
||||
return (
|
||||
<div className={`h-screen w-full flex items-center justify-center overflow-hidden`}>
|
||||
<div className="w-3/5 h-2/3 bg-custom-background-90">
|
||||
<div className="grid h-full place-items-center p-4">
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="space-y-2">
|
||||
<Image src={AccessDeniedImg} height="220" width="550" alt="AccessDeniedImg" />
|
||||
<h3 className="text-3xl font-semibold">Access denied!</h3>
|
||||
<div className="mx-auto text-base text-custom-text-100">
|
||||
<p>Sorry, but you do not have permission to view this page.</p>
|
||||
<p>
|
||||
If you think there{"’"}s a mistake contact <span className="font-semibold">support.</span>
|
||||
<div className={`my-8 w-full flex flex-col gap-4 items-center justify-center overflow-hidden`}>
|
||||
<div className="w-3/5 bg-custom-background-90">
|
||||
<div className="grid h-full place-items-center p-2 pb-0">
|
||||
<div className="text-center">
|
||||
<Image src={AccessDeniedImg} height="250" width="550" alt="AccessDeniedImg" />
|
||||
<h3 className="text-3xl font-semibold">God mode needs a god role</h3>
|
||||
<p className="text-base text-custom-text-300">Doesn’t look like you have that role.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 my-8 text-center">
|
||||
<div>
|
||||
<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 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 className="flex items-center justify-center gap-2">
|
||||
@ -52,15 +90,12 @@ export const AdminAuthWrapper: FC<IAdminAuthWrapper> = observer(({ children }) =
|
||||
<a>
|
||||
<Button variant="primary" size="sm">
|
||||
<LayoutGrid width={16} height={16} />
|
||||
Back to Dashboard
|
||||
To the workspace
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
const {
|
||||
theme: { sidebarCollapsed, toggleSidebar },
|
||||
workspace: { workspaces },
|
||||
user: { currentUser, currentUserSettings },
|
||||
user: { currentUser, currentUserSettings, isUserInstanceAdmin },
|
||||
} = useMobxStore();
|
||||
|
||||
// redirect url for normal mode
|
||||
@ -135,6 +135,17 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
</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>
|
||||
</Transition>
|
||||
</Menu>
|
||||
|
62
web/pages/god-mode/ai.tsx
Normal file
62
web/pages/god-mode/ai.tsx
Normal 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;
|
@ -1,19 +1,17 @@
|
||||
import { ReactElement, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// layouts
|
||||
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout";
|
||||
import { InstanceAdminLayout } from "layouts/admin-layout";
|
||||
// types
|
||||
import { NextPageWithLayout } from "types/app";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// icons
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
// ui
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { InstanceGoogleConfigForm } from "components/instance/google-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 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);
|
||||
|
||||
const payload = {
|
||||
ENABLE_SIGNUP: value,
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
await updateInstanceConfigurations(payload)
|
||||
@ -46,7 +49,7 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Authorization Settings updated successfully",
|
||||
message: "SSO and OAuth Settings updated successfully",
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
})
|
||||
@ -55,102 +58,121 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: "Failed to update Authorization Settings",
|
||||
message: "Failed to update SSO and OAuth Settings",
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{formattedConfig ? (
|
||||
<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">Authorization</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">Single sign-on and OAuth</div>
|
||||
<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
|
||||
settings.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-8 pb-4 border-b border-custom-border-100">
|
||||
<div>
|
||||
<div className="text-custom-text-100 font-medium text-sm">Enable sign-up</div>
|
||||
{formattedConfig ? (
|
||||
<>
|
||||
<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">
|
||||
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 className={isSubmitting ? "opacity-70" : ""}>
|
||||
<div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableSignup))}
|
||||
value={Boolean(parseInt(enableMagicLogin))}
|
||||
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"
|
||||
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">
|
||||
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">
|
||||
<Disclosure as="div">
|
||||
{({ open }) => (
|
||||
<div className="w-full">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex items-center justify-between w-full py-2 border-b border-custom-border-100"
|
||||
>
|
||||
<div className="flex items-center justify-between py-2 border-b border-custom-border-100">
|
||||
<span className="text-lg font-medium tracking-tight">Google</span>
|
||||
{open ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
|
||||
</Disclosure.Button>
|
||||
<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">
|
||||
</div>
|
||||
<div className="px-2 py-6">
|
||||
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
<Disclosure as="div">
|
||||
{({ open }) => (
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex items-center justify-between w-full py-2 border-b border-custom-border-100"
|
||||
>
|
||||
<div className="flex items-center justify-between py-2 border-b border-custom-border-100">
|
||||
<span className="text-lg font-medium tracking-tight">Github</span>
|
||||
{open ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
|
||||
</Disclosure.Button>
|
||||
<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">
|
||||
</div>
|
||||
<div className="px-2 py-6">
|
||||
<InstanceGithubConfigForm config={formattedConfig} />
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-4 m-8">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader className="space-y-4">
|
||||
<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" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
</Loader>
|
||||
@ -160,7 +182,7 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
||||
});
|
||||
|
||||
InstanceAdminAuthorizationPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <InstanceAdminLayout header={<InstanceAdminHeader title="Authorization" />}>{page}</InstanceAdminLayout>;
|
||||
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
|
||||
};
|
||||
|
||||
export default InstanceAdminAuthorizationPage;
|
||||
|
@ -2,7 +2,7 @@ import { ReactElement } from "react";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// layouts
|
||||
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout";
|
||||
import { InstanceAdminLayout } from "layouts/admin-layout";
|
||||
// types
|
||||
import { NextPageWithLayout } from "types/app";
|
||||
// store
|
||||
@ -21,11 +21,21 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
|
||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
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 ? (
|
||||
<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" width="25%" />
|
||||
@ -38,7 +48,7 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
|
||||
});
|
||||
|
||||
InstanceAdminEmailPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <InstanceAdminLayout header={<InstanceAdminHeader title="Email" />}>{page}</InstanceAdminLayout>;
|
||||
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
|
||||
};
|
||||
|
||||
export default InstanceAdminEmailPage;
|
||||
|
47
web/pages/god-mode/image.tsx
Normal file
47
web/pages/god-mode/image.tsx
Normal 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;
|
@ -2,7 +2,7 @@ import { ReactElement } from "react";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// layouts
|
||||
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout";
|
||||
import { InstanceAdminLayout } from "layouts/admin-layout";
|
||||
// types
|
||||
import { NextPageWithLayout } from "types/app";
|
||||
// store
|
||||
@ -21,11 +21,18 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
|
||||
useSWR("INSTANCE_INFO", () => fetchInstanceInfo());
|
||||
|
||||
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 ? (
|
||||
<InstanceGeneralForm instance={instance} />
|
||||
) : (
|
||||
<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="25%" />
|
||||
@ -36,7 +43,7 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
|
||||
});
|
||||
|
||||
InstanceAdminPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <InstanceAdminLayout header={<InstanceAdminHeader title="General" />}>{page}</InstanceAdminLayout>;
|
||||
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
|
||||
};
|
||||
|
||||
export default InstanceAdminPage;
|
||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user