feat: Instance Admin Panel: Configuration Settings (#2800)

* feat: Instance Admin Panel: Configuration Settings

* refactor: seprate Google and Github form into independent components.

* feat: add admin auth wrapper and access denied page.

* style: design updates.
This commit is contained in:
Prateek Shourya 2023-11-20 20:46:49 +05:30 committed by GitHub
parent f44f70168f
commit b903126e5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1144 additions and 105 deletions

View File

@ -0,0 +1,204 @@
import { FC } from "react";
import { Controller, useForm } from "react-hook-form";
// ui
import { Button, Input, ToggleSwitch } 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 IInstanceEmailForm {
config: IFormattedInstanceConfiguration;
}
export interface EmailFormValues {
EMAIL_HOST: string;
EMAIL_PORT: string;
EMAIL_HOST_USER: string;
EMAIL_HOST_PASSWORD: string;
EMAIL_USE_TLS: string;
EMAIL_USE_SSL: string;
}
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
const { config } = props;
// store
const { instance: instanceStore } = useMobxStore();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<EmailFormValues>({
defaultValues: {
EMAIL_HOST: config["EMAIL_HOST"],
EMAIL_PORT: config["EMAIL_PORT"],
EMAIL_HOST_USER: config["EMAIL_HOST_USER"],
EMAIL_HOST_PASSWORD: config["EMAIL_HOST_PASSWORD"],
EMAIL_USE_TLS: config["EMAIL_USE_TLS"],
EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
},
});
const onSubmit = async (formData: EmailFormValues) => {
const payload: Partial<EmailFormValues> = { ...formData };
await instanceStore
.updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Email 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">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>
<Controller
control={control}
name="EMAIL_HOST"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_HOST"
name="EMAIL_HOST"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_HOST)}
placeholder="Email Host"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Port</h4>
<Controller
control={control}
name="EMAIL_PORT"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_PORT"
name="EMAIL_PORT"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_PORT)}
placeholder="Email Port"
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">
<h4 className="text-sm">Username</h4>
<Controller
control={control}
name="EMAIL_HOST_USER"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_HOST_USER"
name="EMAIL_HOST_USER"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_HOST_USER)}
placeholder="Username"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Password</h4>
<Controller
control={control}
name="EMAIL_HOST_PASSWORD"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_HOST_PASSWORD"
name="EMAIL_HOST_PASSWORD"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_HOST_PASSWORD)}
placeholder="Password"
className="rounded-md font-medium w-full"
/>
)}
/>
</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>
<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>
</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

@ -52,6 +52,12 @@ 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>

View File

