diff --git a/web/components/instance/email-form.tsx b/web/components/instance/email-form.tsx new file mode 100644 index 00000000000..f0729e9259a --- /dev/null +++ b/web/components/instance/email-form.tsx @@ -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 = (props) => { + const { config } = props; + // store + const { instance: instanceStore } = useMobxStore(); + // toast + const { setToastAlert } = useToast(); + // form data + const { + handleSubmit, + control, + formState: { errors, isSubmitting }, + } = useForm({ + 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 = { ...formData }; + + await instanceStore + .updateInstanceConfigurations(payload) + .then(() => + setToastAlert({ + title: "Success", + type: "success", + message: "Email Settings updated successfully", + }) + ) + .catch((err) => console.error(err)); + }; + + return ( +
+
+
Email
+
Email related settings.
+
+
+
+

Host

+ ( + + )} + /> +
+ +
+

Port

+ ( + + )} + /> +
+
+
+
+

Username

+ ( + + )} + /> +
+ +
+

Password

+ ( + + )} + /> +
+
+ +
+
+
Enable TLS
+
+
+ ( + { + Boolean(parseInt(value)) === true ? onChange("0") : onChange("1"); + }} + size="sm" + /> + )} + /> +
+
+ +
+
+
Enable SSL
+
+
+ ( + { + Boolean(parseInt(value)) === true ? onChange("0") : onChange("1"); + }} + size="sm" + /> + )} + /> +
+
+ +
+ +
+
+ ); +}; diff --git a/web/components/instance/general-form.tsx b/web/components/instance/general-form.tsx index 87a268fd229..5703ce99c4a 100644 --- a/web/components/instance/general-form.tsx +++ b/web/components/instance/general-form.tsx @@ -52,6 +52,12 @@ export const InstanceGeneralForm: FC = (props) => { return (
+
+
General
+
+ The usual things like your mail, name of instance and other stuff. +
+

Name of instance

diff --git a/web/components/instance/github-config-form.tsx b/web/components/instance/github-config-form.tsx new file mode 100644 index 00000000000..ddc0f0ea5bb --- /dev/null +++ b/web/components/instance/github-config-form.tsx @@ -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 = (props) => { + const { config } = props; + // store + const { instance: instanceStore } = useMobxStore(); + // toast + const { setToastAlert } = useToast(); + // form data + const { + handleSubmit, + control, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"], + GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"], + }, + }); + + const onSubmit = async (formData: GithubConfigFormValues) => { + const payload: Partial = { ...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 ( + <> +
+
+

Client ID

+ ( + + )} + /> +
+
+

Client Secret

+ ( + + )} + /> +
+
+
+
+

Origin URL

+ +

*paste this URL in your Github console.

+
+
+
+ +
+
+
+ + ); +}; diff --git a/web/components/instance/google-config-form.tsx b/web/components/instance/google-config-form.tsx new file mode 100644 index 00000000000..a8c8f63ea48 --- /dev/null +++ b/web/components/instance/google-config-form.tsx @@ -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 = (props) => { + const { config } = props; + // store + const { instance: instanceStore } = useMobxStore(); + // toast + const { setToastAlert } = useToast(); + // form data + const { + handleSubmit, + control, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + GOOGLE_CLIENT_ID: config["GOOGLE_CLIENT_ID"], + GOOGLE_CLIENT_SECRET: config["GOOGLE_CLIENT_SECRET"], + }, + }); + + const onSubmit = async (formData: GoogleConfigFormValues) => { + const payload: Partial = { ...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 ( + <> +
+
+

Client ID

+ ( + + )} + /> +
+
+

Client Secret

+ ( + + )} + /> +
+
+
+
+

Origin URL

+ +

*paste this URL in your Google developer console.

+
+
+
+ +
+
+
+ + ); +}; diff --git a/web/components/instance/openai-form.tsx b/web/components/instance/openai-form.tsx new file mode 100644 index 00000000000..1f1ef301c98 --- /dev/null +++ b/web/components/instance/openai-form.tsx @@ -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 = (props) => { + const { config } = props; + // store + const { instance: instanceStore } = useMobxStore(); + // toast + const { setToastAlert } = useToast(); + // form data + const { + handleSubmit, + control, + formState: { errors, isSubmitting }, + } = useForm({ + 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 = { ...formData }; + + await instanceStore + .updateInstanceConfigurations(payload) + .then(() => + setToastAlert({ + title: "Success", + type: "success", + message: "Open AI Settings updated successfully", + }) + ) + .catch((err) => console.error(err)); + }; + + return ( +
+
+
OpenAI
+
+ AI is everywhere make use it as much as you can! Learn more. +
+
+
+
+

OpenAI API Base

+ ( + + )} + /> +
+ +
+

