mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #315 from akhilmhdh/feat/new-layout
Feat new layout synced with api changes
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Join your organization on Infisical</h2>
|
<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>
|
<a href="{{callback_url}}?token={{token}}&to={{email}}">Join now</a>
|
||||||
<h3>What is Infisical?</h3>
|
<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>
|
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Join your team on Infisical</h2>
|
<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>
|
<a href="{{callback_url}}">Join now</a>
|
||||||
<h3>What is Infisical?</h3>
|
<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>
|
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"support": {
|
"support": {
|
||||||
"slack": "[NEW] Join Slack Forum",
|
"slack": "Join Slack Forum",
|
||||||
"docs": "Read Docs",
|
"docs": "Read Docs",
|
||||||
"issue": "Open a Github Issue",
|
"issue": "Open a Github Issue",
|
||||||
"email": "Send us an Email"
|
"email": "Send us an Email"
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import { useWorkspace } from '@app/context';
|
import { useOrganization, useWorkspace } from '@app/context';
|
||||||
import getOrganization from '@app/pages/api/organization/GetOrg';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the component at the top of almost every page.
|
* This is the component at the top of almost every page.
|
||||||
@@ -22,28 +19,15 @@ export default function NavHeader({
|
|||||||
pageName: string;
|
pageName: string;
|
||||||
isProjectRelated?: boolean;
|
isProjectRelated?: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [orgName, setOrgName] = useState('');
|
|
||||||
const router = useRouter();
|
|
||||||
const projectId = String(router.query.id);
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ml-6 flex flex-row items-center pt-8">
|
<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">
|
<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>
|
||||||
<div className="text-sm font-semibold text-primary">{orgName}</div>
|
<div className="text-sm font-semibold text-primary">{currentOrg?.name}</div>
|
||||||
{isProjectRelated && (
|
{isProjectRelated && (
|
||||||
<>
|
<>
|
||||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const buttonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
colorSchema: {
|
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'],
|
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
|
||||||
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
|
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export type CardTitleProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CardTitle = ({ children, className, subTitle }: 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}
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ export const Checkbox = ({
|
|||||||
<div className="flex items-center font-inter text-bunker-300">
|
<div className="flex items-center font-inter text-bunker-300">
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
className={twMerge(
|
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',
|
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
|
||||||
|
isChecked && 'bg-primary hover:bg-primary',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const HoverObject = ({
|
|||||||
</a>
|
</a>
|
||||||
</HoverCard.Trigger>
|
</HoverCard.Trigger>
|
||||||
<HoverCard.Portal>
|
<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 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 style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -31,18 +31,20 @@ export const MenuItem = <T extends ElementType = 'button'>({
|
|||||||
as: Item = 'button',
|
as: Item = 'button',
|
||||||
description,
|
description,
|
||||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||||
inputRef
|
inputRef,
|
||||||
|
...props
|
||||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => (
|
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => (
|
||||||
<li
|
<li
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'px-2 py-3 font-inter flex flex-col text-sm text-white transition-all rounded cursor-pointer hover:bg-gray-700',
|
'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 && 'text-primary',
|
isSelected && 'bg-mineshaft-500',
|
||||||
isDisabled && 'text-gray-500 hover:bg-transparent cursor-not-allowed',
|
isDisabled && 'hover:bg-transparent cursor-not-allowed',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Item type="button" role="menuitem" class="flex items-center" ref={inputRef}>
|
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
|
||||||
{icon && <span className="mr-2">{icon}</span>}
|
<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>
|
<span className="flex-grow text-left">{children}</span>
|
||||||
</Item>
|
</Item>
|
||||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
|||||||
<Card
|
<Card
|
||||||
isRounded
|
isRounded
|
||||||
className={twMerge(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -36,7 +36,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
|||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="close"
|
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" />
|
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
dropdownContainerClassName?: string;
|
dropdownContainerClassName?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
position?: 'item-aligned' | 'popper';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SelectProps = SelectPrimitive.SelectProps & Props;
|
export type SelectProps = SelectPrimitive.SelectProps & Props;
|
||||||
|
|
||||||
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||||
(
|
(
|
||||||
{ children, placeholder, className, isLoading, dropdownContainerClassName, ...props },
|
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
|
||||||
ref
|
ref
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
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]',
|
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
|
||||||
dropdownContainerClassName
|
dropdownContainerClassName
|
||||||
)}
|
)}
|
||||||
|
position={position}
|
||||||
|
style={{ width: 'var(--radix-select-trigger-width)' }}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.ScrollUpButton>
|
<SelectPrimitive.ScrollUpButton>
|
||||||
<FontAwesomeIcon icon={faChevronUp} size="sm" />
|
<FontAwesomeIcon icon={faChevronUp} size="sm" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
<SelectPrimitive.Viewport className="p-1.5">
|
<SelectPrimitive.Viewport className="p-1">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<Spinner size="xs" />
|
<Spinner size="xs" />
|
||||||
@@ -82,7 +85,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`relative flex cursor-pointer
|
`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`,
|
outline-none transition-all hover:bg-mineshaft-500`,
|
||||||
isSelected && 'bg-primary',
|
isSelected && 'bg-primary',
|
||||||
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-gray-600',
|
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}
|
ref={forwardedRef}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.ItemIndicator className="absolute left-3.5">
|
<SelectPrimitive.ItemIndicator className="absolute left-3.5 text-primary">
|
||||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
<FontAwesomeIcon icon={faCheck} />
|
||||||
</SelectPrimitive.ItemIndicator>
|
</SelectPrimitive.ItemIndicator>
|
||||||
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
1
frontend/src/context/OrganizationContext/index.tsx
Normal file
1
frontend/src/context/OrganizationContext/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { OrgProvider, useOrganization } from './OrganizationContext';
|
||||||
38
frontend/src/context/UserContext/UserContext.tsx
Normal file
38
frontend/src/context/UserContext/UserContext.tsx
Normal 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;
|
||||||
|
};
|
||||||
1
frontend/src/context/UserContext/index.tsx
Normal file
1
frontend/src/context/UserContext/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { UserProvider, useUser } from './UserContext';
|
||||||
@@ -23,9 +23,10 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
|
|||||||
|
|
||||||
// memorize the workspace details for the context
|
// memorize the workspace details for the context
|
||||||
const value = useMemo<TWorkspaceContext>(() => {
|
const value = useMemo<TWorkspaceContext>(() => {
|
||||||
|
const wsId = workspaceId || localStorage.getItem('projectData.id');
|
||||||
return {
|
return {
|
||||||
workspaces: ws || [],
|
workspaces: ws || [],
|
||||||
currentWorkspace: (ws || []).find(({ _id: id }) => id === workspaceId),
|
currentWorkspace: (ws || []).find(({ _id: id }) => id === wsId),
|
||||||
isLoading
|
isLoading
|
||||||
};
|
};
|
||||||
}, [ws, workspaceId, isLoading]);
|
}, [ws, workspaceId, isLoading]);
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
|
export { AuthProvider } from './AuthContext';
|
||||||
|
export { OrgProvider, useOrganization } from './OrganizationContext';
|
||||||
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
|
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
|
||||||
|
export { UserProvider, useUser } from './UserContext';
|
||||||
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
|
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './keys';
|
export * from './keys';
|
||||||
|
export * from './organization';
|
||||||
export * from './serviceTokens';
|
export * from './serviceTokens';
|
||||||
export * from './subscriptions';
|
export * from './subscriptions';
|
||||||
export * from './tags';
|
export * from './tags';
|
||||||
|
export * from './users';
|
||||||
export * from './workspace';
|
export * from './workspace';
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { useGetUserWsKey } from './queries';
|
export { useGetUserWsKey, useUploadWsKey } from './queries';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { apiRequest } from '@app/config/request';
|
import { apiRequest } from '@app/config/request';
|
||||||
|
|
||||||
import { UserWsKeyPair } from './types';
|
import { UploadWsKeyDTO, UserWsKeyPair } from './types';
|
||||||
|
|
||||||
const encKeyKeys = {
|
const encKeyKeys = {
|
||||||
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
|
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
|
||||||
@@ -22,3 +22,10 @@ export const useGetUserWsKey = (workspaceID: string) =>
|
|||||||
queryFn: () => fetchUserWsKey(workspaceID),
|
queryFn: () => fetchUserWsKey(workspaceID),
|
||||||
enabled: Boolean(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 } })
|
||||||
|
});
|
||||||
|
|||||||
@@ -20,3 +20,10 @@ export type Sender = {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UploadWsKeyDTO = {
|
||||||
|
userId: string;
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
|||||||
1
frontend/src/hooks/api/organization/index.ts
Normal file
1
frontend/src/hooks/api/organization/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { useGetOrganization } from './queries';
|
||||||
18
frontend/src/hooks/api/organization/queries.tsx
Normal file
18
frontend/src/hooks/api/organization/queries.tsx
Normal 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 });
|
||||||
6
frontend/src/hooks/api/organization/types.ts
Normal file
6
frontend/src/hooks/api/organization/types.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type Organization = {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
createAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
export type { GetAuthTokenAPI } from './auth/types';
|
export type { GetAuthTokenAPI } from './auth/types';
|
||||||
export type { UserWsKeyPair } from './keys/types';
|
export type { UserWsKeyPair } from './keys/types';
|
||||||
|
export type { Organization } from './organization/types';
|
||||||
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
||||||
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
||||||
|
export type { User } from './users/types';
|
||||||
export type {
|
export type {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
DeleteEnvironmentDTO,
|
DeleteEnvironmentDTO,
|
||||||
|
|||||||
7
frontend/src/hooks/api/users/index.tsx
Normal file
7
frontend/src/hooks/api/users/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export {
|
||||||
|
fetchOrgUsers,
|
||||||
|
useAddUserToWs,
|
||||||
|
useGetOrgUsers,
|
||||||
|
useGetUser,
|
||||||
|
useLogoutUser
|
||||||
|
} from './queries';
|
||||||
84
frontend/src/hooks/api/users/queries.tsx
Normal file
84
frontend/src/hooks/api/users/queries.tsx
Normal 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', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
39
frontend/src/hooks/api/users/types.ts
Normal file
39
frontend/src/hooks/api/users/types.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export {
|
export {
|
||||||
|
useCreateWorkspace,
|
||||||
useCreateWsEnvironment,
|
useCreateWsEnvironment,
|
||||||
useDeleteWorkspace,
|
useDeleteWorkspace,
|
||||||
useDeleteWsEnvironment,
|
useDeleteWsEnvironment,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { apiRequest } from '@app/config/request';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
|
CreateWorkspaceDTO,
|
||||||
DeleteEnvironmentDTO,
|
DeleteEnvironmentDTO,
|
||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
RenameWorkspaceDTO,
|
RenameWorkspaceDTO,
|
||||||
@@ -40,6 +41,18 @@ export const useGetUserWorkspaces = () =>
|
|||||||
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
||||||
|
|
||||||
// mutation
|
// 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 = () => {
|
export const useRenameWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ export type WorkspaceEnv = { name: string; slug: string };
|
|||||||
export type WorkspaceTag = { _id: string; name: string; slug: string };
|
export type WorkspaceTag = { _id: string; name: string; slug: string };
|
||||||
|
|
||||||
// mutation dto
|
// mutation dto
|
||||||
|
export type CreateWorkspaceDTO = {
|
||||||
|
workspaceName: string;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||||
|
|
||||||
|
|||||||
427
frontend/src/layouts/AppLayout/AppLayout.tsx
Normal file
427
frontend/src/layouts/AppLayout/AppLayout.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
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 { 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { config } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
import Layout from '@app/components/basic/Layout';
|
|
||||||
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
||||||
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
||||||
import { publicPaths } from '@app/const';
|
import { publicPaths } from '@app/const';
|
||||||
import { SubscriptionProvider } from '@app/context';
|
import {
|
||||||
import { AuthProvider } from '@app/context/AuthContext';
|
AuthProvider,
|
||||||
import { WorkspaceProvider } from '@app/context/WorkspaceContext';
|
OrgProvider,
|
||||||
|
SubscriptionProvider,
|
||||||
|
UserProvider,
|
||||||
|
WorkspaceProvider
|
||||||
|
} from '@app/context';
|
||||||
|
import { AppLayout } from '@app/layouts';
|
||||||
import { queryClient } from '@app/reactQuery';
|
import { queryClient } from '@app/reactQuery';
|
||||||
|
|
||||||
import '@fortawesome/fontawesome-svg-core/styles.css';
|
import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||||
@@ -71,13 +75,17 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<WorkspaceProvider>
|
<WorkspaceProvider>
|
||||||
<SubscriptionProvider>
|
<OrgProvider>
|
||||||
<NotificationProvider>
|
<SubscriptionProvider>
|
||||||
<Layout>
|
<UserProvider>
|
||||||
<Component {...pageProps} />
|
<NotificationProvider>
|
||||||
</Layout>
|
<AppLayout>
|
||||||
</NotificationProvider>
|
<Component {...pageProps} />
|
||||||
</SubscriptionProvider>
|
</AppLayout>
|
||||||
|
</NotificationProvider>
|
||||||
|
</UserProvider>
|
||||||
|
</SubscriptionProvider>
|
||||||
|
</OrgProvider>
|
||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { faSlack } from '@fortawesome/free-brands-svg-icons';
|
import { faSlack } from '@fortawesome/free-brands-svg-icons';
|
||||||
@@ -142,6 +143,10 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll pt-4">
|
<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="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-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">
|
<div className="text-md text-left w-full pt-2 pb-4 text-bunker-300">
|
||||||
|
|||||||
Reference in New Issue
Block a user