diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index ecab0740c..b43586509 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -40,11 +40,8 @@ class InstanceEndpoint(BaseAPIView): return Response({"activated": False}, status=status.HTTP_400_BAD_REQUEST) # Return instance serializer = InstanceSerializer(instance) - data = { - "data": serializer.data, - "activated": True, - } - return Response(data, status=status.HTTP_200_OK) + serializer.data["activated"] = True + return Response(serializer.data, status=status.HTTP_200_OK) def patch(self, request): # Get the instance diff --git a/web/components/instance/general-form.tsx b/web/components/instance/general-form.tsx new file mode 100644 index 000000000..10e654cd7 --- /dev/null +++ b/web/components/instance/general-form.tsx @@ -0,0 +1,45 @@ +import { FC } from "react"; +import { useForm } from "react-hook-form"; +// ui +import { Input } from "@plane/ui"; +// types +import { IInstance } from "types/instance"; + +export interface IInstanceGeneralForm { + data: IInstance; +} + +export interface GeneralFormValues { + instance_name: string; + namespace: string | null; + is_telemetry_enabled: boolean; +} + +export const InstanceGeneralForm: FC = (props) => { + const { data } = props; + + const {} = useForm({ + defaultValues: { + instance_name: data.instance_name, + namespace: data.namespace, + is_telemetry_enabled: data.is_telemetry_enabled, + }, + }); + + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ); +}; diff --git a/web/components/instance/help-section.tsx b/web/components/instance/help-section.tsx new file mode 100644 index 000000000..4093f9ffd --- /dev/null +++ b/web/components/instance/help-section.tsx @@ -0,0 +1,134 @@ +import { FC, useState, useRef } from "react"; +import { Transition } from "@headlessui/react"; +import Link from "next/link"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// icons +import { FileText, HelpCircle, MessagesSquare, MoveLeft } from "lucide-react"; +import { DiscordIcon, GithubIcon } from "@plane/ui"; +// assets +import packageJson from "package.json"; + +const helpOptions = [ + { + name: "Documentation", + href: "https://docs.plane.so/", + Icon: FileText, + }, + { + name: "Join our Discord", + href: "https://discord.com/invite/A92xrEGCge", + Icon: DiscordIcon, + }, + { + name: "Report a bug", + href: "https://github.com/makeplane/plane/issues/new/choose", + Icon: GithubIcon, + }, + { + name: "Chat with us", + href: null, + onClick: () => (window as any).$crisp.push(["do", "chat:show"]), + Icon: MessagesSquare, + }, +]; + +export const InstanceHelpSection: FC = () => { + // states + const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); + // store + const { + theme: { sidebarCollapsed, toggleSidebar }, + } = useMobxStore(); + // refs + const helpOptionsRef = useRef(null); + + return ( +
+
+ + + +
+ +
+ +
+
+ {helpOptions.map(({ name, Icon, href, onClick }) => { + if (href) + return ( + + +
+ +
+ {name} +
+ + ); + else + return ( + + ); + })} +
+
Version: v{packageJson.version}
+
+
+
+
+ ); +}; diff --git a/web/components/instance/index.ts b/web/components/instance/index.ts new file mode 100644 index 000000000..52f879062 --- /dev/null +++ b/web/components/instance/index.ts @@ -0,0 +1,3 @@ +export * from "./help-section"; +export * from "./sidebar-menu"; +export * from "./general-form"; diff --git a/web/components/instance/sidebar-menu.tsx b/web/components/instance/sidebar-menu.tsx new file mode 100644 index 000000000..0eeaa7676 --- /dev/null +++ b/web/components/instance/sidebar-menu.tsx @@ -0,0 +1,65 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; +import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { Tooltip } from "@plane/ui"; + +const INSTANCE_ADMIN_LINKS = [ + { + Icon: LayoutGrid, + name: "General", + href: `/admin`, + }, + { + Icon: BarChart2, + name: "OAuth", + href: `/admin/oauth`, + }, + { + Icon: Briefcase, + name: "Email", + href: `/admin/email`, + }, + { + Icon: CheckCircle, + name: "AI", + href: `/admin/ai`, + }, +]; + +export const InstanceAdminSidebarMenu = () => { + const { + theme: { sidebarCollapsed }, + } = useMobxStore(); + // router + const router = useRouter(); + + return ( +
+ {INSTANCE_ADMIN_LINKS.map((item, index) => { + const isActive = item.name === "Settings" ? router.asPath.includes(item.href) : router.asPath === item.href; + + return ( + + + +
+ {} + {!sidebarCollapsed && item.name} +
+
+
+ + ); + })} +
+ ); +}; diff --git a/web/layouts/admin-layout/sidebar.tsx b/web/layouts/admin-layout/sidebar.tsx index 0960389d4..c067354fc 100644 --- a/web/layouts/admin-layout/sidebar.tsx +++ b/web/layouts/admin-layout/sidebar.tsx @@ -1,13 +1,13 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components -import { WorkspaceHelpSection } from "components/workspace"; +import { InstanceAdminSidebarMenu, InstanceHelpSection } from "components/instance"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -export interface IAppSidebar {} +export interface IInstanceAdminSidebar {} -export const InstanceAdminSidebar: FC = observer(() => { +export const InstanceAdminSidebar: FC = observer(() => { // store const { theme: themStore } = useMobxStore(); @@ -19,7 +19,8 @@ export const InstanceAdminSidebar: FC = observer(() => { } ${themStore?.sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`} >
- + +
); diff --git a/web/pages/admin/ai.tsx b/web/pages/admin/ai.tsx new file mode 100644 index 000000000..49557c8ce --- /dev/null +++ b/web/pages/admin/ai.tsx @@ -0,0 +1,16 @@ +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/email.tsx b/web/pages/admin/email.tsx new file mode 100644 index 000000000..9fc572b44 --- /dev/null +++ b/web/pages/admin/email.tsx @@ -0,0 +1,16 @@ +import { ReactElement } from "react"; +// layouts +import { InstanceAdminLayout } from "layouts/admin-layout"; +// types +import { NextPageWithLayout } from "types/app"; + +const InstanceAdminEmailPage: NextPageWithLayout = () => { + console.log("admin page"); + return
Admin Email Page
; +}; + +InstanceAdminEmailPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default InstanceAdminEmailPage; diff --git a/web/pages/admin/index.tsx b/web/pages/admin/index.tsx index 49133ecd5..96d1091ef 100644 --- a/web/pages/admin/index.tsx +++ b/web/pages/admin/index.tsx @@ -1,13 +1,25 @@ 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"; +// components +import { InstanceGeneralForm } from "components/instance"; -const InstanceAdminPage: NextPageWithLayout = () => { - console.log("admin page"); - return
Admin Page
; -}; +const InstanceAdminPage: NextPageWithLayout = observer(() => { + // store + const { + instance: { fetchInstanceInfo, instance }, + } = useMobxStore(); + + useSWR("INSTANCE_INFO", () => fetchInstanceInfo()); + + return
{instance && }
; +}); InstanceAdminPage.getLayout = function getLayout(page: ReactElement) { return {page}; diff --git a/web/pages/admin/oauth.tsx b/web/pages/admin/oauth.tsx new file mode 100644 index 000000000..56bb8fc17 --- /dev/null +++ b/web/pages/admin/oauth.tsx @@ -0,0 +1,16 @@ +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/services/instance.service.ts b/web/services/instance.service.ts new file mode 100644 index 000000000..281069bf4 --- /dev/null +++ b/web/services/instance.service.ts @@ -0,0 +1,27 @@ +import { APIService } from "services/api.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; +// types +import type { IInstance } from "types/instance"; + +export class InstanceService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getInstanceInfo(): Promise { + return this.get("/api/licenses/instances/") + .then((response) => response.data) + .catch((error) => { + throw error; + }); + } + + async getInstanceConfigurations() { + return this.get("/api/licenses/instances/configurations/") + .then((response) => response.data) + .catch((error) => { + throw error; + }); + } +} diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index 30126c2ee..98d85ec8a 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -96,7 +96,7 @@ export class WorkspaceService extends APIService { } async joinWorkspace(workspaceSlug: string, invitationId: string, data: any, user: IUser | undefined): Promise { - return this.post(`/api/users/me/invitations/workspaces/${workspaceSlug}/${invitationId}/join/`, data, { + return this.post(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/join/`, data, { headers: {}, }) .then((response) => { @@ -109,7 +109,7 @@ export class WorkspaceService extends APIService { } async joinWorkspaces(data: any): Promise { - return this.post("/api/users/me/invitations/workspaces/", data) + return this.post("/api/users/me/workspaces/invitations/", data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -125,7 +125,7 @@ export class WorkspaceService extends APIService { } async userWorkspaceInvitations(): Promise { - return this.get("/api/users/me/invitations/workspaces/") + return this.get("/api/users/me/workspaces/invitations/") .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/store/instance/index.ts b/web/store/instance/index.ts new file mode 100644 index 000000000..96a0e600f --- /dev/null +++ b/web/store/instance/index.ts @@ -0,0 +1 @@ +export * from "./instance.store"; diff --git a/web/store/instance/instance.store.ts b/web/store/instance/instance.store.ts new file mode 100644 index 000000000..78740c11e --- /dev/null +++ b/web/store/instance/instance.store.ts @@ -0,0 +1,73 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// store +import { RootStore } from "../root"; +// types +import { IInstance } from "types/instance"; +// services +import { InstanceService } from "services/instance.service"; + +export interface IInstanceStore { + loader: boolean; + error: any | null; + // issues + instance: IInstance | null; + configurations: any | null; + // computed + // action + fetchInstanceInfo: () => Promise; + fetchInstanceConfigurations: () => Promise; +} + +export class InstanceStore implements IInstanceStore { + loader: boolean = false; + error: any | null = null; + instance: IInstance | null = null; + configurations: any | null = null; + // service + instanceService; + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + instance: observable.ref, + configurations: observable.ref, + // computed + // getIssueType: computed, + // actions + fetchInstanceInfo: action, + fetchInstanceConfigurations: action, + }); + + this.rootStore = _rootStore; + this.instanceService = new InstanceService(); + } + + fetchInstanceInfo = async () => { + try { + const instance = await this.instanceService.getInstanceInfo(); + runInAction(() => { + this.instance = instance; + }); + return instance; + } catch (error) { + console.log("Error while fetching the instance"); + throw error; + } + }; + + fetchInstanceConfigurations = async () => { + try { + const configurations = await this.instanceService.getInstanceConfigurations(); + runInAction(() => { + this.configurations = configurations; + }); + return configurations; + } catch (error) { + console.log("Error while fetching the instance"); + throw error; + } + }; +} diff --git a/web/store/root.ts b/web/store/root.ts index f7c3f49c4..3bebdcd70 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -1,5 +1,6 @@ import { enableStaticRendering } from "mobx-react-lite"; // store imports +import { InstanceStore, IInstanceStore } from "./instance"; import AppConfigStore, { IAppConfigStore } from "./app-config.store"; import CommandPaletteStore, { ICommandPaletteStore } from "./command-palette.store"; import UserStore, { IUserStore } from "store/user.store"; @@ -116,6 +117,8 @@ import { IMentionsStore, MentionsStore } from "store/editor"; enableStaticRendering(typeof window === "undefined"); export class RootStore { + instance: IInstanceStore; + user: IUserStore; theme: IThemeStore; appConfig: IAppConfigStore; @@ -184,6 +187,8 @@ export class RootStore { mentionsStore: IMentionsStore; constructor() { + this.instance = new InstanceStore(this); + this.appConfig = new AppConfigStore(this); this.commandPalette = new CommandPaletteStore(this); this.user = new UserStore(this); diff --git a/web/types/instance.d.ts b/web/types/instance.d.ts new file mode 100644 index 000000000..6ba32b138 --- /dev/null +++ b/web/types/instance.d.ts @@ -0,0 +1,22 @@ +import { IUserLite } from "./users"; + +export interface IInstance { + id: string; + primary_owner_details: IUserLite; + created_at: string; + updated_at: string; + instance_name: string; + whitelist_emails: string | null; + instance_id: string; + license_key: string | null; + api_key: string; + version: string; + primary_email: string; + last_checked_at: string; + namespace: string | null; + is_telemetry_enabled: boolean; + is_support_required: boolean; + created_by: string | null; + updated_by: string | null; + primary_owner: string; +}