@ -0,0 +1,132 @@
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";
// icons
import { Copy } from "lucide-react";
export interface IInstanceGithubConfigForm {
config: IFormattedInstanceConfiguration;
}
export interface GithubConfigFormValues {
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;
}
export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) => {
const { config } = props;
// store
const { instance: instanceStore } = useMobxStore();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<GithubConfigFormValues>({
defaultValues: {
GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"],
GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"],
},
});
const onSubmit = async (formData: GithubConfigFormValues) => {
const payload: Partial<GithubConfigFormValues> = { ...formData };
await instanceStore
.updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Github Configuration Settings updated successfully",
})
)
.catch((err) => console.error(err));
};
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-1">
<h4 className="text-sm">Client ID</h4>
<Controller
control={control}
name="GITHUB_CLIENT_ID"
render={({ field: { value, onChange, ref } }) => (
<Input
id="GITHUB_CLIENT_ID"
name="GITHUB_CLIENT_ID"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.GITHUB_CLIENT_ID)}
placeholder="Github Client ID"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Client Secret</h4>
<Controller
control={control}
name="GITHUB_CLIENT_SECRET"
render={({ field: { value, onChange, ref } }) => (
<Input
id="GITHUB_CLIENT_SECRET"
name="GITHUB_CLIENT_SECRET"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.GITHUB_CLIENT_SECRET)}
placeholder="Github Client Secret"
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">
<h4 className="text-sm">Origin URL</h4>
<Button
variant="neutral-primary"
className="py-2 flex justify-between items-center"
onClick={() => {
navigator.clipboard.writeText(originURL);
setToastAlert({
message: "The Origin URL has been successfully copied to your clipboard",
type: "success",
title: "Copied to clipboard",
});
}}
>
<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>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,132 @@
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";
// icons
import { Copy } from "lucide-react";
export interface IInstanceGoogleConfigForm {
config: IFormattedInstanceConfiguration;
}
export interface GoogleConfigFormValues {
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
}
export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) => {
const { config } = props;
// store
const { instance: instanceStore } = useMobxStore();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<GoogleConfigFormValues>({
defaultValues: {
GOOGLE_CLIENT_ID: config["GOOGLE_CLIENT_ID"],
GOOGLE_CLIENT_SECRET: config["GOOGLE_CLIENT_SECRET"],
},
});
const onSubmit = async (formData: GoogleConfigFormValues) => {
const payload: Partial<GoogleConfigFormValues> = { ...formData };
await instanceStore
.updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Google Configuration Settings updated successfully",
})
)
.catch((err) => console.error(err));
};
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-1">
<h4 className="text-sm">Client ID</h4>
<Controller
control={control}
name="GOOGLE_CLIENT_ID"
render={({ field: { value, onChange, ref } }) => (
<Input
id="GOOGLE_CLIENT_ID"
name="GOOGLE_CLIENT_ID"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.GOOGLE_CLIENT_ID)}
placeholder="Google Client ID"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Client Secret</h4>
<Controller
control={control}
name="GOOGLE_CLIENT_SECRET"
render={({ field: { value, onChange, ref } }) => (
<Input
id="GOOGLE_CLIENT_SECRET"
name="GOOGLE_CLIENT_SECRET"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.GOOGLE_CLIENT_SECRET)}
placeholder="Google Client Secret"
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">
<h4 className="text-sm">Origin URL</h4>
<Button
variant="neutral-primary"
className="py-2 flex justify-between items-center"
onClick={() => {
navigator.clipboard.writeText(originURL);
setToastAlert({
message: "The Origin URL has been successfully copied to your clipboard",
type: "success",
title: "Copied to clipboard",
});
}}
>
<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>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,137 @@
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 IInstanceOpenAIForm {
config: IFormattedInstanceConfiguration;
}
export interface OpenAIFormValues {
OPENAI_API_BASE: string;
OPENAI_API_KEY: string;
GPT_ENGINE: string;
}
export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
const { config } = props;
// store
const { instance: instanceStore } = useMobxStore();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<OpenAIFormValues>({
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 };
await instanceStore
.updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Open 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="flex flex-col gap-1">
<h4 className="text-sm">GPT Engine</h4>
<Controller
control={control}
name="GPT_ENGINE"
render={({ field: { value, onChange, ref } }) => (
<Input
id="GPT_ENGINE"
name="GPT_ENGINE"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.GPT_ENGINE)}
placeholder="GPT Engine"
className="rounded-md font-medium w-full"
/>
)}
/>
</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

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { Menu, Transition } from "@headlessui/react";
import { LogOut, Settings, Shield, UserCircle2 } from "lucide-react";
import { Cog, LogIn, LogOut, Settings, UserCircle2 } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
@ -11,7 +11,7 @@ import useToast from "hooks/use-toast";
// services
import { AuthService } from "services/auth.service";
// ui
import { Avatar } from "@plane/ui";
import { Avatar, Tooltip } from "@plane/ui";
// Static Data
const profileLinks = (workspaceSlug: string, userId: string) => [
@ -70,19 +70,30 @@ export const InstanceSidebarDropdown = observer(() => {
sidebarCollapsed ? "justify-center" : ""
}`}
>
<div className={`flex-shrink-0 `}>
<Shield className="h-6 w-6 text-custom-text-100" />
<div className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`}>
<Cog className="h-5 w-5 text-custom-text-200" />
</div>
{!sidebarCollapsed && (
<h4 className="text-custom-text-100 font-medium text-base truncate">Instance Admin Settings</h4>
<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="grid place-items-center outline-none">
<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">
<Link href={`/${redirectWorkspaceSlug}`}>
<a>
<LogIn className="h-5 w-5 text-custom-text-200 rotate-180" />
</a>
</Link>
</div>
</Tooltip>
)}
<Avatar
name={currentUser?.display_name}
src={currentUser?.avatar}
@ -132,7 +143,7 @@ export const InstanceSidebarDropdown = observer(() => {
<div className="p-2 pb-0">
<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">
Normal Mode
</a>

View File

@ -1,6 +1,7 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react";
// icons
import { BrainCog, Cog, Lock, Mail } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// ui
@ -8,24 +9,28 @@ import { Tooltip } from "@plane/ui";
const INSTANCE_ADMIN_LINKS = [
{
Icon: LayoutGrid,
Icon: Cog,
name: "General",
description: "General settings here",
href: `/admin`,
},
{
Icon: BarChart2,
name: "OAuth",
href: `/admin/oauth`,
},
{
Icon: Briefcase,
Icon: Mail,
name: "Email",
description: "Email related settings will go here",
href: `/admin/email`,
},
{
Icon: CheckCircle,
name: "AI",
href: `/admin/ai`,
Icon: Lock,
name: "Authorization",
description: "Autorization",
href: `/admin/authorization`,
},
{
Icon: BrainCog,
name: "OpenAI",
description: "OpenAI configurations",
href: `/admin/openai`,
},
];
@ -37,7 +42,7 @@ export const InstanceAdminSidebarMenu = () => {
const router = useRouter();
return (
<div className="h-full overflow-y-auto w-full cursor-pointer space-y-2 p-4">
<div className="h-full overflow-y-auto w-full cursor-pointer space-y-3 p-4">
{INSTANCE_ADMIN_LINKS.map((item, index) => {
const isActive = item.name === "Settings" ? router.asPath.includes(item.href) : router.asPath === item.href;
@ -46,14 +51,29 @@ export const InstanceAdminSidebarMenu = () => {
<a className="block w-full">
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!sidebarCollapsed}>
<div
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
className={`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none ${
isActive
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapsed ? "justify-center" : ""}`}
>
{<item.Icon className="h-4 w-4" />}
{!sidebarCollapsed && item.name}
{!sidebarCollapsed && (
<div className="flex flex-col leading-snug">
<span
className={`text-sm font-medium ${
isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-200"
}`}
>
{item.name}
</span>
<span
className={`text-xs ${isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-300"}`}
>
{item.description}
</span>
</div>
)}
</div>
</Tooltip>
</a>

View File

@ -1,26 +1,17 @@
import { FC } from "react";
// next
import Link from "next/link";
// mobx
import { observer } from "mobx-react-lite";
// ui
import { Breadcrumbs } from "@plane/ui";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// icons
import { ArrowLeftToLine, Settings } from "lucide-react";
import { Settings } from "lucide-react";
export const InstanceAdminHeader: FC = observer(() => {
const {
workspace: { workspaceSlug },
user: { currentUserSettings },
} = useMobxStore();
export interface IInstanceAdminHeader {
title: string;
}
const redirectWorkspaceSlug =
workspaceSlug ||
currentUserSettings?.workspace?.last_workspace_slug ||
currentUserSettings?.workspace?.fallback_workspace_slug ||
"";
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">
@ -30,18 +21,16 @@ export const InstanceAdminHeader: FC = observer(() => {
<Breadcrumbs.BreadcrumbItem
type="text"
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
label="General"
label="Settings"
link="/admin"
/>
<Breadcrumbs.BreadcrumbItem
type="text"
label={title}
/>
</Breadcrumbs>
</div>
</div>
<div className="flex-shrink-0">
<Link href={redirectWorkspaceSlug}>
<a>
<ArrowLeftToLine className="h-4 w-4 text-custom-text-300" />
</a>
</Link>
</div>
</div>
);
});

View File

@ -1,31 +1,33 @@
import { FC, ReactNode } from "react";
// layouts
import { UserAuthWrapper } from "layouts/auth-layout";
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 } = props;
const { children, header } = props;
return (
<>
<UserAuthWrapper>
<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">
<InstanceAdminHeader />
<div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
<>{children}</>
<AdminAuthWrapper>
<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}
<div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
<>{children}</>
</div>
</div>
</div>
</main>
</div>
</main>
</div>
</AdminAuthWrapper>
</UserAuthWrapper>
</>
);

View File

@ -0,0 +1,68 @@
import { FC, ReactNode } from "react";
import Link from "next/link";
import Image from "next/image";
import { observer } from "mobx-react-lite";
// icons
import { LayoutGrid } from "lucide-react";
// ui
import { Button } from "@plane/ui";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
// images
import AccessDeniedImg from "public/auth/access-denied.svg";
export interface IAdminAuthWrapper {
children: ReactNode;
}
export const AdminAuthWrapper: FC<IAdminAuthWrapper> = observer(({ children }) => {
// store
const {
user: { isUserInstanceAdmin },
workspace: { workspaceSlug },
user: { currentUserSettings },
} = useMobxStore();
// redirect url
const redirectWorkspaceSlug =
workspaceSlug ||
currentUserSettings?.workspace?.last_workspace_slug ||
currentUserSettings?.workspace?.fallback_workspace_slug ||
"";
// 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>
<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>
</a>
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
return <>{children}</>;
});

View File

@ -1,3 +1,4 @@
export * from "./user-wrapper";
export * from "./workspace-wrapper";
export * from "./project-wrapper";
export * from "./admin-wrapper";

View File

@ -1,16 +0,0 @@
import { ReactElement } from "react";
// layouts
import { InstanceAdminLayout } from "layouts/admin-layout";
// types
import { NextPageWithLayout } from "types/app";
const InstanceAdminAIPage: NextPageWithLayout = () => {
console.log("admin page");
return <div>Admin AI Page</div>;
};
InstanceAdminAIPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
};
export default InstanceAdminAIPage;

View File

@ -0,0 +1,166 @@
import { ReactElement, useState } 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";
// 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";
const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
// store
const {
instance: { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations },
} = useMobxStore();
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
// toast
const { setToastAlert } = useToast();
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const enableSignup = formattedConfig?.ENABLE_SIGNUP ?? "0";
const updateConfig = async (value: string) => {
setIsSubmitting(true);
const payload = {
ENABLE_SIGNUP: value,
};
await updateInstanceConfigurations(payload)
.then(() => {
setToastAlert({
title: "Success",
type: "success",
message: "Authorization Settings updated successfully",
});
setIsSubmitting(false);
})
.catch((err) => {
console.error(err);
setToastAlert({
title: "Error",
type: "error",
message: "Failed to update Authorization Settings",
});
setIsSubmitting(false);
});
};
return (
<div>
{formattedConfig ? (
<div className="flex flex-col gap-8 m-8 w-4/5">
<div className="pb-2 mb-2 border-b border-custom-border-100">
<div className="text-custom-text-100 font-medium text-lg">Authorization</div>
<div className="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>
</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>
</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>
</div>
) : (
<Loader className="space-y-4 m-8">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="25%" />
</Loader>
)}
</div>
);
});
InstanceAdminAuthorizationPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout header={<InstanceAdminHeader title="Authorization" />}>{page}</InstanceAdminLayout>;
};
export default InstanceAdminAuthorizationPage;

View File

@ -1,16 +1,44 @@
import { ReactElement } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
import { InstanceAdminLayout } from "layouts/admin-layout";
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 { InstanceEmailForm } from "components/instance/email-form";
const InstanceAdminEmailPage: NextPageWithLayout = () => {
console.log("admin page");
return <div>Admin Email Page</div>;
};
const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
// store
const {
instance: { fetchInstanceConfigurations, formattedConfig },
} = useMobxStore();
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return (
<div>
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-4 m-8">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" width="25%" />
</Loader>
)}
</div>
)
});
InstanceAdminEmailPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
return <InstanceAdminLayout header={<InstanceAdminHeader title="Email" />}>{page}</InstanceAdminLayout>;
};
export default InstanceAdminEmailPage;

