Merge pull request #315 from akhilmhdh/feat/new-layout

Feat new layout synced with api changes
This commit is contained in:
mv-turtle
2023-02-10 22:20:11 -08:00
committed by GitHub
38 changed files with 1076 additions and 56 deletions

View File

@ -8,7 +8,7 @@
</head>
<body>
<h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>

View File

@ -7,7 +7,7 @@
</head>
<body>
<h2>Join your team on Infisical</h2>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
<a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>

View File

@ -1,6 +1,6 @@
{
"support": {
"slack": "[NEW] Join Slack Forum",
"slack": "Join Slack Forum",
"docs": "Read Docs",
"issue": "Open a Github Issue",
"email": "Send us an Email"

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 { useOrganization, 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 } = useOrganization();
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

@ -24,7 +24,7 @@ const buttonVariants = cva(
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-80 hover:bg-opacity-100'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
},

View File

@ -8,9 +8,9 @@ export type CardTitleProps = {
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
<div className={twMerge('px-6 py-4 font-sans text-xl font-medium', className)}>
<div className={twMerge('px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600', className)}>
{children}
{subTitle && <p className="py-1 text-sm font-normal text-gray-400">{subTitle}</p>}
{subTitle && <p className="pt-0.5 text-sm font-normal text-gray-400">{subTitle}</p>}
</div>
);

View File

@ -28,8 +28,9 @@ export const Checkbox = ({
<div className="flex items-center font-inter text-bunker-300">
<CheckboxPrimitive.Root
className={twMerge(
'flex items-center justify-center w-5 h-5 mr-3 transition-all rounded shadow hover:bg-bunker-200 bg-bunker-300',
'flex items-center justify-center w-4 h-4 mr-3 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600',
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
isChecked && 'bg-primary hover:bg-primary',
className
)}
required={isRequired}

View File

@ -24,7 +24,7 @@ export const HoverObject = ({
</a>
</HoverCard.Trigger>
<HoverCard.Portal>
<HoverCard.Content className="HoverCardContent z-[50]" sideOffset={5}>
<HoverCard.Content className="HoverCardContent z-[300]" sideOffset={5}>
<div className='bg-bunker-700 border border-mineshaft-600 p-2 rounded-md drop-shadow-xl text-bunker-300'>
<div style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>
<div>

View File

@ -31,18 +31,20 @@ 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',
isDisabled && 'text-gray-500 hover:bg-transparent cursor-not-allowed',
'px-1 py-2.5 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-600 duration-50',
isSelected && 'bg-mineshaft-500',
isDisabled && '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 relative" ref={inputRef} {...props}>
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.25rem] rounded-md h-8 bg-primary`}/>
{icon && <span className="mr-3 ml-4 w-5">{icon}</span>}
<span className="flex-grow text-left">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}

View File

@ -25,7 +25,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
<Card
isRounded
className={twMerge(
'fixed top-1/2 left-1/2 max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
'fixed top-1/2 left-1/2 max-w-lg border border-mineshaft-600 -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
className
)}
>
@ -36,7 +36,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
<IconButton
variant="plain"
ariaLabel="close"
className="absolute top-3 right-3 rounded text-white hover:bg-gray-600"
className="absolute top-4 right-6 rounded text-bunker-400 hover:text-bunker-50"
>
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
</IconButton>

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,11 +45,13 @@ 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" />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1.5">
<SelectPrimitive.Viewport className="p-1">
{isLoading ? (
<div className="flex items-center justify-center">
<Spinner size="xs" />
@ -82,7 +85,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
{...props}
className={twMerge(
`relative flex cursor-pointer
select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
outline-none transition-all hover:bg-mineshaft-500`,
isSelected && 'bg-primary',
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-gray-600',
@ -90,8 +93,8 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
)}
ref={forwardedRef}
>
<SelectPrimitive.ItemIndicator className="absolute left-3.5">
<FontAwesomeIcon icon={faCheck} size="sm" />
<SelectPrimitive.ItemIndicator className="absolute left-3.5 text-primary">
<FontAwesomeIcon icon={faCheck} />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>

View File

@ -0,0 +1,46 @@
import { createContext, ReactNode, useContext, useMemo } from 'react';
import { useGetOrganization } from '@app/hooks/api';
import { Organization } from '@app/hooks/api/types';
import { useWorkspace } from '../WorkspaceContext';
type TOrgContext = {
orgs?: Organization[];
currentOrg?: Organization;
isLoading: boolean;
};
const OrgContext = createContext<TOrgContext | null>(null);
type Props = {
children: ReactNode;
};
export const OrgProvider = ({ children }: Props): JSX.Element => {
const { currentWorkspace } = useWorkspace();
const { data: userOrgs, isLoading } = useGetOrganization();
const currentWsOrgID = currentWorkspace?.organization;
// memorize the workspace details for the context
const value = useMemo<TOrgContext>(
() => ({
orgs: userOrgs,
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID),
isLoading
}),
[currentWsOrgID, userOrgs, isLoading]
);
return <OrgContext.Provider value={value}>{children}</OrgContext.Provider>;
};
export const useOrganization = () => {
const ctx = useContext(OrgContext);
if (!ctx) {
throw new Error('useOrganization to be used within <OrgContext.Provider>');
}
return ctx;
};

View File

@ -0,0 +1 @@
export { OrgProvider, useOrganization } from './OrganizationContext';

View File

@ -0,0 +1,38 @@
import { createContext, ReactNode, useContext, useMemo } from 'react';
import { useGetUser } from '@app/hooks/api';
import { User } from '@app/hooks/api/types';
type TUserContext = {
user: User;
isLoading: boolean;
};
const UserContext = createContext<TUserContext | null>(null);
type Props = {
children: ReactNode;
};
export const UserProvider = ({ children }: Props): JSX.Element => {
const { data, isLoading } = useGetUser();
// memorize the workspace details for the context
const value = useMemo<TUserContext>(() => {
return {
user: data!,
isLoading
};
}, [data, isLoading]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
export const useUser = () => {
const ctx = useContext(UserContext);
if (!ctx) {
throw new Error('useUser has to be used within <UserContext.Provider>');
}
return ctx;
};

View File

@ -0,0 +1 @@
export { UserProvider, useUser } from './UserContext';

View File

@ -23,9 +23,10 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
// memorize the workspace details for the context
const value = useMemo<TWorkspaceContext>(() => {
const wsId = workspaceId || localStorage.getItem('projectData.id');
return {
workspaces: ws || [],
currentWorkspace: (ws || []).find(({ _id: id }) => id === workspaceId),
currentWorkspace: (ws || []).find(({ _id: id }) => id === wsId),
isLoading
};
}, [ws, workspaceId, isLoading]);

View File

@ -1,2 +1,5 @@
export { AuthProvider } from './AuthContext';
export { OrgProvider, useOrganization } from './OrganizationContext';
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
export { UserProvider, useUser } from './UserContext';
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';

View File

@ -1,6 +1,8 @@
export * from './auth';
export * from './keys';
export * from './organization';
export * from './serviceTokens';
export * from './subscriptions';
export * from './tags';
export * from './users';
export * from './workspace';

View File

@ -1 +1 @@
export { useGetUserWsKey } from './queries';
export { useGetUserWsKey, useUploadWsKey } from './queries';

View File

@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { UserWsKeyPair } from './types';
import { UploadWsKeyDTO, UserWsKeyPair } from './types';
const encKeyKeys = {
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
@ -22,3 +22,10 @@ export const useGetUserWsKey = (workspaceID: string) =>
queryFn: () => fetchUserWsKey(workspaceID),
enabled: Boolean(workspaceID)
});
// mutations
export const useUploadWsKey = () =>
useMutation<{}, {}, UploadWsKeyDTO>({
mutationFn: ({ encryptedKey, nonce, userId, workspaceId }) =>
apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } })
});

View File

@ -20,3 +20,10 @@ export type Sender = {
lastName: string;
publicKey: string;
};
export type UploadWsKeyDTO = {
userId: string;
encryptedKey: string;
nonce: string;
workspaceId: string;
};

View File

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

View File

@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { Organization } from './types';
const organizationKeys = {
getUserOrganization: ['organization'] as const
};
const fetchUserOrganization = async () => {
const { data } = await apiRequest.get<{ organizations: Organization[] }>('/api/v1/organization');
return data.organizations;
};
export const useGetOrganization = () =>
useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });

View File

@ -0,0 +1,6 @@
export type Organization = {
_id: string;
name: string;
createAt: string;
updatedAt: string;
};

View File

@ -1,7 +1,9 @@
export type { GetAuthTokenAPI } from './auth/types';
export type { UserWsKeyPair } from './keys/types';
export type { Organization } from './organization/types';
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
export type { User } from './users/types';
export type {
CreateEnvironmentDTO,
DeleteEnvironmentDTO,

View File

@ -0,0 +1,7 @@
export {
fetchOrgUsers,
useAddUserToWs,
useGetOrgUsers,
useGetUser,
useLogoutUser
} from './queries';

View File

@ -0,0 +1,84 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import {
decryptAssymmetric,
encryptAssymmetric
} from '@app/components/utilities/cryptography/crypto';
import { apiRequest } from '@app/config/request';
import { setAuthToken } from '@app/reactQuery';
import { useUploadWsKey } from '../keys/queries';
import { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './types';
const userKeys = {
getUser: ['user'] as const,
getOrgUsers: (orgId: string) => [{ orgId }, 'user']
};
const fetchUserDetails = async () => {
const { data } = await apiRequest.get<{ user: User }>('/api/v1/user');
return data.user;
};
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
export const fetchOrgUsers = async (orgId: string) => {
const { data } = await apiRequest.get<{ users: OrgUser[] }>(
`/api/v1/organization/${orgId}/users`
);
return data.users;
};
export const useGetOrgUsers = (orgId: string) =>
useQuery(userKeys.getOrgUsers(orgId), () => fetchOrgUsers(orgId));
// mutation
export const useAddUserToWs = () => {
const uploadWsKey = useUploadWsKey();
return useMutation<{ data: AddUserToWsRes }, {}, AddUserToWsDTO>({
mutationFn: ({ email, workspaceId }) =>
apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email }),
onSuccess: ({ data }, { workspaceId }) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
if (!PRIVATE_KEY) return;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: data.latestKey.encryptedKey,
nonce: data.latestKey.nonce,
publicKey: data.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
plaintext: key,
publicKey: data.invitee.publicKey,
privateKey: PRIVATE_KEY
});
uploadWsKey.mutate({
encryptedKey: inviteeCipherText,
nonce: inviteeNonce,
userId: data.invitee._id,
workspaceId
});
}
});
};
export const useLogoutUser = () =>
useMutation({
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
onSuccess: () => {
setAuthToken('');
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.setItem('publicKey', '');
localStorage.setItem('encryptedPrivateKey', '');
localStorage.setItem('iv', '');
localStorage.setItem('tag', '');
localStorage.setItem('PRIVATE_KEY', '');
}
});

View File

@ -0,0 +1,39 @@
import { UserWsKeyPair } from '../keys/types';
export type User = {
seenIps: string[];
_id: string;
email: string;
createdAt: Date;
updatedAt: Date;
__v: number;
firstName: string;
lastName: string;
publicKey: string;
};
export type OrgUser = {
_id: string;
user: {
email: string;
firstName: string;
lastName: string;
_id: string;
publicKey: string;
};
inviteEmail: string;
organization: string;
role: 'owner' | 'admin' | 'member';
status: 'invited' | 'accepted';
deniedPermissions: any[];
};
export type AddUserToWsDTO = {
workspaceId: string;
email: string;
};
export type AddUserToWsRes = {
invitee: OrgUser['user'];
latestKey: UserWsKeyPair;
};

View File

@ -1,4 +1,5 @@
export {
useCreateWorkspace,
useCreateWsEnvironment,
useDeleteWorkspace,
useDeleteWsEnvironment,

View File

@ -4,6 +4,7 @@ import { apiRequest } from '@app/config/request';
import {
CreateEnvironmentDTO,
CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
@ -40,6 +41,18 @@ export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
// mutation
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ organizationId, workspaceName }) =>
apiRequest.post('/api/v1/workspace', { workspaceName, organizationId }),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
export const useRenameWorkspace = () => {
const queryClient = useQueryClient();

View File

@ -11,6 +11,11 @@ export type WorkspaceEnv = { name: string; slug: string };
export type WorkspaceTag = { _id: string; name: string; slug: string };
// mutation dto
export type CreateWorkspaceDTO = {
workspaceName: string;
organizationId: string;
};
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };

View File

@ -0,0 +1,427 @@
/* 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 { useOrganization, 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();
// eslint-disable-next-line prefer-const
let { workspaces, currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
workspaces = workspaces.filter(ws => ws.organization === currentOrg?._id)
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 (!['callback', 'create', 'authorize'].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 (
!['callback', 'create', 'authorize'].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="hidden h-screen w-full flex-col overflow-x-hidden md:flex dark">
<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-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between">
<div>
<div className="w-full p-4 mt-3 mb-4">
<p className="text-xs font-semibold ml-1.5 mb-1 uppercase text-gray-400">Project</p>
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full py-2.5 bg-mineshaft-600 font-medium"
onValueChange={(value) => {
router.push(`/dashboard/${value}`);
}}
position="popper"
dropdownContainerClassName="left-0 text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
>
{workspaces.map(({ _id, name }) => (
<SelectItem key={`ws-layout-list-${_id}`} value={_id} className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}>
{name}
</SelectItem>
))}
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
<div className="w-full">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
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 secrets and configurations."
>
<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 className='pl-1 mt-4'>
<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-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className=""
type="submit"
>
Create Project
</Button>
</div>
</form>
</ModalContent>
</Modal>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
{children}
</main>
</div>
</div>
<div className="z-[200] 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 { useOrganization, 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 } = useOrganization();
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-[70] 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-[125] mt-0.5 w-68 origin-top-right divide-y divide-mineshaft-700 drop-shadow-2xl rounded-md border border-mineshaft-700 bg-mineshaft-900 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>
{orgs && orgs?.length > 1 && (
<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>

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faSlack } from '@fortawesome/free-brands-svg-icons';
@ -142,6 +143,10 @@ export default function Home() {
return (
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll pt-4">
<Head>
<title>Infisical Guide</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="flex flex-col items-center text-gray-300 text-lg mx-auto max-w-2xl lg:max-w-3xl xl:max-w-4xl py-6">
<div className="text-3xl font-bold text-left w-full">Your quick start guide</div>
<div className="text-md text-left w-full pt-2 pb-4 text-bunker-300">