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
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
export interface IInstanceOpenAIForm {
|
export interface IInstanceAIForm {
|
||||||
config: IFormattedInstanceConfiguration;
|
config: IFormattedInstanceConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAIFormValues {
|
export interface AIFormValues {
|
||||||
OPENAI_API_BASE: string;
|
|
||||||
OPENAI_API_KEY: string;
|
OPENAI_API_KEY: string;
|
||||||
GPT_ENGINE: string;
|
GPT_ENGINE: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
|
||||||
const { config } = props;
|
const { config } = props;
|
||||||
// store
|
// store
|
||||||
const { instance: instanceStore } = useMobxStore();
|
const { instance: instanceStore } = useMobxStore();
|
||||||
@ -30,16 +29,15 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<OpenAIFormValues>({
|
} = useForm<AIFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
OPENAI_API_BASE: config["OPENAI_API_BASE"],
|
|
||||||
OPENAI_API_KEY: config["OPENAI_API_KEY"],
|
OPENAI_API_KEY: config["OPENAI_API_KEY"],
|
||||||
GPT_ENGINE: config["GPT_ENGINE"],
|
GPT_ENGINE: config["GPT_ENGINE"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (formData: OpenAIFormValues) => {
|
const onSubmit = async (formData: AIFormValues) => {
|
||||||
const payload: Partial<OpenAIFormValues> = { ...formData };
|
const payload: Partial<AIFormValues> = { ...formData };
|
||||||
|
|
||||||
await instanceStore
|
await instanceStore
|
||||||
.updateInstanceConfigurations(payload)
|
.updateInstanceConfigurations(payload)
|
||||||
@ -47,64 +45,15 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
|||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Open AI Settings updated successfully",
|
message: "AI Settings updated successfully",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 m-8 w-4/5">
|
<>
|
||||||
<div className="pb-2 mb-2 border-b border-custom-border-100">
|
<div className="grid grid-col grid-cols-1 lg:grid-cols-3 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||||
<div className="text-custom-text-100 font-medium text-lg">OpenAI</div>
|
|
||||||
<div className="text-custom-text-300 font-normal text-sm">
|
|
||||||
AI is everywhere make use it as much as you can! <a href="#" className="text-custom-primary-100">Learn more.</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h4 className="text-sm">OpenAI API Base</h4>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="OPENAI_API_BASE"
|
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
|
||||||
<Input
|
|
||||||
id="OPENAI_API_BASE"
|
|
||||||
name="OPENAI_API_BASE"
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
ref={ref}
|
|
||||||
hasError={Boolean(errors.OPENAI_API_BASE)}
|
|
||||||
placeholder="OpenAI API Base"
|
|
||||||
className="rounded-md font-medium w-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h4 className="text-sm">OpenAI API Key</h4>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="OPENAI_API_KEY"
|
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
|
||||||
<Input
|
|
||||||
id="OPENAI_API_KEY"
|
|
||||||
name="OPENAI_API_KEY"
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
ref={ref}
|
|
||||||
hasError={Boolean(errors.OPENAI_API_KEY)}
|
|
||||||
placeholder="OpenAI API Key"
|
|
||||||
className="rounded-md font-medium w-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">GPT Engine</h4>
|
<h4 className="text-sm">GPT Engine</h4>
|
||||||
<Controller
|
<Controller
|
||||||
@ -119,11 +68,54 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.GPT_ENGINE)}
|
hasError={Boolean(errors.GPT_ENGINE)}
|
||||||
placeholder="GPT Engine"
|
placeholder="gpt-3.5-turbo"
|
||||||
className="rounded-md font-medium w-full"
|
className="rounded-md font-medium w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-custom-text-400">
|
||||||
|
Choose an OpenAI engine.{" "}
|
||||||
|
<a
|
||||||
|
href="https://platform.openai.com/docs/models/overview"
|
||||||
|
target="_blank"
|
||||||
|
className="text-custom-primary-100 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm">API Key</h4>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="OPENAI_API_KEY"
|
||||||
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
|
<Input
|
||||||
|
id="OPENAI_API_KEY"
|
||||||
|
name="OPENAI_API_KEY"
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.OPENAI_API_KEY)}
|
||||||
|
placeholder="sk-asddassdfasdefqsdfasd23das3dasdcasd"
|
||||||
|
className="rounded-md font-medium w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-custom-text-400">
|
||||||
|
You will find your API key{" "}
|
||||||
|
<a
|
||||||
|
href="https://platform.openai.com/api-keys"
|
||||||
|
target="_blank"
|
||||||
|
className="text-custom-primary-100 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
here.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -132,6 +124,6 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
|||||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -31,6 +31,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
|||||||
// form data
|
// form data
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
watch,
|
||||||
control,
|
control,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<EmailFormValues>({
|
} = useForm<EmailFormValues>({
|
||||||
@ -60,11 +61,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 m-8 w-4/5">
|
<>
|
||||||
<div className="pb-2 mb-2 border-b border-custom-border-100">
|
|
||||||
<div className="text-custom-text-100 font-medium text-lg">Email</div>
|
|
||||||
<div className="text-custom-text-300 font-normal text-sm">Email related settings.</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Host</h4>
|
<h4 className="text-sm">Host</h4>
|
||||||
@ -80,7 +77,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.EMAIL_HOST)}
|
hasError={Boolean(errors.EMAIL_HOST)}
|
||||||
placeholder="Email Host"
|
placeholder="email.google.com"
|
||||||
className="rounded-md font-medium w-full"
|
className="rounded-md font-medium w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -101,7 +98,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.EMAIL_PORT)}
|
hasError={Boolean(errors.EMAIL_PORT)}
|
||||||
placeholder="Email Port"
|
placeholder="8080"
|
||||||
className="rounded-md font-medium w-full"
|
className="rounded-md font-medium w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -123,7 +120,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.EMAIL_HOST_USER)}
|
hasError={Boolean(errors.EMAIL_HOST_USER)}
|
||||||
placeholder="Username"
|
placeholder="getitdone@projectplane.so"
|
||||||
className="rounded-md font-medium w-full"
|
className="rounded-md font-medium w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -139,7 +136,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
|||||||
<Input
|
<Input
|
||||||
id="EMAIL_HOST_PASSWORD"
|
id="EMAIL_HOST_PASSWORD"
|
||||||
name="EMAIL_HOST_PASSWORD"
|
name="EMAIL_HOST_PASSWORD"
|
||||||
type="text"
|
type="password"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -152,45 +149,55 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-8 pt-4">
|
<div className="w-full lg:w-1/2 flex flex-col px-1 gap-y-8">
|
||||||
<div>
|
<div className="flex items-center gap-8 pt-4 mr-8">
|
||||||
<div className="text-custom-text-100 font-medium text-sm">Enable TLS</div>
|
<div className="grow">
|
||||||
|
<div className="text-custom-text-100 font-medium text-sm">
|
||||||
|
Turn TLS {Boolean(parseInt(watch("EMAIL_USE_TLS"))) ? "off" : "on"}
|
||||||
|
</div>
|
||||||
|
<div 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>
|
||||||
<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 className="flex items-center gap-8 pt-4 mr-8">
|
||||||
<div>
|
<div className="grow">
|
||||||
<div className="text-custom-text-100 font-medium text-sm">Enable SSL</div>
|
<div className="text-custom-text-100 font-medium text-sm">
|
||||||
</div>
|
Turn SSL {Boolean(parseInt(watch("EMAIL_USE_SSL"))) ? "off" : "on"}
|
||||||
<div>
|
</div>
|
||||||
<Controller
|
<div className="text-custom-text-300 font-normal text-xs">
|
||||||
control={control}
|
Most email domains support SSL. Use this to secure comms between this instance and your users.
|
||||||
name="EMAIL_USE_SSL"
|
</div>
|
||||||
render={({ field: { value, onChange } }) => (
|
</div>
|
||||||
<ToggleSwitch
|
<div className="shrink-0">
|
||||||
value={Boolean(parseInt(value))}
|
<Controller
|
||||||
onChange={() => {
|
control={control}
|
||||||
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
|
name="EMAIL_USE_SSL"
|
||||||
}}
|
render={({ field: { value, onChange } }) => (
|
||||||
size="sm"
|
<ToggleSwitch
|
||||||
/>
|
value={Boolean(parseInt(value))}
|
||||||
)}
|
onChange={() => {
|
||||||
/>
|
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -199,6 +206,6 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
|||||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ export interface IInstanceGeneralForm {
|
|||||||
|
|
||||||
export interface GeneralFormValues {
|
export interface GeneralFormValues {
|
||||||
instance_name: string;
|
instance_name: string;
|
||||||
is_telemetry_enabled: boolean;
|
// is_telemetry_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||||
@ -31,7 +31,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
|||||||
} = useForm<GeneralFormValues>({
|
} = useForm<GeneralFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
instance_name: instance.instance_name,
|
instance_name: instance.instance_name,
|
||||||
is_telemetry_enabled: instance.is_telemetry_enabled,
|
// is_telemetry_enabled: instance.is_telemetry_enabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,13 +51,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 m-8">
|
<>
|
||||||
<div className="pb-2 mb-2 border-b border-custom-border-100">
|
|
||||||
<div className="text-custom-text-100 font-medium text-lg">General</div>
|
|
||||||
<div className="text-custom-text-300 font-normal text-sm">
|
|
||||||
The usual things like your mail, name of instance and other stuff.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-8 w-full">
|
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-8 w-full">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Name of instance</h4>
|
<h4 className="text-sm">Name of instance</h4>
|
||||||
@ -106,7 +100,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-8 pt-4">
|
{/* <div className="flex items-center gap-12 pt-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-custom-text-100 font-medium text-sm">Share anonymous usage instance</div>
|
<div className="text-custom-text-100 font-medium text-sm">Share anonymous usage instance</div>
|
||||||
<div className="text-custom-text-300 font-normal text-xs">
|
<div className="text-custom-text-300 font-normal text-xs">
|
||||||
@ -120,13 +114,13 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
|||||||
render={({ field: { value, onChange } }) => <ToggleSwitch value={value} onChange={onChange} size="sm" />}
|
render={({ field: { value, onChange } }) => <ToggleSwitch value={value} onChange={onChange} size="sm" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="flex items-center py-1">
|
<div className="flex items-center py-1">
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -56,8 +56,8 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
|||||||
const originURL = typeof window !== "undefined" ? window.location.origin : "";
|
const originURL = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
<div className="grid grid-col grid-cols-1 lg:grid-cols-3 justify-between gap-x-12 gap-y-8 w-full">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Client ID</h4>
|
<h4 className="text-sm">Client ID</h4>
|
||||||
<Controller
|
<Controller
|
||||||
@ -72,11 +72,22 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.GITHUB_CLIENT_ID)}
|
hasError={Boolean(errors.GITHUB_CLIENT_ID)}
|
||||||
placeholder="Github Client ID"
|
placeholder="70a44354520df8bd9bcd"
|
||||||
className="rounded-md font-medium w-full"
|
className="rounded-md font-medium w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-custom-text-400">
|
||||||
|
You will get this from your{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/settings/applications/new"
|
||||||
|
target="_blank"
|
||||||
|
className="text-custom-primary-100 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
GitHub OAuth application settings.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Client Secret</h4>
|
<h4 className="text-sm">Client Secret</h4>
|
||||||
@ -92,14 +103,23 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.GITHUB_CLIENT_SECRET)}
|
hasError={Boolean(errors.GITHUB_CLIENT_SECRET)}
|
||||||
placeholder="Github Client Secret"
|
placeholder="9b0050f94ec1b744e32ce79ea4ffacd40d4119cb"
|
||||||
className="rounded-md font-medium w-full"
|
className="rounded-md font-medium w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-custom-text-400">
|
||||||
|
Your client secret is also found in your{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/settings/applications/new"
|
||||||
|
target="_blank"
|
||||||
|
className="text-custom-primary-100 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
GitHub OAuth application settings.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Origin URL</h4>
|
<h4 className="text-sm">Origin URL</h4>
|
||||||
<Button
|
<Button
|
||||||
@ -117,16 +137,26 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
|||||||
<p className="font-medium text-sm">{originURL}</p>
|
<p className="font-medium text-sm">{originURL}</p>
|
||||||
<Copy size={18} color="#B9B9B9" />
|
<Copy size={18} color="#B9B9B9" />
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-custom-text-400/60">*paste this URL in your Github console.</p>
|
<p className="text-xs text-custom-text-400">
|
||||||
</div>
|
We will auto-generate this. Paste this into the Authorization callback URL field{" "}
|
||||||
<div className="flex flex-col gap-1">
|
<a
|
||||||
<div className="flex items-center p-2">
|
href="https://github.com/settings/applications/new"
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
target="_blank"
|
||||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
className="text-custom-primary-100 hover:underline"
|
||||||
</Button>
|
rel="noreferrer"
|
||||||
</div>
|
>
|
||||||
|
here.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -56,8 +56,8 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
|||||||
const originURL = typeof window !== "undefined" ? window.location.origin : "";
|
const originURL = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
<div className="grid grid-col grid-cols-1 lg:grid-cols-3 justify-between gap-x-12 gap-y-8 w-full">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Client ID</h4>
|
<h4 className="text-sm">Client ID</h4>
|
||||||
<Controller
|
<Controller
|
||||||
@ -72,11 +72,22 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.GOOGLE_CLIENT_ID)}
|
hasError={Boolean(errors.GOOGLE_CLIENT_ID)}
|
||||||
placeholder="Google Client ID"
|
placeholder="840195096245-0p2tstej9j5nc4l8o1ah2dqondscqc1g.apps.googleusercontent.com"
|
||||||
className="rounded-md font-medium w-full"
|
className="rounded-md font-medium w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-custom-text-400">
|
||||||
|
Your client ID lives in your Google API Console.{" "}
|
||||||
|
<a
|
||||||
|
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
|
||||||
|
target="_blank"
|
||||||
|
className="text-custom-primary-100 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Client Secret</h4>
|
<h4 className="text-sm">Client Secret</h4>
|
||||||
@ -92,14 +103,23 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.GOOGLE_CLIENT_SECRET)}
|
hasError={Boolean(errors.GOOGLE_CLIENT_SECRET)}
|
||||||
placeholder="Google Client Secret"
|
placeholder="GOCShX-ADp4cI0kPqav1gGCBg5bE02E"
|
||||||
className="rounded-md font-medium w-full"
|
className="rounded-md font-medium w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-custom-text-400">
|
||||||
|
Your client secret should also be in your Google API Console.{" "}
|
||||||
|
<a
|
||||||
|
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
|
||||||
|
target="_blank"
|
||||||
|
className="text-custom-primary-100 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Origin URL</h4>
|
<h4 className="text-sm">Origin URL</h4>
|
||||||
<Button
|
<Button
|
||||||
@ -117,16 +137,26 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
|||||||
<p className="font-medium text-sm">{originURL}</p>
|
<p className="font-medium text-sm">{originURL}</p>
|
||||||
<Copy size={18} color="#B9B9B9" />
|
<Copy size={18} color="#B9B9B9" />
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-custom-text-400/60">*paste this URL in your Google developer console.</p>
|
<p className="text-xs text-custom-text-400">
|
||||||
</div>
|
We will auto-generate this. Paste this into your Authorized JavaScript origins field. For this OAuth client{" "}
|
||||||
<div className="flex flex-col gap-1">
|
<a
|
||||||
<div className="flex items-center p-2">
|
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
target="_blank"
|
||||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
className="text-custom-primary-100 hover:underline"
|
||||||
</Button>
|
rel="noreferrer"
|
||||||
</div>
|
>
|
||||||
|
here.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
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 { useTheme } from "next-themes";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
|
||||||
import { Cog, LogIn, LogOut, Settings } from "lucide-react";
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
// components
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
|
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
@ -64,28 +66,21 @@ export const InstanceSidebarDropdown = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-3 gap-y-2 px-4 py-4">
|
<div className="flex items-center gap-x-2 gap-y-2 px-4 pt-3 pb-2 mb-2 border border-custom-sidebar-border-200">
|
||||||
<div className="w-full h-full truncate">
|
<div className="w-full h-full truncate">
|
||||||
<div
|
<div
|
||||||
className={`flex flex-grow items-center gap-x-2 rounded p-1 truncate ${
|
className={`flex flex-grow items-center gap-x-2 rounded p-1 truncate ${
|
||||||
sidebarCollapsed ? "justify-center" : ""
|
sidebarCollapsed ? "justify-center" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div className={`flex-shrink-0 flex items-center justify-center h-7 w-7 rounded bg-custom-sidebar-background-80`}>
|
||||||
className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`}
|
<UserCog2 className="h-6 w-6 text-custom-text-200" />
|
||||||
>
|
|
||||||
<Cog className="h-5 w-5 text-custom-text-200" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!sidebarCollapsed && <h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>}
|
{!sidebarCollapsed && (
|
||||||
</div>
|
<div className="flex w-full gap-2">
|
||||||
</div>
|
<h4 className="grow text-custom-text-200 font-medium text-base truncate">God Mode</h4>
|
||||||
|
<Tooltip position="bottom-left" tooltipContent="Exit God Mode">
|
||||||
{!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-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||||
<a>
|
<a>
|
||||||
@ -94,7 +89,14 @@ export const InstanceSidebarDropdown = observer(() => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<Menu as="div" className="relative flex-shrink-0">
|
||||||
|
<Menu.Button className="grid place-items-center outline-none">
|
||||||
<Avatar
|
<Avatar
|
||||||
name={currentUser?.display_name}
|
name={currentUser?.display_name}
|
||||||
src={currentUser?.avatar}
|
src={currentUser?.avatar}
|
||||||
@ -114,8 +116,8 @@ export const InstanceSidebarDropdown = observer(() => {
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 origin-top-left rounded-md
|
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 rounded-md
|
||||||
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none"
|
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-100 shadow-lg text-xs outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2.5 pb-2">
|
<div className="flex flex-col gap-2.5 pb-2">
|
||||||
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||||
@ -145,8 +147,8 @@ export const InstanceSidebarDropdown = observer(() => {
|
|||||||
<div className="p-2 pb-0">
|
<div className="p-2 pb-0">
|
||||||
<Menu.Item as="button" type="button" className="w-full">
|
<Menu.Item as="button" type="button" className="w-full">
|
||||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||||
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-10 hover:bg-custom-primary-20">
|
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-100/20 hover:bg-custom-primary-100/30">
|
||||||
Normal Mode
|
Exit God Mode
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// icons
|
// icons
|
||||||
import { BrainCog, Cog, Lock, Mail } from "lucide-react";
|
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// ui
|
// ui
|
||||||
@ -11,26 +11,32 @@ const INSTANCE_ADMIN_LINKS = [
|
|||||||
{
|
{
|
||||||
Icon: Cog,
|
Icon: Cog,
|
||||||
name: "General",
|
name: "General",
|
||||||
description: "General settings here",
|
description: "Identify your instances and get key details",
|
||||||
href: `/god-mode`,
|
href: `/god-mode`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: Mail,
|
Icon: Mail,
|
||||||
name: "Email",
|
name: "Email",
|
||||||
description: "Email related settings will go here",
|
description: "Set up emails to your users",
|
||||||
href: `/god-mode/email`,
|
href: `/god-mode/email`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: Lock,
|
Icon: Lock,
|
||||||
name: "Authorization",
|
name: "SSO and OAuth",
|
||||||
description: "Autorization",
|
description: "Configure your Google and GitHub SSOs",
|
||||||
href: `/god-mode/authorization`,
|
href: `/god-mode/authorization`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: BrainCog,
|
Icon: BrainCog,
|
||||||
name: "OpenAI",
|
name: "Artificial intelligence",
|
||||||
description: "OpenAI configurations",
|
description: "Configure your OpenAI creds",
|
||||||
href: `/god-mode/openai`,
|
href: `/god-mode/ai`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: Image,
|
||||||
|
name: "Images in Plane",
|
||||||
|
description: "Allow third-party image libraries",
|
||||||
|
href: `/god-mode/image`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -68,7 +74,9 @@ export const InstanceAdminSidebarMenu = () => {
|
|||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-xs ${isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-300"}`}
|
className={`text-[10px] ${
|
||||||
|
isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-300"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{item.description}
|
{item.description}
|
||||||
</span>
|
</span>
|
||||||
|
@ -303,8 +303,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
|||||||
<div className="p-2 pb-0">
|
<div className="p-2 pb-0">
|
||||||
<Menu.Item as="button" type="button" className="w-full">
|
<Menu.Item as="button" type="button" className="w-full">
|
||||||
<Link href="/god-mode">
|
<Link href="/god-mode">
|
||||||
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-10 hover:bg-custom-primary-20">
|
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-100/20 hover:bg-custom-primary-100/30">
|
||||||
God Mode
|
Enter God Mode
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
@ -7,26 +7,28 @@ import { Breadcrumbs } from "@plane/ui";
|
|||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
|
|
||||||
export interface IInstanceAdminHeader {
|
export interface IInstanceAdminHeader {
|
||||||
title: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstanceAdminHeader: FC<IInstanceAdminHeader> = observer((props) => {
|
export const InstanceAdminHeader: FC<IInstanceAdminHeader> = observer((props) => {
|
||||||
const { title } = props;
|
const { title } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.4rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||||
<div>
|
{title && (
|
||||||
<Breadcrumbs>
|
<div>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs>
|
||||||
type="text"
|
<Breadcrumbs.BreadcrumbItem
|
||||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
type="text"
|
||||||
label="Settings"
|
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||||
link="/god-mode"
|
label="Settings"
|
||||||
/>
|
link="/god-mode"
|
||||||
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
|
/>
|
||||||
</Breadcrumbs>
|
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
|
||||||
</div>
|
</Breadcrumbs>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,14 +3,14 @@ import { FC, ReactNode } from "react";
|
|||||||
import { AdminAuthWrapper, UserAuthWrapper } from "layouts/auth-layout";
|
import { AdminAuthWrapper, UserAuthWrapper } from "layouts/auth-layout";
|
||||||
// components
|
// components
|
||||||
import { InstanceAdminSidebar } from "./sidebar";
|
import { InstanceAdminSidebar } from "./sidebar";
|
||||||
|
import { InstanceAdminHeader } from "./header";
|
||||||
|
|
||||||
export interface IInstanceAdminLayout {
|
export interface IInstanceAdminLayout {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
header: ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstanceAdminLayout: FC<IInstanceAdminLayout> = (props) => {
|
export const InstanceAdminLayout: FC<IInstanceAdminLayout> = (props) => {
|
||||||
const { children, header } = props;
|
const { children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -19,7 +19,7 @@ export const InstanceAdminLayout: FC<IInstanceAdminLayout> = (props) => {
|
|||||||
<div className="relative flex h-screen w-full overflow-hidden">
|
<div className="relative flex h-screen w-full overflow-hidden">
|
||||||
<InstanceAdminSidebar />
|
<InstanceAdminSidebar />
|
||||||
<main className="relative flex flex-col h-full w-full overflow-hidden bg-custom-background-100">
|
<main className="relative flex flex-col h-full w-full overflow-hidden bg-custom-background-100">
|
||||||
{header}
|
<InstanceAdminHeader />
|
||||||
<div className="h-full w-full overflow-hidden">
|
<div className="h-full w-full overflow-hidden">
|
||||||
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
|
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
|
@ -33,33 +33,68 @@ export const AdminAuthWrapper: FC<IAdminAuthWrapper> = observer(({ children }) =
|
|||||||
// if user does not have admin access to the instance
|
// if user does not have admin access to the instance
|
||||||
if (isUserInstanceAdmin !== undefined && isUserInstanceAdmin === false) {
|
if (isUserInstanceAdmin !== undefined && isUserInstanceAdmin === false) {
|
||||||
return (
|
return (
|
||||||
<div className={`h-screen w-full flex items-center justify-center overflow-hidden`}>
|
<div className={`my-8 w-full flex flex-col gap-4 items-center justify-center overflow-hidden`}>
|
||||||
<div className="w-3/5 h-2/3 bg-custom-background-90">
|
<div className="w-3/5 bg-custom-background-90">
|
||||||
<div className="grid h-full place-items-center p-4">
|
<div className="grid h-full place-items-center p-2 pb-0">
|
||||||
<div className="space-y-8 text-center">
|
<div className="text-center">
|
||||||
<div className="space-y-2">
|
<Image src={AccessDeniedImg} height="250" width="550" alt="AccessDeniedImg" />
|
||||||
<Image src={AccessDeniedImg} height="220" width="550" alt="AccessDeniedImg" />
|
<h3 className="text-3xl font-semibold">God mode needs a god role</h3>
|
||||||
<h3 className="text-3xl font-semibold">Access denied!</h3>
|
<p className="text-base text-custom-text-300">Doesn’t look like you have that role.</p>
|
||||||
<div className="mx-auto text-base text-custom-text-100">
|
</div>
|
||||||
<p>Sorry, but you do not have permission to view this page.</p>
|
<div className="flex flex-col gap-2 my-8 text-center">
|
||||||
<p>
|
<div>
|
||||||
If you think there{"’"}s a mistake contact <span className="font-semibold">support.</span>
|
<p className="font-medium text-xs text-custom-text-400 tracking-tight">Do we have a god role?</p>
|
||||||
</p>
|
<p className="text-custom-text-300 text-sm">Yes.</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div>
|
||||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
<p className="font-medium text-xs text-custom-text-400 tracking-tight">Do we call it god role?</p>
|
||||||
<a>
|
<p className="text-custom-text-300 text-sm">No. Obviously not.</p>
|
||||||
<Button variant="primary" size="sm">
|
</div>
|
||||||
<LayoutGrid width={16} height={16} />
|
<div>
|
||||||
Back to Dashboard
|
<p className="font-medium text-xs text-custom-text-400 tracking-tight">Can you get it?</p>
|
||||||
</Button>
|
<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>
|
</a>
|
||||||
</Link>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
const {
|
const {
|
||||||
theme: { sidebarCollapsed, toggleSidebar },
|
theme: { sidebarCollapsed, toggleSidebar },
|
||||||
workspace: { workspaces },
|
workspace: { workspaces },
|
||||||
user: { currentUser, currentUserSettings },
|
user: { currentUser, currentUserSettings, isUserInstanceAdmin },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
// redirect url for normal mode
|
// redirect url for normal mode
|
||||||
@ -135,6 +135,17 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||||
Sign out
|
Sign out
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
{isUserInstanceAdmin && (
|
||||||
|
<div className="p-2 pb-0 border-t border-custom-border-100">
|
||||||
|
<Menu.Item as="button" type="button" className="w-full">
|
||||||
|
<Link href="/god-mode">
|
||||||
|
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-100/20 hover:bg-custom-primary-100/30">
|
||||||
|
Enter God Mode
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
62
web/pages/god-mode/ai.tsx
Normal file
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 { ReactElement, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// layouts
|
// layouts
|
||||||
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout";
|
import { InstanceAdminLayout } from "layouts/admin-layout";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
// store
|
// store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// icons
|
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
|
||||||
// components
|
// components
|
||||||
import { InstanceGoogleConfigForm } from "components/instance/google-config-form";
|
import { InstanceGoogleConfigForm } from "components/instance/google-config-form";
|
||||||
import { InstanceGithubConfigForm } from "components/instance/github-config-form";
|
import { InstanceGithubConfigForm } from "components/instance/github-config-form";
|
||||||
@ -33,12 +31,17 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
|
|
||||||
const enableSignup = formattedConfig?.ENABLE_SIGNUP ?? "0";
|
const enableSignup = formattedConfig?.ENABLE_SIGNUP ?? "0";
|
||||||
|
const enableMagicLogin = formattedConfig?.ENABLE_MAGIC_LINK_LOGIN ?? "0";
|
||||||
|
// const enableEmailPassword = formattedConfig?.ENABLE_EMAIL_PASSWORD ?? "0";
|
||||||
|
|
||||||
const updateConfig = async (value: string) => {
|
const updateConfig = async (
|
||||||
|
key: "ENABLE_SIGNUP" | "ENABLE_MAGIC_LINK_LOGIN" | "ENABLE_EMAIL_PASSWORD",
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
ENABLE_SIGNUP: value,
|
[key]: value,
|
||||||
};
|
};
|
||||||
|
|
||||||
await updateInstanceConfigurations(payload)
|
await updateInstanceConfigurations(payload)
|
||||||
@ -46,7 +49,7 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
|||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Authorization Settings updated successfully",
|
message: "SSO and OAuth Settings updated successfully",
|
||||||
});
|
});
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
})
|
})
|
||||||
@ -55,102 +58,121 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
|||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
type: "error",
|
type: "error",
|
||||||
message: "Failed to update Authorization Settings",
|
message: "Failed to update SSO and OAuth Settings",
|
||||||
});
|
});
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col gap-8 my-8 mx-12">
|
||||||
|
<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 ? (
|
{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="flex flex-col gap-12 w-full lg:w-2/5 pb-8 border-b border-custom-border-100">
|
||||||
<div className="text-custom-text-100 font-medium text-lg">Authorization</div>
|
<div className="flex items-center gap-14 mr-4">
|
||||||
<div className="text-custom-text-300 font-normal text-sm">
|
<div className="grow">
|
||||||
Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the
|
<div className="text-custom-text-100 font-medium text-sm">
|
||||||
settings.
|
Turn Magic Links {Boolean(parseInt(enableMagicLogin)) ? "off" : "on"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="text-custom-text-300 font-normal text-xs">
|
||||||
<div className="flex items-center gap-8 pb-4 border-b border-custom-border-100">
|
<p>Slack-like emails for authentication.</p>
|
||||||
<div>
|
You need to have set up email{" "}
|
||||||
<div className="text-custom-text-100 font-medium text-sm">Enable sign-up</div>
|
<Link href="email">
|
||||||
<div className="text-custom-text-300 font-normal text-xs">
|
<a className="text-custom-primary-100 hover:underline">here</a>
|
||||||
Keep the doors open so people can join your workspaces.
|
</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>
|
</div>
|
||||||
<div className={isSubmitting ? "opacity-70" : ""}>
|
<div className="flex items-center gap-14 mr-4">
|
||||||
<ToggleSwitch
|
<div className="grow">
|
||||||
value={Boolean(parseInt(enableSignup))}
|
<div className="text-custom-text-100 font-medium text-sm">
|
||||||
onChange={() => {
|
Let your users log in via the methods below
|
||||||
Boolean(parseInt(enableSignup)) === true ? updateConfig("0") : updateConfig("1");
|
</div>
|
||||||
}}
|
<div className="text-custom-text-300 font-normal text-xs">
|
||||||
size="sm"
|
Toggling this off will disable all previous configs. Users will only be able to login with an e-mail
|
||||||
disabled={isSubmitting}
|
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>
|
||||||
|
{/* <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>
|
||||||
<div className="flex flex-col gap-y-6 py-2">
|
<div className="flex flex-col gap-y-6 py-2">
|
||||||
<Disclosure as="div">
|
<div className="w-full">
|
||||||
{({ open }) => (
|
<div className="flex items-center justify-between py-2 border-b border-custom-border-100">
|
||||||
<div className="w-full">
|
<span className="text-lg font-medium tracking-tight">Google</span>
|
||||||
<Disclosure.Button
|
</div>
|
||||||
as="button"
|
<div className="px-2 py-6">
|
||||||
type="button"
|
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||||
className="flex items-center justify-between w-full py-2 border-b border-custom-border-100"
|
</div>
|
||||||
>
|
</div>
|
||||||
<span className="text-lg font-medium tracking-tight">Google</span>
|
<div className="w-full">
|
||||||
{open ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
|
<div className="flex items-center justify-between py-2 border-b border-custom-border-100">
|
||||||
</Disclosure.Button>
|
<span className="text-lg font-medium tracking-tight">Github</span>
|
||||||
<Transition
|
</div>
|
||||||
show={open}
|
<div className="px-2 py-6">
|
||||||
enter="transition duration-100 ease-out"
|
<InstanceGithubConfigForm config={formattedConfig} />
|
||||||
enterFrom="transform opacity-0"
|
</div>
|
||||||
enterTo="transform opacity-100"
|
</div>
|
||||||
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>
|
</div>
|
||||||
</div>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-4 m-8">
|
<Loader className="space-y-4">
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" width="50%" />
|
||||||
|
<Loader.Item height="50px" width="50%" />
|
||||||
|
<Loader.Item height="50px" width="50%" />
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
|
<Loader.Item height="50px" width="25%" />
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
<Loader.Item height="50px" width="25%" />
|
<Loader.Item height="50px" width="25%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
@ -160,7 +182,7 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
InstanceAdminAuthorizationPage.getLayout = function getLayout(page: ReactElement) {
|
InstanceAdminAuthorizationPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <InstanceAdminLayout header={<InstanceAdminHeader title="Authorization" />}>{page}</InstanceAdminLayout>;
|
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InstanceAdminAuthorizationPage;
|
export default InstanceAdminAuthorizationPage;
|
||||||
|
@ -2,7 +2,7 @@ import { ReactElement } from "react";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// layouts
|
// layouts
|
||||||
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout";
|
import { InstanceAdminLayout } from "layouts/admin-layout";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
// store
|
// store
|
||||||
@ -21,11 +21,21 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
|
|||||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col gap-8 my-8 mx-12 w-4/5">
|
||||||
|
<div className="pb-3 mb-2 border-b border-custom-border-100">
|
||||||
|
<div className="text-custom-text-100 font-medium text-xl pb-1">Secure emails from your own instance</div>
|
||||||
|
<div className="text-custom-text-300 font-normal text-sm">
|
||||||
|
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
|
||||||
|
</div>
|
||||||
|
<div className="text-custom-text-300 font-normal text-sm">
|
||||||
|
Set it up below and please test your settings before you save them.{" "}
|
||||||
|
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<InstanceEmailForm config={formattedConfig} />
|
<InstanceEmailForm config={formattedConfig} />
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-4 m-8">
|
<Loader className="space-y-4">
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
<Loader.Item height="50px" width="25%" />
|
<Loader.Item height="50px" width="25%" />
|
||||||
@ -38,7 +48,7 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
InstanceAdminEmailPage.getLayout = function getLayout(page: ReactElement) {
|
InstanceAdminEmailPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <InstanceAdminLayout header={<InstanceAdminHeader title="Email" />}>{page}</InstanceAdminLayout>;
|
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InstanceAdminEmailPage;
|
export default InstanceAdminEmailPage;
|
||||||
|
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 useSWR from "swr";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// layouts
|
// layouts
|
||||||
import { InstanceAdminHeader, InstanceAdminLayout } from "layouts/admin-layout";
|
import { InstanceAdminLayout } from "layouts/admin-layout";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
// store
|
// store
|
||||||
@ -21,11 +21,18 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
|
|||||||
useSWR("INSTANCE_INFO", () => fetchInstanceInfo());
|
useSWR("INSTANCE_INFO", () => fetchInstanceInfo());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col gap-8 my-8 mx-12">
|
||||||
|
<div className="pb-3 mb-2 border-b border-custom-border-100">
|
||||||
|
<div className="text-custom-text-100 font-medium text-xl pb-1">ID your instance easily</div>
|
||||||
|
<div className="text-custom-text-300 font-normal text-sm">
|
||||||
|
Change the name of your instance and instance admin e-mail addresses. If you have a paid subscription, you
|
||||||
|
will find your license key here.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{instance ? (
|
{instance ? (
|
||||||
<InstanceGeneralForm instance={instance} />
|
<InstanceGeneralForm instance={instance} />
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-4 m-8">
|
<Loader className="space-y-4">
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
<Loader.Item height="50px" width="50%" />
|
<Loader.Item height="50px" width="50%" />
|
||||||
<Loader.Item height="50px" width="25%" />
|
<Loader.Item height="50px" width="25%" />
|
||||||
@ -36,7 +43,7 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
InstanceAdminPage.getLayout = function getLayout(page: ReactElement) {
|
InstanceAdminPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <InstanceAdminLayout header={<InstanceAdminHeader title="General" />}>{page}</InstanceAdminLayout>;
|
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InstanceAdminPage;
|
export default InstanceAdminPage;
|
||||||
|
@ -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