View File

@ -2,11 +2,13 @@ import { ReactElement } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
import { InstanceAdminLayout } from "layouts/admin-layout";
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 { InstanceGeneralForm } from "components/instance";
@ -18,11 +20,23 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_INFO", () => fetchInstanceInfo());
return <div>{instance && <InstanceGeneralForm instance={instance} />}</div>;
return (
<div>
{instance ? (
<InstanceGeneralForm instance={instance} />
) : (
<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>
);
});
InstanceAdminPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
return <InstanceAdminLayout header={<InstanceAdminHeader title="General" />}>{page}</InstanceAdminLayout>;
};
export default InstanceAdminPage;

View File

@ -1,16 +0,0 @@
import { ReactElement } from "react";
// layouts
import { InstanceAdminLayout } from "layouts/admin-layout";
// types
import { NextPageWithLayout } from "types/app";
const InstanceAdminOAuthPage: NextPageWithLayout = () => {
console.log("admin page");
return <div>Admin oauth Page</div>;
};
InstanceAdminOAuthPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
};
export default InstanceAdminOAuthPage;

View File

@ -0,0 +1,42 @@
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;

View File

@ -0,0 +1,49 @@
<svg width="632" height="269" viewBox="0 0 632 269" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2279_42539)">
<path d="M447.648 9.2942L108.777 58.3359L132.006 219.429L470.877 170.387L447.648 9.2942Z" fill="#F5F5F5"/>
<path d="M439.95 24.4973L120.459 70.7344L139.839 205.14L459.33 158.902L439.95 24.4973Z" fill="white"/>
<path d="M446.161 -0.00267782L107.291 49.0391L109.371 63.4616L448.241 14.4198L446.161 -0.00267782Z" fill="#E5E5E5"/>
<path d="M119.212 58.6596C120.701 58.6596 121.908 57.4504 121.908 55.9587C121.908 54.467 120.701 53.2578 119.212 53.2578C117.723 53.2578 116.516 54.467 116.516 55.9587C116.516 57.4504 117.723 58.6596 119.212 58.6596Z" fill="white"/>
<path d="M129.34 57.2065C130.829 57.2065 132.036 55.9973 132.036 54.5056C132.036 53.0139 130.829 51.8047 129.34 51.8047C127.852 51.8047 126.645 53.0139 126.645 54.5056C126.645 55.9973 127.852 57.2065 129.34 57.2065Z" fill="white"/>
<path d="M139.467 55.7377C140.956 55.7377 142.163 54.5285 142.163 53.0368C142.163 51.5452 140.956 50.3359 139.467 50.3359C137.978 50.3359 136.771 51.5452 136.771 53.0368C136.771 54.5285 137.978 55.7377 139.467 55.7377Z" fill="white"/>
<path d="M525.492 78.6016H183.104V241.367H525.492V78.6016Z" fill="#F1F1F1"/>
<path d="M515.702 92.5547H192.895V228.355H515.702V92.5547Z" fill="white"/>
<path d="M525.345 69.1953H182.957V83.7675H525.345V69.1953Z" fill="#E5E5E5"/>
<path d="M193.766 80.4643C195.255 80.4643 196.462 79.2551 196.462 77.7634C196.462 76.2717 195.255 75.0625 193.766 75.0625C192.277 75.0625 191.07 76.2717 191.07 77.7634C191.07 79.2551 192.277 80.4643 193.766 80.4643Z" fill="white"/>
<path d="M203.999 80.4643C205.488 80.4643 206.695 79.2551 206.695 77.7634C206.695 76.2717 205.488 75.0625 203.999 75.0625C202.51 75.0625 201.303 76.2717 201.303 77.7634C201.303 79.2551 202.51 80.4643 203.999 80.4643Z" fill="white"/>
<path d="M214.233 80.4643C215.722 80.4643 216.929 79.2551 216.929 77.7634C216.929 76.2717 215.722 75.0625 214.233 75.0625C212.744 75.0625 211.537 76.2717 211.537 77.7634C211.537 79.2551 212.744 80.4643 214.233 80.4643Z" fill="white"/>
<path d="M424.368 113.203H378.904V116.626H424.368V113.203Z" fill="#F1F1F1"/>
<path d="M449.11 123.258H378.904V126.681H449.11V123.258Z" fill="#F1F1F1"/>
<path d="M436.851 132.82H379.635V136.244H436.851V132.82Z" fill="#F1F1F1"/>
<path d="M412.306 143.328H378.904V146.751H412.306V143.328Z" fill="#F1F1F1"/>
<path d="M429.316 153.586H378.904V157.009H429.316V153.586Z" fill="#F1F1F1"/>
<path d="M363.751 112.133H358.184V117.71H363.751V112.133Z" fill="#F1F1F1"/>
<path d="M363.751 122.172H358.184V127.749H363.751V122.172Z" fill="#F1F1F1"/>
<path d="M363.751 131.75H358.184V137.327H363.751V131.75Z" fill="#F1F1F1"/>
<path d="M363.751 142.258H358.184V147.835H363.751V142.258Z" fill="#F1F1F1"/>
<path d="M363.751 152.508H358.184V158.085H363.751V152.508Z" fill="#F1F1F1"/>
<path d="M424.368 163.906H378.904V167.33H424.368V163.906Z" fill="#F1F1F1"/>
<path d="M449.11 173.945H378.904V177.369H449.11V173.945Z" fill="#F1F1F1"/>
<path d="M436.851 183.523H379.635V186.947H436.851V183.523Z" fill="#F1F1F1"/>
<path d="M412.306 194.031H378.904V197.455H412.306V194.031Z" fill="#F1F1F1"/>
<path d="M429.316 204.289H378.904V207.712H429.316V204.289Z" fill="#F1F1F1"/>
<path d="M363.751 162.836H358.184V168.413H363.751V162.836Z" fill="#F1F1F1"/>
<path d="M363.751 172.875H358.184V178.452H363.751V172.875Z" fill="#F1F1F1"/>
<path d="M363.751 182.453H358.184V188.03H363.751V182.453Z" fill="#F1F1F1"/>
<path d="M363.751 192.945H358.184V198.522H363.751V192.945Z" fill="#F1F1F1"/>
<path d="M363.751 203.219H358.184V208.796H363.751V203.219Z" fill="#F1F1F1"/>
<path d="M280.545 190.758H235.082V194.181H280.545V190.758Z" fill="#F1F1F1"/>
<path d="M286.216 199.664H229V203.087H286.216V199.664Z" fill="#F1F1F1"/>
<path d="M287.364 147.583C287.362 152.728 286.028 157.785 283.493 162.259C280.957 166.734 277.307 170.473 272.898 173.112C268.489 175.75 263.472 177.199 258.337 177.315C253.203 177.431 248.126 176.211 243.603 173.774L243.597 173.768C238.977 171.274 235.099 167.598 232.359 163.114C229.618 158.63 228.113 153.498 227.997 148.242C227.881 142.986 229.158 137.792 231.698 133.191C234.238 128.59 237.95 124.746 242.456 122.051C246.962 119.355 252.1 117.904 257.348 117.846C262.596 117.787 267.765 119.124 272.33 121.719C276.894 124.314 280.69 128.075 283.331 132.618C285.972 137.162 287.363 142.325 287.364 147.583Z" fill="#F1F1F1"/>
<path d="M257.829 146.138C262.286 146.138 265.899 142.518 265.899 138.053C265.899 133.588 262.286 129.969 257.829 129.969C253.373 129.969 249.76 133.588 249.76 138.053C249.76 142.518 253.373 146.138 257.829 146.138Z" fill="white"/>
<path d="M271.607 162.829C262.585 166.001 252.748 165.963 243.752 162.721L243.746 162.717L249.758 150.828H265.897L271.607 162.829Z" fill="white"/>
<path d="M315.893 221.387C301.828 221.387 290.385 209.209 290.385 194.24C290.385 179.272 301.828 167.094 315.893 167.094C329.959 167.094 341.402 179.272 341.402 194.24C341.402 209.209 329.959 221.387 315.893 221.387ZM315.893 173.953C305.603 173.953 297.232 183.054 297.232 194.24C297.232 205.427 305.603 214.528 315.893 214.528C326.184 214.528 334.555 205.427 334.555 194.24C334.555 183.054 326.184 173.953 315.893 173.953Z" fill="#3F76FF"/>
<rect x="275" y="194" width="80" height="74" rx="6" fill="#C5D6FF"/>
<path d="M325.307 221.118C325.308 219.442 324.864 217.795 324.018 216.348C323.173 214.901 321.958 213.706 320.499 212.886C319.039 212.065 317.388 211.65 315.715 211.681C314.041 211.713 312.407 212.191 310.979 213.066C309.552 213.941 308.383 215.181 307.593 216.66C306.803 218.138 306.421 219.8 306.486 221.475C306.55 223.151 307.059 224.778 307.961 226.191C308.862 227.604 310.123 228.75 311.614 229.512V244.268H320.172V229.512C321.717 228.723 323.014 227.522 323.921 226.04C324.827 224.559 325.307 222.856 325.307 221.118Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_2279_42539">
<rect width="632" height="269" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -2,7 +2,7 @@ import { APIService } from "services/api.service";
// helpers
import { API_BASE_URL } from "helpers/common.helper";
// types
import type { IInstance } from "types/instance";
import type { IFormattedInstanceConfiguration, IInstance, IInstanceConfiguration } from "types/instance";
export class InstanceService extends APIService {
constructor() {
@ -34,4 +34,14 @@ export class InstanceService extends APIService {
throw error;
});
}
async updateInstanceConfigurations(
data: Partial<IFormattedInstanceConfiguration>
): Promise<IInstanceConfiguration[]> {
return this.patch("/api/licenses/instances/configurations/", data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
})
}
}

