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
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>
</>
);
};

View File

@ -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,45 +149,55 @@ 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 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"
render={({ field: { value, onChange } }) => (
<ToggleSwitch
value={Boolean(parseInt(value))}
onChange={() => {
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
}}
size="sm"
/>
)}
/>
</div>
</div>
<div>
<Controller
control={control}
name="EMAIL_USE_TLS"
render={({ field: { value, onChange } }) => (
<ToggleSwitch
value={Boolean(parseInt(value))}
onChange={() => {
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
}}
size="sm"
/>
)}
/>
</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>
<div>
<Controller
control={control}
name="EMAIL_USE_SSL"
render={({ field: { value, onChange } }) => (
<ToggleSwitch
value={Boolean(parseInt(value))}
onChange={() => {
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
}}
size="sm"
/>
)}
/>
<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 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"
render={({ field: { value, onChange } }) => (
<ToggleSwitch
value={Boolean(parseInt(value))}
onChange={() => {
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
}}
size="sm"
/>
)}
/>
</div>
</div>
</div>
@ -199,6 +206,6 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
{isSubmitting ? "Saving..." : "Save Changes"}
</Button>
</div>
</div>
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -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>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center p-2">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"}
</Button>
</div>
<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">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"}
</Button>
</div>
</div>
</div>
);
};

View File

@ -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>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center p-2">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"}
</Button>
</div>
<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">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"}
</Button>
</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 { 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 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 && <h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>}
</div>
</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">
{!sidebarCollapsed && (
<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>

View File

@ -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>

View File

@ -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>

View File

@ -7,26 +7,28 @@ 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">
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
label="Settings"
link="/god-mode"
/>
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
</Breadcrumbs>
</div>
{title && (
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
label="Settings"
link="/god-mode"
/>
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
</Breadcrumbs>
</div>
)}
</div>
</div>
);

View File

@ -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}</>

View File

@ -33,33 +33,68 @@ 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>
</p>
</div>
<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">Doesnt 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 className="flex items-center justify-center gap-2">
<Link href={`/${redirectWorkspaceSlug}`}>
<a>
<Button variant="primary" size="sm">
<LayoutGrid width={16} height={16} />
Back to Dashboard
</Button>
<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>
</Link>
</p>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-center gap-2">
<Link href={`/${redirectWorkspaceSlug}`}>
<a>
<Button variant="primary" size="sm">
<LayoutGrid width={16} height={16} />
To the workspace
</Button>
</a>
</Link>
</div>
</div>
);
}

View File

@ -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
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 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>
<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>
{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="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>
<div className="text-custom-text-300 font-normal text-xs">
Keep the doors open so people can join your workspaces.
<>
<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">
<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={`shrink-0 ${isSubmitting && "opacity-70"}`}>
<ToggleSwitch
value={Boolean(parseInt(enableMagicLogin))}
onChange={() => {
Boolean(parseInt(enableMagicLogin)) === true
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
<div className={isSubmitting ? "opacity-70" : ""}>
<ToggleSwitch
value={Boolean(parseInt(enableSignup))}
onChange={() => {
Boolean(parseInt(enableSignup)) === true ? updateConfig("0") : updateConfig("1");
}}
size="sm"
disabled={isSubmitting}
/>
<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"
>
<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">
<InstanceGoogleConfigForm config={formattedConfig} />
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
<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"
>
<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">
<InstanceGithubConfigForm config={formattedConfig} />
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
<div className="w-full">
<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>
</div>
<div className="px-2 py-6">
<InstanceGoogleConfigForm config={formattedConfig} />
</div>
</div>
<div className="w-full">
<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>
</div>
<div className="px-2 py-6">
<InstanceGithubConfigForm config={formattedConfig} />
</div>
</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;

View File

@ -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;

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 { 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;

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;