feat(ui): added new layout

This commit is contained in:
akhilmhdh
2023-02-08 23:07:03 +05:30
parent 8dc47110a0
commit a486390015
9 changed files with 772 additions and 37 deletions

View File

@@ -1,10 +1,7 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useWorkspace } from '@app/context';
import getOrganization from '@app/pages/api/organization/GetOrg';
import { useOrgnization, useWorkspace } from '@app/context';
/**
* This is the component at the top of almost every page.
@@ -22,28 +19,15 @@ export default function NavHeader({
pageName: string;
isProjectRelated?: boolean;
}): JSX.Element {
const [orgName, setOrgName] = useState('');
const router = useRouter();
const projectId = String(router.query.id);
const { currentWorkspace } = useWorkspace();
useEffect(() => {
(async () => {
const orgId = localStorage.getItem('orgData.id');
const org = await getOrganization({
orgId: orgId || ''
});
setOrgName(org.name);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
const { currentOrg } = useOrgnization();
return (
<div className="ml-6 flex flex-row items-center pt-8">
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
{orgName?.charAt(0)}
{currentOrg?.name?.charAt(0)}
</div>
<div className="text-sm font-semibold text-primary">{orgName}</div>
<div className="text-sm font-semibold text-primary">{currentOrg?.name}</div>
{isProjectRelated && (
<>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />

View File

@@ -31,18 +31,19 @@ export const MenuItem = <T extends ElementType = 'button'>({
as: Item = 'button',
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef
inputRef,
...props
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => (
<li
className={twMerge(
'px-2 py-3 font-inter flex flex-col text-sm text-white transition-all rounded cursor-pointer hover:bg-gray-700',
isSelected && 'text-primary',
'px-3 py-3 font-inter flex flex-col text-sm text-white transition-all rounded cursor-pointer hover:bg-gray-700',
isSelected && 'text-primary bg-gray-700',
isDisabled && 'text-gray-500 hover:bg-transparent cursor-not-allowed',
className
)}
>
<Item type="button" role="menuitem" class="flex items-center" ref={inputRef}>
{icon && <span className="mr-2">{icon}</span>}
<Item type="button" role="menuitem" className="flex items-center" ref={inputRef} {...props}>
{icon && <span className="mr-3">{icon}</span>}
<span className="flex-grow text-left">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}

View File

@@ -12,13 +12,14 @@ type Props = {
className?: string;
dropdownContainerClassName?: string;
isLoading?: boolean;
position?: 'item-aligned' | 'popper';
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
(
{ children, placeholder, className, isLoading, dropdownContainerClassName, ...props },
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
ref
): JSX.Element => {
return (
@@ -44,6 +45,8 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
dropdownContainerClassName
)}
position={position}
style={{ width: 'var(--radix-select-trigger-width)' }}
>
<SelectPrimitive.ScrollUpButton>
<FontAwesomeIcon icon={faChevronUp} size="sm" />

View File

@@ -0,0 +1,431 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable no-unexpected-multiline */
/* eslint-disable react-hooks/exhaustive-deps */
import crypto from 'crypto';
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import {
faBookOpen,
faFileLines,
faGear,
faKey,
faMobile,
faPlug,
faPlus,
faUser
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import onboardingCheck from '@app/components/utilities/checks/OnboardingCheck';
import { tempLocalStorage } from '@app/components/utilities/checks/tempLocalStorage';
import { encryptAssymmetric } from '@app/components/utilities/cryptography/crypto';
import {
Button,
Checkbox,
FormControl,
Input,
Menu,
MenuItem,
Modal,
ModalContent,
Select,
SelectItem
} from '@app/components/v2';
import { useOrgnization, useUser, useWorkspace } from '@app/context';
import { usePopUp } from '@app/hooks';
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from '@app/hooks/api';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import { Navbar } from './components/NavBar';
interface LayoutProps {
children: React.ReactNode;
}
const formSchema = yup.object({
name: yup.string().required().label('Project Name').trim(),
addMembers: yup.bool().required().label('Add Members')
});
type TAddProjectFormData = yup.InferType<typeof formSchema>;
export const AppLayout = ({ children }: LayoutProps) => {
const router = useRouter();
const { createNotification } = useNotificationContext();
const { workspaces, currentWorkspace } = useWorkspace();
const { currentOrg } = useOrgnization();
const { user } = useUser();
const createWs = useCreateWorkspace();
const uploadWsKey = useUploadWsKey();
const addWsUser = useAddUserToWs();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
'addNewWs'
] as const);
const {
control,
formState: { isSubmitting },
reset,
handleSubmit
} = useForm<TAddProjectFormData>({
resolver: yupResolver(formSchema)
});
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
const [workspaceSelected, setWorkspaceSelected] = useState('∞');
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
const { t } = useTranslation();
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
// Placing the localstorage as much as possible
// Wait till tony integrates the azure and its launched
useEffect(() => {
// Put a user in a workspace if they're not in one yet
const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') {
const userOrgs = await getOrganizations();
localStorage.setItem('orgData.id', userOrgs[0]._id);
}
const orgUserProjects = await getOrganizationUserProjects({
orgId: tempLocalStorage('orgData.id')
});
const userWorkspaces = orgUserProjects;
if (
(userWorkspaces.length === 0 &&
router.asPath !== '/noprojects' &&
!router.asPath.includes('home') &&
!router.asPath.includes('settings')) ||
router.asPath === '/dashboard/undefined'
) {
router.push('/noprojects');
} else if (router.asPath !== '/noprojects') {
const intendedWorkspaceId = router.asPath
.split('/')
[router.asPath.split('/').length - 1].split('?')[0];
if (!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId)) {
localStorage.setItem('projectData.id', intendedWorkspaceId);
}
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
) {
router.push(`/dashboard/${userWorkspaces[0]._id}`);
} else {
setWorkspaceMapping(
Object.fromEntries(
userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
) as any
);
setWorkspaceSelected(
Object.fromEntries(
userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
)[router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]]
);
}
}
};
putUserInWorkSpace();
onboardingCheck({ setTotalOnboardingActionsDone });
}, [router.query.id]);
useEffect(() => {
try {
if (
workspaceMapping[workspaceSelected as any] &&
`${workspaceMapping[workspaceSelected as any]}` !==
router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]
) {
localStorage.setItem('projectData.id', `${workspaceMapping[workspaceSelected as any]}`);
router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
}
} catch (err) {
console.log(err);
}
}, [workspaceSelected]);
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
// type check
if (!currentOrg?._id) return;
try {
const {
data: {
workspace: { _id: newWorkspaceId }
}
} = await createWs.mutateAsync({
organizationId: currentOrg?._id,
workspaceName: name
});
const randomBytes = crypto.randomBytes(16).toString('hex');
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: user.publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKey.mutateAsync({
encryptedKey: ciphertext,
nonce,
userId: user?._id,
workspaceId: newWorkspaceId
});
if (addMembers) {
console.log('adding other users');
// not using hooks because need at this point only
const orgUsers = await fetchOrgUsers(currentOrg._id);
orgUsers.forEach(({ status, user: orgUser }) => {
// skip if status of org user is not accepted
// this orgUser is the person who created the ws
if (status !== 'accepted' || user.email === orgUser.email) return;
addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId });
});
}
createNotification({ text: 'Workspace created', type: 'success' });
handlePopUpClose('addNewWs');
router.push(`/dashboard/${newWorkspaceId}`);
} catch (err) {
console.error(err);
createNotification({ text: 'Failed to create workspace', type: 'error' });
}
};
return (
<>
<div className="flex h-screen w-full flex-col overflow-x-hidden">
<Navbar />
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="w-full border-r border-mineshaft-500 bg-mineshaft-800 md:w-60">
<nav className="items-between flex h-full flex-col justify-between">
<div>
<div className="w-full p-4">
<p className="mb-2 text-sm font-medium uppercase text-gray-400">Projects</p>
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full"
onValueChange={(value) => {
router.push(`/dashboard/${value}`);
}}
position="popper"
dropdownContainerClassName="left-0"
>
{workspaces.map(({ _id, name }) => (
<SelectItem key={`ws-layout-list-${_id}`} value={_id}>
{name}
</SelectItem>
))}
<hr className="my-4 h-px border-0 bg-gray-700" />
<div className="w-full px-2 pb-2">
<Button
className="w-full py-2"
color="mineshaft"
size="xs"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
</Select>
</div>
<div>
<Menu>
<Link href={`/dashboard/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/dashboard/${currentWorkspace?._id}`}
icon={<FontAwesomeIcon icon={faKey} size="lg" />}
>
{t('nav:menu.secrets')}
</MenuItem>
</a>
</Link>
<Link href={`/users/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/users/${currentWorkspace?._id}`}
icon={<FontAwesomeIcon icon={faUser} size="lg" />}
>
{t('nav:menu.members')}
</MenuItem>
</a>
</Link>
<Link href={`/integrations/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`}
icon={<FontAwesomeIcon icon={faPlug} size="lg" />}
>
{t('nav:menu.integrations')}
</MenuItem>
</a>
</Link>
<Link href={`/activity/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/activity/${currentWorkspace?._id}`}
icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
>
Activity Logs
</MenuItem>
</a>
</Link>
<Link href={`/settings/project/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/settings/project/${currentWorkspace?._id}`
}
icon={<FontAwesomeIcon icon={faGear} size="lg" />}
>
{t('nav:menu.project-settings')}
</MenuItem>
</a>
</Link>
</Menu>
</div>
</div>
<div className="mt-40 mb-4 w-full px-2">
{router.asPath.split('/')[1] === 'home' ? (
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} />
</p>
Infisical Guide
<img
src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
totalOnboardingActionsDone === 1 ? '14' : ''
}${totalOnboardingActionsDone === 2 ? '28' : ''}${
totalOnboardingActionsDone === 3 ? '43' : ''
}${totalOnboardingActionsDone === 4 ? '57' : ''}${
totalOnboardingActionsDone === 5 ? '71' : ''
}.svg`}
height={58}
width={58}
alt="progress bar"
className="absolute right-2 -top-2"
/>
</div>
) : (
<Link href={`/home/${currentWorkspace?._id}`}>
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
<p className="flex w-10 items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} />
</p>
Infisical Guide
<img
src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
totalOnboardingActionsDone === 1 ? '14' : ''
}${totalOnboardingActionsDone === 2 ? '28' : ''}${
totalOnboardingActionsDone === 3 ? '43' : ''
}${totalOnboardingActionsDone === 4 ? '57' : ''}${
totalOnboardingActionsDone === 5 ? '71' : ''
}.svg`}
height={58}
width={58}
alt="progress bar"
className="absolute right-2 -top-2"
/>
</div>
</Link>
)}
</div>
</nav>
</aside>
<Modal
isOpen={popUp.addNewWs.isOpen}
onOpenChange={(isModalOpen) => {
handlePopUpToggle('addNewWs', isModalOpen);
reset();
}}
>
<ModalContent
title="Create a new project"
subTitle="This project will contain your environment variables"
>
<form onSubmit={handleSubmit(onCreateProject)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Type your project name" />
</FormControl>
)}
/>
<div>
<Controller
control={control}
name="addMembers"
defaultValue
render={({ field: { onBlur, value, onChange } }) => (
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
>
Add all members of my organization to this project
</Checkbox>
)}
/>
</div>
<div className="mt-8 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className=""
type="submit"
>
Create Project
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose('addNewWs')}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800">{children}</main>
</div>
</div>
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t('common:no-mobile')} `}
</p>
</div>
</>
);
};

View File

@@ -0,0 +1,305 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable react/jsx-key */
import { Fragment, useMemo } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { TFunction, useTranslation } from 'next-i18next';
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
import {
faAngleDown,
faBook,
faCoins,
faEnvelope,
faGear,
faPlus,
faRightFromBracket
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
import guidGenerator from '@app/components/utilities/randomId';
import { useOrgnization, useUser } from '@app/context';
import { useLogoutUser } from '@app/hooks/api';
const supportOptions = (t: TFunction) => [
[
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faSlack} />,
t('nav:support.slack'),
'https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA'
],
[
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faBook} />,
t('nav:support.docs'),
'https://infisical.com/docs/getting-started/introduction'
],
[
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faGithub} />,
t('nav:support.issue'),
'https://github.com/Infisical/infisical-cli/issues'
],
[
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faEnvelope} />,
t('nav:support.email'),
'mailto:support@infisical.com'
]
];
export interface ICurrentOrg {
name: string;
}
export interface IUser {
firstName: string;
lastName: string;
email: string;
}
/**
* This is the navigation bar in the main app.
* It has two main components: support options and user menu (inlcudes billing, logout, org/user settings)
* @returns NavBar
*/
export const Navbar = () => {
const router = useRouter();
const { currentOrg, orgs } = useOrgnization();
const { user } = useUser();
const logout = useLogoutUser();
const { t } = useTranslation();
// remove this memo
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
const closeApp = async () => {
try {
console.log('Logging out...');
await logout.mutateAsync();
router.push('/login');
} catch (error) {
console.error(error);
}
};
return (
<div className="z-50 flex w-full flex-row justify-between border-b border-mineshaft-500 bg-mineshaft-900 text-white">
<div className="m-auto mx-4 flex items-center justify-start">
<div className="flex flex-row items-center">
<div className="flex justify-center py-4">
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
</div>
<a href="#" className="mx-2 text-2xl font-semibold text-white">
Infisical
</a>
</div>
</div>
<div className="relative z-40 mx-2 flex items-center justify-start">
<a
href="https://infisical.com/docs/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mr-4 flex items-center rounded-md px-3 py-2 text-sm text-gray-200 duration-200 hover:bg-white/10"
>
<FontAwesomeIcon icon={faBook} className="mr-2 text-xl" />
Docs
</a>
<Menu as="div" className="relative inline-block text-left">
<div className="mr-4">
<Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
</Menu.Button>
</div>
<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"
>
<Menu.Items className="absolute right-0 z-20 mt-0.5 w-64 origin-top-right rounded-md border border-mineshaft-700 bg-bunker px-2 py-1.5 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{supportOptionsList.map(([icon, text, url]) => (
<a
key={guidGenerator()}
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md py-0.5 font-normal text-gray-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/10 hover:text-gray-200">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
))}
</Menu.Items>
</Transition>
</Menu>
<Menu as="div" className="relative mr-4 inline-block text-left">
<div>
<Menu.Button className="inline-flex w-full justify-center rounded-md py-2 pr-2 pl-2 text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
{user?.firstName} {user?.lastName}
<FontAwesomeIcon
icon={faAngleDown}
className="ml-2 mt-1 text-sm text-gray-300 hover:text-lime-100"
/>
</Menu.Button>
</div>
<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"
>
<Menu.Items className="absolute right-0 z-20 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md border border-mineshaft-700 bg-bunker shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="px-1 py-1 ">
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
{t('nav:user.signed-in-as')}
</div>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/personal/${router.query.id}`)}
className="mx-1 my-1 flex cursor-pointer flex-row items-center rounded-md px-1 hover:bg-white/5"
>
<div className="flex h-8 w-9 items-center justify-center rounded-full bg-white/10 text-gray-300">
{user?.firstName?.charAt(0)}
</div>
<div className="flex w-full items-center justify-between">
<div>
<p className="px-2 pt-1 text-sm text-gray-300">
{' '}
{user?.firstName} {user?.lastName}
</p>
<p className="px-2 pb-1 text-xs text-gray-400"> {user?.email}</p>
</div>
<FontAwesomeIcon
icon={faGear}
className="mr-1 cursor-pointer rounded-md p-2 text-lg text-gray-400 hover:bg-white/10"
/>
</div>
</div>
</div>
<div className="px-2 pt-2">
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
{t('nav:user.current-organization')}
</div>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/org/${router.query.id}`)}
className="mt-2 flex cursor-pointer flex-row items-center rounded-md px-2 py-1 hover:bg-white/5"
>
<div className="flex h-7 w-8 items-center justify-center rounded-md bg-white/10 text-gray-300">
{currentOrg?.name?.charAt(0)}
</div>
<div className="flex w-full items-center justify-between">
<p className="px-2 text-sm text-gray-300">{currentOrg?.name}</p>
<FontAwesomeIcon
icon={faGear}
className="cursor-pointer rounded-md p-2 text-lg text-gray-400 hover:bg-white/10"
/>
</div>
</div>
<button
// onClick={buttonAction}
type="button"
className="w-full cursor-pointer"
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/5 hover:text-gray-200"
>
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faCoins} />
<div className="text-sm">{t('nav:user.usage-billing')}</div>
</div>
</button>
<button
type="button"
// onClick={buttonAction}
className="mb-2 w-full cursor-pointer"
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 pl-10 pr-4 text-gray-400 duration-200 hover:bg-primary/100 hover:font-semibold hover:text-black"
>
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 pr-4">
<FontAwesomeIcon icon={faPlus} className="ml-1" />
</span>
<div className="ml-1 text-sm">{t('nav:user.invite')}</div>
</div>
</button>
</div>
{Boolean(orgs) && (
<div className="px-1 pt-1">
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
{t('nav:user.other-organizations')}
</div>
<div className="mt-3 mb-2 flex flex-col items-start px-1">
{orgs
?.filter((org: { _id: string }) => org._id !== currentOrg?._id)
.map((org: { _id: string; name: string }) => (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
key={guidGenerator()}
onClick={() => {
localStorage.setItem('orgData.id', org._id);
router.reload();
}}
className="flex w-full cursor-pointer flex-row items-center justify-start rounded-md p-1.5 hover:bg-white/5"
>
<div className="flex h-7 w-8 items-center justify-center rounded-md bg-white/10 text-gray-300">
{org.name.charAt(0)}
</div>
<div className="flex w-full items-center justify-between">
<p className="px-2 text-sm text-gray-300">{org.name}</p>
</div>
</div>
))}
</div>
</div>
)}
<div className="px-1 py-1">
<Menu.Item>
{({ active }) => (
<button
type="button"
onClick={closeApp}
className={`${
active ? 'bg-red font-semibold text-white' : 'text-gray-400'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<div className="relative flex cursor-pointer select-none items-center justify-start">
<FontAwesomeIcon
className="ml-1.5 mr-3 text-lg"
icon={faRightFromBracket}
/>
{t('common:logout')}
</div>
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { Navbar } from './NavBar';

View File

@@ -0,0 +1 @@
export { AppLayout } from './AppLayout';

View File

@@ -0,0 +1 @@
export { AppLayout } from './AppLayout';

View File

@@ -6,13 +6,17 @@ import { appWithTranslation } from 'next-i18next';
import { config } from '@fortawesome/fontawesome-svg-core';
import { QueryClientProvider } from '@tanstack/react-query';
import Layout from '@app/components/basic/Layout';
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
import { publicPaths } from '@app/const';
import { SubscriptionProvider } from '@app/context';
import { AuthProvider } from '@app/context/AuthContext';
import { WorkspaceProvider } from '@app/context/WorkspaceContext';
import {
AuthProvider,
OrgProvider,
SubscriptionProvider,
UserProvider,
WorkspaceProvider
} from '@app/context';
import { AppLayout } from '@app/layouts';
import { queryClient } from '@app/reactQuery';
import '@fortawesome/fontawesome-svg-core/styles.css';
@@ -71,13 +75,17 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<WorkspaceProvider>
<SubscriptionProvider>
<NotificationProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</NotificationProvider>
</SubscriptionProvider>
<OrgProvider>
<SubscriptionProvider>
<UserProvider>
<NotificationProvider>
<AppLayout>
<Component {...pageProps} />
</AppLayout>
</NotificationProvider>
</UserProvider>
</SubscriptionProvider>
</OrgProvider>
</WorkspaceProvider>
</AuthProvider>
</QueryClientProvider>