View File

@ -2,7 +2,7 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"
// store
import { RootStore } from "../root";
// types
import { IInstance } from "types/instance";
import { IInstance, IInstanceConfiguration, IFormattedInstanceConfiguration } from "types/instance";
// services
import { InstanceService } from "services/instance.service";
@ -11,19 +11,21 @@ export interface IInstanceStore {
error: any | null;
// issues
instance: IInstance | null;
configurations: any | null;
configurations: IInstanceConfiguration[] | null;
// computed
formattedConfig: IFormattedInstanceConfiguration | null;
// action
fetchInstanceInfo: () => Promise<IInstance>;
updateInstanceInfo: (data: Partial<IInstance>) => Promise<IInstance>;
fetchInstanceConfigurations: () => Promise<any>;
updateInstanceConfigurations: (data: Partial<IFormattedInstanceConfiguration>) => Promise<IInstanceConfiguration[]>;
}
export class InstanceStore implements IInstanceStore {
loader: boolean = false;
error: any | null = null;
instance: IInstance | null = null;
configurations: any | null = null;
configurations: IInstanceConfiguration[] | null = null;
// service
instanceService;
rootStore;
@ -36,17 +38,31 @@ export class InstanceStore implements IInstanceStore {
instance: observable.ref,
configurations: observable.ref,
// computed
// getIssueType: computed,
formattedConfig: computed,
// actions
fetchInstanceInfo: action,
updateInstanceInfo: action,
fetchInstanceConfigurations: action,
updateInstanceConfigurations: action,
});
this.rootStore = _rootStore;
this.instanceService = new InstanceService();
}
/**
* computed value for instance configurations data for forms.
* @returns configurations in the form of {key, value} pair.
*/
get formattedConfig() {
if (!this.configurations) return null;
return this.configurations?.reduce((formData: IFormattedInstanceConfiguration, config) => {
formData[config.key] = config.value;
return formData;
}, {});
}
/**
* fetch instace info from API
*/
@ -58,7 +74,7 @@ export class InstanceStore implements IInstanceStore {
});
return instance;
} catch (error) {
console.log("Error while fetching the instance");
console.log("Error while fetching the instance info");
throw error;
}
};
@ -104,7 +120,37 @@ export class InstanceStore implements IInstanceStore {
});
return configurations;
} catch (error) {
console.log("Error while fetching the instance");
console.log("Error while fetching the instance configurations");
throw error;
}
};
/**
* update instance configurations
* @param data
*/
updateInstanceConfigurations = async (data: Partial<IFormattedInstanceConfiguration>) => {
try {
runInAction(() => {
this.loader = true;
this.error = null;
});
const response = await this.instanceService.updateInstanceConfigurations(data);
runInAction(() => {
this.loader = false;
this.error = null;
this.configurations = this.configurations ? [...this.configurations, ...response] : response;
});
return response;
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};

View File

@ -20,3 +20,17 @@ export interface IInstance {
updated_by: string | null;
primary_owner: string;
}
export interface IInstanceConfiguration {
id: string;
created_at: string;
updated_at: string;
key: string;
value: string;
created_by: string | null;
updated_by: string | null;
}
export interface IFormattedInstanceConfiguration{
[key: string]: string;
}