mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(ui): added new layout
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
431
frontend/src/layouts/AppLayout/AppLayout.tsx
Normal file
431
frontend/src/layouts/AppLayout/AppLayout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
305
frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx
Normal file
305
frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Navbar } from './NavBar';
|
||||
1
frontend/src/layouts/AppLayout/index.tsx
Normal file
1
frontend/src/layouts/AppLayout/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { AppLayout } from './AppLayout';
|
||||
1
frontend/src/layouts/index.tsx
Normal file
1
frontend/src/layouts/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { AppLayout } from './AppLayout';
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user