OpenAI API Key

+ ( + + )} + /> +
+
+
+
+

GPT Engine

+ ( + + )} + /> +
+
+ +
+ +
+
+ ); +}; diff --git a/web/components/instance/sidebar-dropdown.tsx b/web/components/instance/sidebar-dropdown.tsx index 923dd8d2191..94575b5e9b4 100644 --- a/web/components/instance/sidebar-dropdown.tsx +++ b/web/components/instance/sidebar-dropdown.tsx @@ -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" : "" }`} > -
- +
+
{!sidebarCollapsed && ( -

Instance Admin Settings

+

Instance Admin

)}
{!sidebarCollapsed && ( - + + {!sidebarCollapsed && ( + +
+ + + + + +
+
+ )} {
- + Normal Mode diff --git a/web/components/instance/sidebar-menu.tsx b/web/components/instance/sidebar-menu.tsx index dbb697efbf4..7d8ca476e76 100644 --- a/web/components/instance/sidebar-menu.tsx +++ b/web/components/instance/sidebar-menu.tsx @@ -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 ( -
+
{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 = () => {
{} - {!sidebarCollapsed && item.name} + {!sidebarCollapsed && ( +
+ + {item.name} + + + {item.description} + +
+ )}
diff --git a/web/layouts/admin-layout/header.tsx b/web/layouts/admin-layout/header.tsx index a111222f378..5f2864e3235 100644 --- a/web/layouts/admin-layout/header.tsx +++ b/web/layouts/admin-layout/header.tsx @@ -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 = observer((props) => { + const { title } = props; return (
@@ -30,18 +21,16 @@ export const InstanceAdminHeader: FC = observer(() => { } - label="General" + label="Settings" + link="/admin" + /> +
-
- - - - - -
); }); diff --git a/web/layouts/admin-layout/layout.tsx b/web/layouts/admin-layout/layout.tsx index 1a1dbfa6393..9d908a91a0f 100644 --- a/web/layouts/admin-layout/layout.tsx +++ b/web/layouts/admin-layout/layout.tsx @@ -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 = (props) => { - const { children } = props; + const { children, header } = props; return ( <> -
- -
- -
-
- <>{children} + +
+ +
+ {header} +
+
+ <>{children} +
-
-
-
+ +
+ ); diff --git a/web/layouts/auth-layout/admin-wrapper.tsx b/web/layouts/auth-layout/admin-wrapper.tsx new file mode 100644 index 00000000000..37eb06a439e --- /dev/null +++ b/web/layouts/auth-layout/admin-wrapper.tsx @@ -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 = 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 ( +
+
+
+
+
+ AccessDeniedImg +

Access denied!

+
+

Sorry, but you do not have permission to view this page.

+

+ If you think there{"’"}s a mistake contact support. +

+
+
+ +
+
+
+
+ ); + } + + return <>{children}; +}); diff --git a/web/layouts/auth-layout/index.ts b/web/layouts/auth-layout/index.ts index fcd53c5eb90..60279117f69 100644 --- a/web/layouts/auth-layout/index.ts +++ b/web/layouts/auth-layout/index.ts @@ -1,3 +1,4 @@ export * from "./user-wrapper"; export * from "./workspace-wrapper"; export * from "./project-wrapper"; +export * from "./admin-wrapper"; diff --git a/web/pages/admin/ai.tsx b/web/pages/admin/ai.tsx deleted file mode 100644 index 49557c8cea7..00000000000 --- a/web/pages/admin/ai.tsx +++ /dev/null @@ -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
Admin AI Page
; -}; - -InstanceAdminAIPage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default InstanceAdminAIPage; diff --git a/web/pages/admin/authorization.tsx b/web/pages/admin/authorization.tsx new file mode 100644 index 00000000000..7f5fadbe889 --- /dev/null +++ b/web/pages/admin/authorization.tsx @@ -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(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 ( +
+ {formattedConfig ? ( +
+
+
Authorization
+
+ Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the + settings. +
+
+
+
+
Enable sign-up
+
+ Keep the doors open so people can join your workspaces. +
+
+
+ { + Boolean(parseInt(enableSignup)) === true ? updateConfig("0") : updateConfig("1"); + }} + size="sm" + disabled={isSubmitting} + /> +
+
+
+ + {({ open }) => ( +
+ + Google + {open ? : } + + + + + + +
+ )} +
+ + {({ open }) => ( +
+ + Github + {open ? : } + + + + + + +
+ )} +
+
+
+ ) : ( + + + + + + + )} +
+ ); +}); + +InstanceAdminAuthorizationPage.getLayout = function getLayout(page: ReactElement) { + return }>{page}; +}; + +export default InstanceAdminAuthorizationPage; diff --git a/web/pages/admin/email.tsx b/web/pages/admin/email.tsx index 9fc572b4499..bdb3a16d1c4 100644 --- a/web/pages/admin/email.tsx +++ b/web/pages/admin/email.tsx @@ -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
Admin Email Page
; -}; +const InstanceAdminEmailPage: NextPageWithLayout = observer(() => { + // store + const { + instance: { fetchInstanceConfigurations, formattedConfig }, + } = useMobxStore(); + + useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); + + return ( +
+ {formattedConfig ? ( + + ) : ( + + + + + + + + )} +
+ ) +}); InstanceAdminEmailPage.getLayout = function getLayout(page: ReactElement) { - return {page}; + return }>{page}; }; export default InstanceAdminEmailPage; diff --git a/web/pages/admin/index.tsx b/web/pages/admin/index.tsx index 70ffd0cc1c2..ba6113a4bab 100644 --- a/web/pages/admin/index.tsx +++ b/web/pages/admin/index.tsx @@ -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
{instance && }
; + return ( +
+ {instance ? ( + + ) : ( + + + + + + )} +
+ ); }); InstanceAdminPage.getLayout = function getLayout(page: ReactElement) { - return {page}; + return }>{page}; }; export default InstanceAdminPage; diff --git a/web/pages/admin/oauth.tsx b/web/pages/admin/oauth.tsx deleted file mode 100644 index 56bb8fc17b9..00000000000 --- a/web/pages/admin/oauth.tsx +++ /dev/null @@ -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
Admin oauth Page
; -}; - -InstanceAdminOAuthPage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default InstanceAdminOAuthPage; diff --git a/web/pages/admin/openai.tsx b/web/pages/admin/openai.tsx new file mode 100644 index 00000000000..214a14c4634 --- /dev/null +++ b/web/pages/admin/openai.tsx @@ -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 ( +
+ {formattedConfig ? ( + + ) : ( + + + + + + )} +
+ ); +}); + +InstanceAdminOpenAIPage.getLayout = function getLayout(page: ReactElement) { + return }>{page}; +}; + +export default InstanceAdminOpenAIPage; diff --git a/web/public/auth/access-denied.svg b/web/public/auth/access-denied.svg new file mode 100644 index 00000000000..c7979fee20d --- /dev/null +++ b/web/public/auth/access-denied.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/services/instance.service.ts b/web/services/instance.service.ts index 74c32aa5f06..b9937f2bf40 100644 --- a/web/services/instance.service.ts +++ b/web/services/instance.service.ts @@ -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 + ): Promise { + return this.patch("/api/licenses/instances/configurations/", data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }) + } } diff --git a/web/store/instance/instance.store.ts b/web/store/instance/instance.store.ts index bd37110a154..a2cf0c185f7 100644 --- a/web/store/instance/instance.store.ts +++ b/web/store/instance/instance.store.ts @@ -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; updateInstanceInfo: (data: Partial) => Promise; fetchInstanceConfigurations: () => Promise; + updateInstanceConfigurations: (data: Partial) => Promise; } 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) => { + 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; } }; diff --git a/web/types/instance.d.ts b/web/types/instance.d.ts index 6ba32b13894..0b985279793 100644 --- a/web/types/instance.d.ts +++ b/web/types/instance.d.ts @@ -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; +}