feat: session auth implementation (#4411)
* feat: session authentication and god-mode implementation (#4302) * dev: move authentication to base class for credentials * chore: new account creation * dev: return error as query parameter * dev: accounts and profile endpoints for user * fix: user store updates * fix: store fixes * fix: type fixes * dev: set is_password_autoset and is_email_verifier for auth providers * dev: move all auth configuration to different apps * dev: fix circular imports * dev: remove unused imports * dev: fix imports for authentication * dev: update endpoints to use rest framework api viewa * fix: onboarding fixes * dev: session model changes * fix: session model and add check for last name first name and avatar * dev: fix referer redirect * dev: remove auth imports * dev: fix imports * dev: update migrations * fix: instance admin login * comflict: conflicts resolved * dev: fix import errors and email check endpoint * fix: error messages and redirects after login * dev: configs api * fix: is github enabled boolean * dev: merge config and instance api * conflict: merge conflict resolved * dev: instance admin sign up endpoint * dev: enable magic link login * dev: configure instance variables for github and google enabled * chore: typo fixes * fix: god mode docker file changes * build-error: resolved build errors * fix: docker compose changes * dev: add email credential check endpoint * fix: minor package changes * fix: docker related changes * dev: add nginx rules in the nginx template * dev: refactor the url patterns * fix: docker changes * fix: docker files for god-mode * fix: static export * fix: nginx conf * dev: smtp sender refused exception * fix: godmode fixes * chore: god mode revamp. * dev: add csrf secured flag * fix: oauth redirect uri and session settings * chore: god mode app changes. (#3982) * chore: send test email functionality. * style: authentication methods page UI revamp. * chore: create workspace popup. * fix: user me endpoint * dev: fix redirection after authentication * dev: handle god mode redirection * fix: redirections * fix: auth related hooks * fix: store related fixes * dev: fix session authentication for rest apis * fix: linting errors * fix: removing references of useStore= * dev: fix redirection and password validation * dev: add useUser hook * fix: build fixes and lint issues * fix: removing useApplication hook * fix: build errors * fix: delete unused files * fix: auth build fixes * fix: bugfixes * dev: alter avatar to support more than 255 chars * dev: fix profile endpoint and increase session expiry time and update session on every request * chore: resolved the migration * chore: resolved merge conflicts * dev: error codes and error messages for the auth flow * dev: instance admin sign up and sign in endpoint * dev: use zxcvbn to validate password strength * dev: add extra parameters when error handling on instance god mode * chore: auth init * chore: signin/ signup form ui updates and password strength meter. * chore: update password fields. * chore: validations and error handling. * chore: updated sign-up form * chore: updated workflow and updated the code structure * chore: instance empty state for god-mode. * chore: instance and auth wrappers update * fix: renaming godmode * fix: docker changes * chore: updated authentication wrappers * chore: updated the authentication workflow and rendered all pages * fix: build errors * fix: docker related fixes * fix: tailing slash added to space and admin for valid nginx locations * chore: seperate pages for signup and login * git-action modified for admin file changes * feature build action updated for admin app * self host modified * chore: resolved build errors and handled signin and signup in a seperate route * chore: sign-in and sign-up revamp. * fix: migration conflicts * dev: migrations * chore: handled redirection * dev: admin url * dev: create seperate endpoint for instance admin me * dev: instance admin endpoint * git action fixed * chore: handled auth wrappers * dev: add serializer and remove print logs * fix: build errors * dev: fix migrations * dev: instance folder structuring * fix: linting errors * chore: resolved build errors * chore: updated store and auth workflow and updates api service types * chore: Replaced Next Link with Anchoer tag for god-mode redirection * add 3333 port to allowed origins * make password login working again * dev: fix redirection, add admin signout endpoint and fix email credential check endpoint * fix unique code sign in * fix small build error * enable sign out * dev: add google client secret variable to configure instance * dev: add referer for redirection * fix origin urls for oauths * admin setup and login separation * dev: fix user redirection and tour completed endpoint * fix build errors * dev: add set password endpoint * dev: remove user creation logic for redirection * fix unique code page * fix forgot password * chore: onboarding revamp. * dev: fix workspace slug redirection in login * chore: invited user onboarding flow update. * chore: fix switch or delete account modal. * fix members exception * refactor auth flows and add invitations to auth flow * fix sig in sign up url * fix action url * fix build errors * dev: fix user set password when logging in * dev: reset password endpoint * chore: confirm password validation for signup and onboarding. * enable reset password * fix build error * chore: minor UI updates. * chore: forgot and reset password UI revamp. * fix authentication re directions * dev: auth redirections * change url paths for signup and signin * dev: make the user logged in when changing passwords * dev: next path redirection for web and space app * dev: next path for magic sign in endpoint * dev: github space endpoint * chore: minor ui updates and fixes in web app. * set password screen * fix multiple unique code generation * dev: next path base redirection * dev: remove print logs * dev: auth space endpoints * fix build errors * dev: invalidate cache on configuration update, god mode exception errors and authentication failed code * dev: fix space endpoints and add extra endpoints * chore: space auth revamp. * dev: add sign up for space app * fix: build errors. * fix: auth redirection logic. * chore: space app onboarding revamp. --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Co-authored-by: Manish Gupta <manish@mgupta.me> Co-authored-by: = <=> Co-authored-by: rahulramesha <rahulramesham@gmail.com> * chore: updated file structure for admin * chore: updated admin-sidebar * chore: auth error handling * chore: onboarding UI updates and dark mode fixes. * chore: add `user personalization` step to onboarding profile setup screen. * chore: fix minor UI bugs * chore: authentication workflow changes * chore: handled signin workflow * style: switch or delete account workflow * chore: god mode redirection URL * feat(dashboard): improve label readability (#4321) change none label for all time in dashbard filters * chore: god-mode redirection * chore: onboarding ui updates and accept invitation workflow updates. * chore: rename unique code auth form. * style: space auth ux copy. * chore: updated intance and auth wrapper logic * chore: update default layout style. * chore: update confirm password. * chore: backend redirection * style: update banner ui * chore: minor ui updates and validation fix. * chore: removed old auth hook * chore: handled auth wrapper * chore: handled store loaders in the user * chore: handled logs * chore: add loading spinners for all auth and onboarding form buttons. * chore: add background pattern in admin auth forms and minor ui fixes. * chore: UI changes and revamp components for authentication * chore: auth UI consistency in web, space and admin. * chore: resolved build errors * chore: removed old auth hooks * chore: handled lint errors in use accounts * chore: updated authentication wrapper logic in web app * [WEB -1149] dev: update dependencies (#4333) * dev: upgrade dependencies remove unwanted dependency and add ruff as local dependency * dev: add comments * chore: authentication wrapper fetch user * chore: updated store loader * chore: removed old auth wrapper and replaced the imports with new auth wrapper * chore: join workspace invitation workflow updates * chore: build error resolved in deploy * chore: handled onboarding step error in web app * chore: SMTP Name and Password validation removed * chore: handled seo and signout logic and new user popup * chore: added redirection to plane in the sidebar * chore: resolved build errors * dev: admin session cookie update * chore: updated cookie session time for admin * dev: add start date and end date to projects (#4355) * chore: add email security dropdown and remove SMTP username and password validation. * chore: add tooltip to admin sidebar help-section. * chore: add dropdown to collapsed admin sidebar. * chore: profile themning * chore: updated page error messages and theme in command palette * dev: add email validation in email check apis * dev: remove start date and end date from project * chore: updated space folder structure and updated the store hooks * dev: error codes for authentication * chore: handled authentication in space and web apps * chore: banner redirect handling the email * dev: god mode error codes * chore: updated error codes * chore: updated onboarding images * dev: signout endpoints and saving login domain while creating sessions * feat: Self Host Data Backup (#4383) * feat: implemented backup , support for docker-compose tool, readme updated * minor fix in shell script * codacy fixes * chore: handled build errors in web * chore: updated react, react-dom, and next versions * chore: updated password autioset in the signin * dev: add logo prop to views and pages * chore: updated api service and handled the set password in store * chore: handled build errors and code cleanup * dev: return 401 when the session is not valid * dev: users/me exception for api * chore: installed lodash in space app * dev: add auth route in nginx --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Co-authored-by: Manish Gupta <manish@mgupta.me> Co-authored-by: rahulramesha <rahulramesham@gmail.com> Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> Co-authored-by: Daniel Alba <56451942+redrum15@users.noreply.github.com> Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
This commit is contained in:
parent
ae43d05714
commit
59335618b4
903 changed files with 25736 additions and 16041 deletions
131
admin/components/admin-sidebar/help-section.tsx
Normal file
131
admin/components/admin-sidebar/help-section.tsx
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState, useRef } from "react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks";
|
||||
// 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,
|
||||
},
|
||||
];
|
||||
|
||||
export const HelpSection: FC = () => {
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
// store
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
// refs
|
||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
|
||||
isSidebarCollapsed ? "flex-col" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
|
||||
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
|
||||
<a
|
||||
href={redirectionLink}
|
||||
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded border border-custom-primary-100/20 bg-custom-primary-100/10 text-xs text-custom-primary-200 whitespace-nowrap`}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
{!isSidebarCollapsed && "Redirect to plane"}
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className={`ml-auto grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
isSidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||
>
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
isSidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||
>
|
||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Transition
|
||||
show={isNeedHelpOpen}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className={`absolute bottom-2 min-w-[10rem] ${
|
||||
isSidebarCollapsed ? "left-full" : "-left-[75px]"
|
||||
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
|
||||
ref={helpOptionsRef}
|
||||
>
|
||||
<div className="space-y-1 pb-2">
|
||||
{helpOptions.map(({ name, Icon, href }) => {
|
||||
if (href)
|
||||
return (
|
||||
<Link href={href} key={name} target="_blank">
|
||||
<div className="flex items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80">
|
||||
<div className="grid flex-shrink-0 place-items-center">
|
||||
<Icon className="h-3.5 w-3.5 text-custom-text-200" size={14} />
|
||||
</div>
|
||||
<span className="text-xs">{name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<div className="grid flex-shrink-0 place-items-center">
|
||||
<Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</div>
|
||||
<span className="text-xs">{name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="px-2 pb-1 pt-2 text-[10px]">Version: v{packageJson.version}</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
5
admin/components/admin-sidebar/index.ts
Normal file
5
admin/components/admin-sidebar/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export * from "./root";
|
||||
export * from "./help-section";
|
||||
export * from "./sidebar-menu";
|
||||
export * from "./sidebar-dropdown";
|
||||
export * from "./sidebar-menu-hamburger-toogle";
|
||||
57
admin/components/admin-sidebar/root.tsx
Normal file
57
admin/components/admin-sidebar/root.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
|
||||
|
||||
export interface IInstanceSidebar {}
|
||||
|
||||
export const InstanceSidebar: FC<IInstanceSidebar> = observer(() => {
|
||||
// store
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (isSidebarCollapsed === false) {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar(!isSidebarCollapsed);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth <= 768) {
|
||||
toggleSidebar(true);
|
||||
}
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [toggleSidebar]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
|
||||
fixed md:relative
|
||||
${isSidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
sm:${isSidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
md:ml-0 ${isSidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||
lg:ml-0 ${isSidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||
`}
|
||||
>
|
||||
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
|
||||
<SidebarDropdown />
|
||||
<SidebarMenu />
|
||||
<HelpSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
147
admin/components/admin-sidebar/sidebar-dropdown.tsx
Normal file
147
admin/components/admin-sidebar/sidebar-dropdown.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
"use client";
|
||||
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useTheme as useNextTheme } from "next-themes";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { LogOut, UserCog2, Palette } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Avatar } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme, useUser } from "@/hooks";
|
||||
// helpers
|
||||
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { AuthService } from "@/services";
|
||||
|
||||
// service initialization
|
||||
const authService = new AuthService();
|
||||
|
||||
export const SidebarDropdown = observer(() => {
|
||||
// store hooks
|
||||
const { isSidebarCollapsed } = useTheme();
|
||||
const { currentUser, signOut } = useUser();
|
||||
// hooks
|
||||
const { resolvedTheme, setTheme } = useNextTheme();
|
||||
// state
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
|
||||
const handleThemeSwitch = () => {
|
||||
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||
setTheme(newTheme);
|
||||
};
|
||||
|
||||
const handleSignOut = () => signOut();
|
||||
|
||||
const getSidebarMenuItems = () => (
|
||||
<Menu.Items
|
||||
className={cn(
|
||||
"absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-custom-sidebar-border-100 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none",
|
||||
{
|
||||
"left-4": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleThemeSwitch}
|
||||
>
|
||||
<Palette className="h-4 w-4 stroke-[1.5]" />
|
||||
Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={handleSignOut}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="submit"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
</Menu.Item>
|
||||
</form>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
return (
|
||||
<div className="flex max-h-[3.75rem] items-center gap-x-5 gap-y-2 border-b border-custom-sidebar-border-200 px-4 py-3.5">
|
||||
<div className="h-full w-full truncate">
|
||||
<div
|
||||
className={`flex flex-grow items-center gap-x-2 truncate rounded py-1 ${
|
||||
isSidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<Menu as="div" className="flex-shrink-0">
|
||||
<Menu.Button
|
||||
className={cn("grid place-items-center outline-none", {
|
||||
"cursor-default": !isSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded bg-custom-sidebar-background-80">
|
||||
<UserCog2 className="h-5 w-5 text-custom-text-200" />
|
||||
</div>
|
||||
</Menu.Button>
|
||||
{isSidebarCollapsed && (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
{getSidebarMenuItems()}
|
||||
</Transition>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
{!isSidebarCollapsed && (
|
||||
<div className="flex w-full gap-2">
|
||||
<h4 className="grow truncate text-base font-medium text-custom-text-200">Instance admin</h4>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isSidebarCollapsed && currentUser && (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
<Menu.Button className="grid place-items-center outline-none">
|
||||
<Avatar
|
||||
name={currentUser.display_name}
|
||||
src={currentUser.avatar ?? undefined}
|
||||
size={24}
|
||||
shape="square"
|
||||
className="!text-base"
|
||||
/>
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
{getSidebarMenuItems()}
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks";
|
||||
// icons
|
||||
import { Menu } from "lucide-react";
|
||||
|
||||
export const SidebarHamburgerToggle: FC = observer(() => {
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
return (
|
||||
<div
|
||||
className="w-7 h-7 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
|
||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||
>
|
||||
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
104
admin/components/admin-sidebar/sidebar-menu.tsx
Normal file
104
admin/components/admin-sidebar/sidebar-menu.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
const INSTANCE_ADMIN_LINKS = [
|
||||
{
|
||||
Icon: Cog,
|
||||
name: "General",
|
||||
description: "Identify your instances and get key details",
|
||||
href: `/general/`,
|
||||
},
|
||||
{
|
||||
Icon: Mail,
|
||||
name: "Email",
|
||||
description: "Set up emails to your users",
|
||||
href: `/email/`,
|
||||
},
|
||||
{
|
||||
Icon: Lock,
|
||||
name: "Authentication",
|
||||
description: "Configure authentication modes",
|
||||
href: `/authentication/`,
|
||||
},
|
||||
{
|
||||
Icon: BrainCog,
|
||||
name: "Artificial intelligence",
|
||||
description: "Configure your OpenAI creds",
|
||||
href: `/ai/`,
|
||||
},
|
||||
{
|
||||
Icon: Image,
|
||||
name: "Images in Plane",
|
||||
description: "Allow third-party image libraries",
|
||||
href: `/image/`,
|
||||
},
|
||||
];
|
||||
|
||||
export const SidebarMenu = observer(() => {
|
||||
// store hooks
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
// router
|
||||
const pathName = usePathname();
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar(!isSidebarCollapsed);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-auto px-4 py-4">
|
||||
{INSTANCE_ADMIN_LINKS.map((item, index) => {
|
||||
const isActive = item.href === pathName || pathName.includes(item.href);
|
||||
return (
|
||||
<Link key={index} href={item.href} onClick={handleItemClick}>
|
||||
<div>
|
||||
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
||||
<div
|
||||
className={cn(
|
||||
`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none transition-colors`,
|
||||
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",
|
||||
isSidebarCollapsed ? "justify-center" : "w-[260px]"
|
||||
)}
|
||||
>
|
||||
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
|
||||
{!isSidebarCollapsed && (
|
||||
<div className="w-full ">
|
||||
<div
|
||||
className={cn(
|
||||
`text-sm font-medium transition-colors`,
|
||||
isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-200"
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
`text-[10px] transition-colors`,
|
||||
isActive ? "text-custom-primary-90" : "text-custom-sidebar-text-400"
|
||||
)}
|
||||
>
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
90
admin/components/auth-header.tsx
Normal file
90
admin/components/auth-header.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// ui
|
||||
import { Settings } from "lucide-react";
|
||||
// icons
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "components/common";
|
||||
import { SidebarHamburgerToggle } from "@/components/admin-sidebar";
|
||||
|
||||
export const InstanceHeader: FC = observer(() => {
|
||||
const pathName = usePathname();
|
||||
|
||||
const getHeaderTitle = (pathName: string) => {
|
||||
switch (pathName) {
|
||||
case "general":
|
||||
return "General";
|
||||
case "ai":
|
||||
return "Artificial Intelligence";
|
||||
case "email":
|
||||
return "Email";
|
||||
case "authentication":
|
||||
return "Authentication";
|
||||
case "image":
|
||||
return "Image";
|
||||
case "google":
|
||||
return "Google";
|
||||
case "github":
|
||||
return "Github";
|
||||
default:
|
||||
return pathName.toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
// Function to dynamically generate breadcrumb items based on pathname
|
||||
const generateBreadcrumbItems = (pathname: string) => {
|
||||
const pathSegments = pathname.split("/").slice(1); // removing the first empty string.
|
||||
pathSegments.pop();
|
||||
|
||||
let currentUrl = "";
|
||||
const breadcrumbItems = pathSegments.map((segment) => {
|
||||
currentUrl += "/" + segment;
|
||||
return {
|
||||
title: getHeaderTitle(segment),
|
||||
href: currentUrl,
|
||||
};
|
||||
});
|
||||
return breadcrumbItems;
|
||||
};
|
||||
|
||||
const breadcrumbItems = generateBreadcrumbItems(pathName);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-sidebar-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<SidebarHamburgerToggle />
|
||||
{breadcrumbItems.length >= 0 && (
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href="/general/"
|
||||
label="Settings"
|
||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{breadcrumbItems.map(
|
||||
(item) =>
|
||||
item.title && (
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
key={item.title}
|
||||
type="text"
|
||||
link={<BreadcrumbLink href={item.href} label={item.title} />}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
32
admin/components/common/banner.tsx
Normal file
32
admin/components/common/banner.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { FC } from "react";
|
||||
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
||||
|
||||
type TBanner = {
|
||||
type: "success" | "error";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const Banner: FC<TBanner> = (props) => {
|
||||
const { type, message } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-red-500/5 border-red-400" : "bg-green-500/5 border-green-400"}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex-shrink-0">
|
||||
{type === "error" ? (
|
||||
<span className="flex items-center justify-center h-6 w-6 rounded-full">
|
||||
<AlertCircle className="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</span>
|
||||
) : (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-1">
|
||||
<p className={`text-sm font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
36
admin/components/common/breadcrumb-link.tsx
Normal file
36
admin/components/common/breadcrumb-link.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Link from "next/link";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
href?: string;
|
||||
icon?: React.ReactNode | undefined;
|
||||
};
|
||||
|
||||
export const BreadcrumbLink: React.FC<Props> = (props) => {
|
||||
const { href, label, icon } = props;
|
||||
return (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
{href ? (
|
||||
<Link
|
||||
className="flex items-center gap-1 text-sm font-medium text-custom-text-300 hover:text-custom-text-100"
|
||||
href={href}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
|
||||
)}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
|
||||
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
83
admin/components/common/confirm-discard-modal.tsx
Normal file
83
admin/components/common/confirm-discard-modal.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import React from "react";
|
||||
import Link from "next/link";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
onDiscardHref: string;
|
||||
};
|
||||
|
||||
export const ConfirmDiscardModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, handleClose, onDiscardHref } = props;
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[30rem]">
|
||||
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-custom-text-300"
|
||||
>
|
||||
You have unsaved changes
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-400">
|
||||
Changes you made will be lost if you go back. Do you
|
||||
wish to go back?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end items-center p-4 sm:px-6 gap-2">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Keep editing
|
||||
</Button>
|
||||
<Link
|
||||
href={onDiscardHref}
|
||||
className={getButtonStyling("primary", "sm")}
|
||||
>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
82
admin/components/common/controller-input.tsx
Normal file
82
admin/components/common/controller-input.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Controller, Control } from "react-hook-form";
|
||||
// ui
|
||||
import { Input } from "@plane/ui";
|
||||
// icons
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
control: Control<any>;
|
||||
type: "text" | "password";
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string | JSX.Element;
|
||||
placeholder: string;
|
||||
error: boolean;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
export type TControllerInputFormField = {
|
||||
key: string;
|
||||
type: "text" | "password";
|
||||
label: string;
|
||||
description?: string | JSX.Element;
|
||||
placeholder: string;
|
||||
error: boolean;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
export const ControllerInput: React.FC<Props> = (props) => {
|
||||
const { name, control, type, label, description, placeholder, error, required } = props;
|
||||
// states
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">{label}</h4>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
rules={{ required: required ? `${label} is required.` : false }}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id={name}
|
||||
name={name}
|
||||
type={type === "password" && showPassword ? "text" : type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={error}
|
||||
placeholder={placeholder}
|
||||
className={cn("w-full rounded-md font-medium", {
|
||||
"pr-10": type === "password",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{type === "password" &&
|
||||
(showPassword ? (
|
||||
<button
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => setShowPassword(false)}
|
||||
>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => setShowPassword(true)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{description && <p className="text-xs text-custom-text-300">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
46
admin/components/common/copy-field.tsx
Normal file
46
admin/components/common/copy-field.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// icons
|
||||
import { Copy } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
url: string;
|
||||
description: string | JSX.Element;
|
||||
};
|
||||
|
||||
export type TCopyField = {
|
||||
key: string;
|
||||
label: string;
|
||||
url: string;
|
||||
description: string | JSX.Element;
|
||||
};
|
||||
|
||||
export const CopyField: React.FC<Props> = (props) => {
|
||||
const { label, url, description } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">{label}</h4>
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(url);
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Copied to clipboard",
|
||||
message: `The ${label} has been successfully copied to your clipboard`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-medium">{url}</p>
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
</Button>
|
||||
<p className="text-xs text-custom-text-400">{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
6
admin/components/common/index.ts
Normal file
6
admin/components/common/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export * from "./breadcrumb-link";
|
||||
export * from "./confirm-discard-modal";
|
||||
export * from "./controller-input";
|
||||
export * from "./copy-field";
|
||||
export * from "./password-strength-meter";
|
||||
export * from "./banner";
|
||||
69
admin/components/common/password-strength-meter.tsx
Normal file
69
admin/components/common/password-strength-meter.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"use client";
|
||||
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getPasswordStrength } from "@/helpers/password.helper";
|
||||
// icons
|
||||
import { CircleCheck } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export const PasswordStrengthMeter: React.FC<Props> = (props: Props) => {
|
||||
const { password } = props;
|
||||
|
||||
const strength = getPasswordStrength(password);
|
||||
let bars = [];
|
||||
let text = "";
|
||||
let textColor = "";
|
||||
|
||||
if (password.length === 0) {
|
||||
bars = [`bg-[#F0F0F3]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`];
|
||||
text = "Password requirements";
|
||||
} else if (password.length < 8) {
|
||||
bars = [`bg-[#DC3E42]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`];
|
||||
text = "Password is too short";
|
||||
textColor = `text-[#DC3E42]`;
|
||||
} else if (strength < 3) {
|
||||
bars = [`bg-[#FFBA18]`, `bg-[#FFBA18]`, `bg-[#F0F0F3]`];
|
||||
text = "Password is weak";
|
||||
textColor = `text-[#FFBA18]`;
|
||||
} else {
|
||||
bars = [`bg-[#3E9B4F]`, `bg-[#3E9B4F]`, `bg-[#3E9B4F]`];
|
||||
text = "Password is strong";
|
||||
textColor = `text-[#3E9B4F]`;
|
||||
}
|
||||
|
||||
const criteria = [
|
||||
{ label: "Min 8 characters", isValid: password.length >= 8 },
|
||||
{ label: "Min 1 upper-case letter", isValid: /[A-Z]/.test(password) },
|
||||
{ label: "Min 1 number", isValid: /\d/.test(password) },
|
||||
{ label: "Min 1 special character", isValid: /[!@#$%^&*]/.test(password) },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full p-1">
|
||||
<div className="flex w-full gap-1.5">
|
||||
{bars.map((color, index) => (
|
||||
<div key={index} className={cn("w-full h-1 rounded-full", color)} />
|
||||
))}
|
||||
</div>
|
||||
<p className={cn("text-xs font-medium py-1", textColor)}>{text}</p>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
||||
{criteria.map((criterion, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex items-center gap-1 text-xs font-medium",
|
||||
criterion.isValid ? `text-[#3E9B4F]` : "text-custom-text-400"
|
||||
)}
|
||||
>
|
||||
<CircleCheck width={14} height={14} />
|
||||
{criterion.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
admin/components/core/index.ts
Normal file
1
admin/components/core/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./page-header";
|
||||
17
admin/components/core/page-header.tsx
Normal file
17
admin/components/core/page-header.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
"use client";
|
||||
|
||||
type TPageHeader = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export const PageHeader: React.FC<TPageHeader> = (props) => {
|
||||
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
admin/components/instance/index.ts
Normal file
1
admin/components/instance/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./instance-not-ready";
|
||||
30
admin/components/instance/instance-not-ready.tsx
Normal file
30
admin/components/instance/instance-not-ready.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import PlaneTakeOffImage from "@/public/images/plane-takeoff.png";
|
||||
|
||||
export const InstanceNotReady: FC = () => (
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="font-medium text-base text-onboarding-text-400">
|
||||
Get started by setting up your instance and workspace
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link href={"/setup/?auth_enabled=0"}>
|
||||
<Button size="lg" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
54
admin/components/new-user-popup.tsx
Normal file
54
admin/components/new-user-popup.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
import { useTheme as nextUseTheme } from "next-themes";
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "helpers/common.helper";
|
||||
// icons
|
||||
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
|
||||
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
|
||||
import { useTheme } from "@/hooks";
|
||||
|
||||
export const NewUserPopup: React.FC = observer(() => {
|
||||
// hooks
|
||||
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
|
||||
// theme
|
||||
const { resolvedTheme } = nextUseTheme();
|
||||
|
||||
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`;
|
||||
|
||||
if (!isNewUserPopup) return <></>;
|
||||
return (
|
||||
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-base font-semibold">Create workspace</div>
|
||||
<div className="py-2 text-sm font-medium text-custom-text-300">
|
||||
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
|
||||
workspace, you will need to login again.
|
||||
</div>
|
||||
<div className="flex items-center gap-4 pt-2">
|
||||
<a href={redirectionLink} className={getButtonStyling("primary", "sm")}>
|
||||
Create workspace
|
||||
</a>
|
||||
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 flex items-center justify-center">
|
||||
<Image
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
|
||||
height={80}
|
||||
width={80}
|
||||
alt="Plane icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue