Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-01-30 19:48:12 +07:00
167 changed files with 5540 additions and 1256 deletions
@@ -5,6 +5,8 @@ import posthog from 'posthog-js';
import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config';
export const initPostHog = () => {
// @ts-ignore
console.log("Init Infisical")
try {
if (typeof window !== 'undefined') {
// @ts-ignore
@@ -86,7 +86,7 @@ export const AddUpdateEnvironmentDialog = ({
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
>
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
@@ -1,8 +1,8 @@
import { Fragment } from 'react';
import { useRouter } from 'next/router';
import { Dialog, Transition } from '@headlessui/react';
import { plans } from 'public/data/frequentConstants';
import { STRIPE_PRODUCT_STARTER } from '../../utilities/config';
import Button from '../buttons/Button';
import InputField from '../InputField';
@@ -81,7 +81,7 @@ const AddUserDialog = ({
isRequired
/>
</div>
{currentPlan === STRIPE_PRODUCT_STARTER && (
{currentPlan === plans.starter && (
<div className='flex flex-row'>
<button
type='button'
@@ -90,11 +90,11 @@ const AddUserDialog = ({
router.push(`/settings/billing/${ router.query.id}`)
}
>
You can add up to 5 members on a Free tier.
You can add up to 5 members on the Starter plan.
</button>
<button
type='button'
className='ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
className='ml-1 inline-flex justify-center rounded-md py-1 underline underline-offset-2 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={() =>
router.push(`/settings/billing/${ router.query.id}`)
}
@@ -38,7 +38,7 @@ const DeleteActionModal = ({
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='fixed inset-0 bg-black bg-opacity-25' />
<div className='fixed inset-0 bg-black bg-opacity-70' />
</Transition.Child>
<div className='fixed inset-0 overflow-y-auto'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
@@ -51,7 +51,7 @@ const DeleteActionModal = ({
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
>
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
@@ -0,0 +1,86 @@
import { Fragment } from 'react';
import { useRouter } from 'next/router';
import { Dialog, Transition } from '@headlessui/react';
// REFACTOR: Move all these modals into one reusable one
type Props = {
isOpen?: boolean;
onClose: ()=>void;
text: string;
}
const UpgradePlanModal = ({
isOpen,
onClose,
text,
}:Props) => {
const router = useRouter();
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as='div' className='relative z-10' onClose={onClose}>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-150'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='fixed inset-0 bg-black bg-opacity-50 drop-shadow-xl' />
</Transition.Child>
<div className='fixed inset-0 overflow-y-auto'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
>
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-bunker-400 p-6 pt-5 text-left align-middle shadow-xl transition-all'>
<Dialog.Title
as='h3'
className='text-lg font-medium leading-6 text-bunker-200'
>
Unleash Infisical&apos;s Full Power
</Dialog.Title>
<div className='mt-2'>
<p className='text-sm text-bunker-300 mb-0.5'>
{text}
</p>
<p className='text-sm text-bunker-300'>
Upgrade and get access to this, as well as to other powerful enhancements.
</p>
</div>
<div className='mt-4'>
<button
type='button'
className='inline-flex justify-center rounded-md border border-transparent bg-primary opacity-90 hover:opacity-100 px-4 py-2 text-sm font-medium text-black hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={() => router.push(`/settings/billing/${localStorage.getItem("projectData.id")}`)}
>
Upgrade Now
</button>
<button
type='button'
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-red hover:text-red hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={onClose}
>
Maybe Later
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};
export default UpgradePlanModal;
@@ -1,9 +1,13 @@
import { useEffect, useState } from 'react';
import { faPencil, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
import { plans } from 'public/data/frequentConstants';
import { usePopUp } from '../../../hooks/usePopUp';
import getOrganizationSubscriptions from '../../../pages/api/organization/GetOrgSubscription';
import Button from '../buttons/Button';
import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog';
import DeleteActionModal from '../dialog/DeleteActionModal';
import UpgradePlanModal from '../dialog/UpgradePlan';
type Env = { name: string; slug: string };
@@ -17,8 +21,24 @@ type Props = {
const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'createUpdateEnv',
'deleteEnv'
'deleteEnv',
'upgradePlan'
] as const);
const [plan, setPlan] = useState('');
const host = window.location.origin;
useEffect(() => {
// on initial load - run auth check
(async () => {
const orgId = localStorage.getItem('orgData.id') as string;
const subscriptions = await getOrganizationSubscriptions({
orgId
});
setPlan(subscriptions.data[0].plan.product);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onEnvCreateCB = async (env: Env) => {
try {
@@ -63,7 +83,13 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
<div className="w-48">
<Button
text="Add New Env"
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
onButtonPressed={() => {
if (plan !== plans.starter || host !== "https://app.infisical.com") {
handlePopUpOpen('createUpdateEnv')
} else {
handlePopUpOpen('upgradePlan')
}
}}
color="mineshaft"
icon={faPlus}
size="md"
@@ -88,18 +114,30 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
{name}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">{slug}</td>
<td className="py-2 border-mineshaft-700 border-t flex">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center mr-8">
<td className="py-2 border-mineshaft-700 border-t flex justify-end">
<div className="hover:opacity-100 duration-200 flex items-center mr-2">
<Button
onButtonPressed={() => handlePopUpOpen('createUpdateEnv', { name, slug })}
color="red"
onButtonPressed={() => {
if (plan !== plans.starter || host !== "https://app.infisical.com") {
handlePopUpOpen('createUpdateEnv', { name, slug })
} else {
handlePopUpOpen('upgradePlan')
}
}}
color="mineshaft"
size="icon-sm"
icon={faPencil}
/>
</div>
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center mr-6">
<Button
onButtonPressed={() => handlePopUpOpen('deleteEnv', { name, slug })}
onButtonPressed={() => {
if (plan !== plans.starter || host !== "https://app.infisical.com") {
handlePopUpOpen('deleteEnv', { name, slug })
} else {
handlePopUpOpen('upgradePlan')
}
}}
color="red"
size="icon-sm"
icon={faX}
@@ -134,6 +172,11 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
onCreateSubmit={onEnvCreateCB}
onEditSubmit={onEnvUpdateCB}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onClose={() => handlePopUpClose('upgradePlan')}
text="You can add custom environments if you switch to Infisical's Team plan."
/>
</div>
</>
);
@@ -1,3 +1,5 @@
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
@@ -6,7 +8,16 @@ const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['v2'],
argTypes: {}
argTypes: {
isRounded: {
defaultValue: true,
type: 'boolean'
},
isFullWidth: {
defaultValue: false,
type: 'boolean'
}
}
};
export default meta;
@@ -18,3 +29,61 @@ export const Primary: Story = {
children: 'Hello Infisical'
}
};
export const Secondary: Story = {
args: {
children: 'Hello Infisical',
colorSchema: 'secondary',
variant: 'outline'
}
};
export const Danger: Story = {
args: {
children: 'Hello Infisical',
colorSchema: 'danger',
variant: 'solid'
}
};
export const Plain: Story = {
args: {
children: 'Hello Infisical',
variant: 'plain'
}
};
export const Disabled: Story = {
args: {
children: 'Hello Infisical',
disabled: true
}
};
export const FullWidth: Story = {
args: {
children: 'Hello Infisical',
isFullWidth: true
}
};
export const Loading: Story = {
args: {
children: 'Hello Infisical',
isLoading: true
}
};
export const LeftIcon: Story = {
args: {
children: 'Hello Infisical',
leftIcon: <FontAwesomeIcon icon={faPlus} className="pr-0.5" />
}
};
export const RightIcon: Story = {
args: {
children: 'Hello Infisical',
rightIcon: <FontAwesomeIcon icon={faPlus} className="pr-0.5" />
}
};
+141 -7
View File
@@ -1,32 +1,166 @@
import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react';
import { cva, VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
type Props = {
children: ReactNode;
isDisabled?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
// loading state
isLoading?: boolean;
// various button sizes
size: 'sm' | 'md' | 'lg';
};
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & Props;
const buttonVariants = cva(
[
'button',
'transition-all',
'font-inter font-medium',
'cursor-pointer',
'inline-flex items-center justify-center',
'relative'
],
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red']
},
variant: {
solid: '',
outline: ['bg-transparent', 'border-2', 'border-solid'],
plain: ''
},
isDisabled: {
true: 'bg-opacity-70 cursor-not-allowed',
false: ''
},
isFullWidth: {
true: 'w-full',
false: ''
},
isRounded: {
true: 'rounded-md',
false: ''
},
size: {
xs: ['text-xs', 'py-1', 'px-2'],
sm: ['text-sm', 'py-2', 'px-4'],
md: ['text-md', 'py-2', 'px-6'],
lg: ['text-lg', 'py-2', 'px-8']
}
},
compoundVariants: [
{
colorSchema: 'primary',
variant: 'outline',
className: 'text-primary hover:bg-primary hover:text-black'
},
{
colorSchema: 'secondary',
variant: 'outline',
className: 'hover:bg-mineshaft'
},
{
colorSchema: 'danger',
variant: 'outline',
className: 'text-red hover:bg-red hover:text-black'
},
{
colorSchema: 'primary',
variant: 'plain',
className: 'text-primary'
},
{
colorSchema: 'secondary',
variant: 'plain',
className: 'text-mineshaft'
},
{
colorSchema: 'danger',
variant: 'plain',
className: 'text-red'
},
{
colorSchema: ['danger', 'primary', 'secondary'],
variant: ['plain'],
className: 'bg-transparent py-1 px-1'
}
]
}
);
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> &
Props;
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, isDisabled = false, className, ...props }, ref): JSX.Element => {
(
{
children,
isDisabled = false,
className = '',
size = 'md',
variant = 'solid',
isFullWidth,
isRounded = true,
leftIcon,
rightIcon,
isLoading,
colorSchema = 'primary',
...props
},
ref
): JSX.Element => {
const loadingToggleClass = isLoading ? 'opacity-0' : 'opacity-100';
return (
<button
ref={ref}
aria-disabled={isDisabled}
type="button"
className={twMerge(
'bg-primary hover:opacity-80 transition-all text-sm px-4 py-2 font-bold rounded',
className
buttonVariants({
className,
colorSchema,
size,
variant,
isRounded,
isDisabled,
isFullWidth
})
)}
disabled={isDisabled}
{...props}
>
{children}
{isLoading && (
<img
src="/images/loading/loadingblack.gif"
width={36}
alt="loading animation"
className="absolute rounded-xl"
/>
)}
<span
className={twMerge(
'transition-all shrink-0 cursor-pointer',
loadingToggleClass,
size === 'xs' ? 'mr-1' : 'mr-2'
)}
>
{leftIcon}
</span>
<span className={twMerge('transition-all', loadingToggleClass)}>{children}</span>
<span
className={twMerge(
'transition-all shrink-0 cursor-pointer',
loadingToggleClass,
size === 'xs' ? 'ml-1' : 'ml-2'
)}
>
{rightIcon}
</span>
</button>
);
}
@@ -1 +1,2 @@
export type { ButtonProps } from './Button';
export { Button } from './Button';
@@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Card, CardBody, CardFooter, CardProps, CardTitle } from './Card';
const meta: Meta<typeof Card> = {
title: 'Components/Card',
component: Card,
tags: ['v2'],
argTypes: {
isRounded: {
type: 'boolean',
defaultValue: true
}
}
};
export default meta;
type Story = StoryObj<typeof Card>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
const Template = (args: CardProps) => (
<div className="w-96">
<Card {...args}>
<CardTitle subTitle="Please add your subtitle here">Title</CardTitle>
<CardBody>Content</CardBody>
<CardFooter>Footer</CardFooter>
</Card>
</div>
);
export const Basic: Story = {
render: (args) => <Template {...args} />
};
export const Hoverable: Story = {
render: (args) => <Template {...args} />,
args: {
isHoverable: true
}
};
+64
View File
@@ -0,0 +1,64 @@
import { forwardRef, ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
export type CardTitleProps = {
children: ReactNode;
subTitle?: string;
className?: string;
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
<div className={twMerge('p-6 pb-4 font-sans text-xl font-medium', className)}>
{children}
{subTitle && <p className="py-1 text-sm font-normal text-gray-400">{subTitle}</p>}
</div>
);
export type CardFooterProps = {
children: ReactNode;
className?: string;
};
export const CardFooter = ({ children, className }: CardFooterProps) => (
<div className={twMerge('p-6 pt-0', className)}>{children}</div>
);
export type CardBodyProps = {
children: ReactNode;
className?: string;
};
export const CardBody = ({ children, className }: CardBodyProps) => (
<div className={twMerge('p-6 pt-0', className)}>{children}</div>
);
export type CardProps = {
children: ReactNode;
className?: string;
isFullHeight?: boolean;
isRounded?: boolean;
isPlain?: boolean;
isHoverable?: boolean;
};
export const Card = forwardRef<HTMLDivElement, CardProps>(
({ children, isFullHeight, isRounded, isHoverable, isPlain, className }, ref): JSX.Element => {
return (
<div
ref={ref}
className={twMerge(
'flex flex-col w-full font-inter text-gray-200 bg-mineshaft-700 shadow-md',
isFullHeight && 'h-full',
isRounded && 'rounded-md',
isPlain && 'shadow-none',
isHoverable && 'hover:shadow-xl',
className
)}
>
{children}
</div>
);
}
);
Card.displayName = 'Card';
@@ -0,0 +1,2 @@
export type { CardBodyProps, CardFooterProps, CardProps, CardTitleProps } from './Card';
export { Card, CardBody, CardFooter, CardTitle } from './Card';
@@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Checkbox } from './Checkbox';
const meta: Meta<typeof Checkbox> = {
title: 'Components/Checkbox',
component: Checkbox,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Checkbox>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Simple: Story = {
args: {
children: 'Accept the condition'
}
};
export const Disabled: Story = {
args: {
children: 'Accept the condition',
isDisabled: true
}
};
export const Required: Story = {
args: {
children: 'Accept the condition',
isRequired: true
}
};
@@ -0,0 +1,51 @@
import { ReactNode } from 'react';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { twMerge } from 'tailwind-merge';
export type CheckboxProps = Omit<
CheckboxPrimitive.CheckboxProps,
'checked' | 'disabled' | 'required'
> & {
children: ReactNode;
id: string;
isDisabled?: boolean;
isChecked?: boolean;
isRequired?: boolean;
};
export const Checkbox = ({
children,
className,
id,
isChecked,
isDisabled,
isRequired,
...props
}: CheckboxProps): JSX.Element => {
return (
<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',
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
className
)}
required={isRequired}
checked={isChecked}
disabled={isDisabled}
{...props}
id={id}
>
<CheckboxPrimitive.Indicator className="text-bunker-800">
<FontAwesomeIcon icon={faCheck} size="sm" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
<label className="text-sm" htmlFor={id}>
{children}
{isRequired && <span className="pl-1 text-red">*</span>}
</label>
</div>
);
};
@@ -0,0 +1,2 @@
export type { CheckboxProps } from './Checkbox';
export { Checkbox } from './Checkbox';
@@ -0,0 +1,111 @@
import { faPlus, faTrash, faUndo } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { Meta, StoryObj } from '@storybook/react';
import { IconButton } from '../IconButton';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from './Dropdown';
const meta: Meta<typeof DropdownMenu> = {
title: 'Components/DropdownMenu',
component: DropdownMenu,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof DropdownMenuContent>;
export const Basic: Story = {
render: (args) => (
<div className="flex justify-center w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton ariaLabel="add">
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" {...args}>
<DropdownMenuItem>Delete</DropdownMenuItem>
<DropdownMenuItem>Undo</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
};
export const Icons: Story = {
render: (args) => (
<div className="flex justify-center w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton ariaLabel="add">
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" {...args}>
<DropdownMenuItem icon={<FontAwesomeIcon icon={faTrash} size="sm" />}>
Delete
</DropdownMenuItem>
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUndo} size="sm" />}>
Undo
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
};
export const WithDivider: Story = {
render: (args) => (
<div className="flex justify-center w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton ariaLabel="add">
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" {...args}>
<DropdownMenuItem>Delete</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Undo</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Redo</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
};
export const Group: Story = {
render: (args) => (
<div className="flex justify-center w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton ariaLabel="add">
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" {...args}>
<DropdownMenuGroup>
<DropdownMenuLabel>Group</DropdownMenuLabel>
<DropdownMenuItem>Undo</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuGroup>
<DropdownMenuLabel>Group#2</DropdownMenuLabel>
<DropdownMenuItem>Undo</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
};
@@ -0,0 +1,97 @@
import { ComponentPropsWithRef, ElementType, forwardRef, ReactNode, Ref } from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { twMerge } from 'tailwind-merge';
// Main menu or parent container
export type DropdownMenuProps = DropdownMenuPrimitive.DropdownMenuProps;
export const DropdownMenu = DropdownMenuPrimitive.Root;
// trigger
export type DropdownMenuTriggerProps = DropdownMenuPrimitive.DropdownMenuTriggerProps;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
// item container
export type DropdownMenuContentProps = DropdownMenuPrimitive.MenuContentProps;
export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
({ children, className, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
sideOffset={10}
{...props}
ref={forwardedRef}
className={twMerge(
'min-w-[220px] bg-bunker will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade',
className
)}
>
{children}
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
);
}
);
DropdownMenuContent.displayName = 'DropdownMenuContent';
// item label component
export type DropdownLabelProps = DropdownMenuPrimitive.MenuLabelProps;
export const DropdownMenuLabel = ({ className, ...props }: DropdownLabelProps) => (
<DropdownMenuPrimitive.Label
{...props}
className={twMerge('text-xs text-bunker-400 px-4 pt-2 pb-1', className)}
/>
);
// dropdown items
export type DropdownMenuItemProps<T extends ElementType> =
DropdownMenuPrimitive.MenuContentProps & {
icon?: ReactNode;
as?: T;
inputRef?: Ref<T>;
};
export const DropdownMenuItem = <T extends ElementType = 'button'>({
children,
inputRef,
className,
icon,
as: Item = 'button',
...props
}: DropdownMenuItemProps<T> & ComponentPropsWithRef<T>) => (
<DropdownMenuPrimitive.Item
{...props}
className={twMerge(
'text-sm block font-inter px-4 py-2 data-[highlighted]:bg-gray-700 outline-none cursor-pointer',
className
)}
>
<Item type="button" role="menuitem" class="flex w-full items-center" ref={inputRef}>
{icon && <span className="flex items-center mr-2">{icon}</span>}
<span className="flex-grow text-left">{children}</span>
</Item>
</DropdownMenuPrimitive.Item>
);
// grouping items into 1
export type DropdownMenuGroupProps = DropdownMenuPrimitive.DropdownMenuGroupProps;
export const DropdownMenuGroup = forwardRef<HTMLDivElement, DropdownMenuGroupProps>(
({ ...props }, ref) => <DropdownMenuPrimitive.Group {...props} ref={ref} />
);
DropdownMenuGroup.displayName = 'DropdownMenuGroup';
// Divider
export const DropdownMenuSeparator = forwardRef<
HTMLDivElement,
DropdownMenuPrimitive.DropdownMenuSeparatorProps
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
{...props}
className={twMerge('h-[1px] bg-gray-700 m-1', className)}
/>
));
DropdownMenuSeparator.displayName = 'DropdownMenuSeperator';
@@ -0,0 +1,17 @@
export type {
DropdownLabelProps,
DropdownMenuContentProps,
DropdownMenuGroupProps,
DropdownMenuItemProps,
DropdownMenuProps,
DropdownMenuTriggerProps
} from './Dropdown';
export {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from './Dropdown';
@@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react';
// Be careful on dep cycle
import { Input } from '../Input/Input';
import { FormControl } from './FormControl';
const meta: Meta<typeof FormControl> = {
title: 'Components/FormControl',
component: FormControl,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof FormControl>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Basic: Story = {
args: {
children: <Input />,
label: 'Email',
id: 'email',
helperText: 'Type something..'
}
};
export const RequiredInput: Story = {
args: {
...Basic.args,
isRequired: true
}
};
export const ErrorInput: Story = {
args: {
...Basic.args,
errorText: 'Some random error',
isError: true
}
};
@@ -0,0 +1,72 @@
import { cloneElement, ReactNode } from 'react';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as Label from '@radix-ui/react-label';
import { twMerge } from 'tailwind-merge';
export type FormLabelProps = {
id?: string;
isRequired?: boolean;
label?: ReactNode;
};
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
<Label.Root className="text-mineshaft-300 text-sm font-medium block mb-1 ml-0.5" htmlFor={id}>
{label}
{isRequired && <span className="ml-1 text-red">*</span>}
</Label.Root>
);
export type FormHelperTextProps = {
isError?: boolean;
text?: ReactNode;
};
export const FormHelperText = ({ isError, text }: FormHelperTextProps) => (
<div
className={twMerge(
'text-xs font-inter flex items-center opacity-90 text-mineshaft-300 mt-2',
isError && 'text-red-600'
)}
>
{isError && (
<span>
<FontAwesomeIcon icon={faExclamationTriangle} size="sm" className="mr-1" />
</span>
)}
<span>{text}</span>
</div>
);
export type FormControlProps = {
id?: string;
isRequired?: boolean;
isError?: boolean;
label?: ReactNode;
helperText?: ReactNode;
errorText?: ReactNode;
children: JSX.Element;
};
export const FormControl = ({
children,
isRequired,
label,
helperText,
errorText,
id,
isError
}: FormControlProps): JSX.Element => {
return (
<div>
{typeof label === 'string' ? (
<FormLabel label={label} isRequired={isRequired} id={id} />
) : (
label
)}
{cloneElement(children, { isRequired, 'data-required': isRequired, isError })}
{!isError && helperText && <FormHelperText isError={isError} text={helperText} />}
{isError && errorText && <FormHelperText isError={isError} text={errorText} />}
</div>
);
};
@@ -0,0 +1,2 @@
export type { FormControlProps, FormHelperTextProps, FormLabelProps } from './FormControl';
export { FormControl, FormHelperText, FormLabel } from './FormControl';
@@ -0,0 +1,60 @@
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { Meta, StoryObj } from '@storybook/react';
import { IconButton } from './IconButton';
const meta: Meta<typeof IconButton> = {
title: 'Components/IconButton',
component: IconButton,
tags: ['v2'],
argTypes: {
isRounded: {
defaultValue: true,
type: 'boolean'
},
ariaLabel: {
defaultValue: 'Some buttons...'
}
}
};
export default meta;
type Story = StoryObj<typeof IconButton>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Primary: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />
}
};
export const Secondary: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />,
colorSchema: 'secondary',
variant: 'outline'
}
};
export const Danger: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />,
colorSchema: 'danger',
variant: 'solid'
}
};
export const Plain: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />,
variant: 'plain'
}
};
export const Disabled: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />,
disabled: true
}
};
@@ -0,0 +1,131 @@
import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react';
import { cva, VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
type Props = {
children: ReactNode;
// This is kept as required because by accessibility convention and eslint
// when button doesn't have text an aria-label needs to be passed
ariaLabel: string;
isDisabled?: boolean;
};
const iconButtonVariants = cva(
[
'button',
'transition-all',
'font-inter font-medium',
'cursor-pointer',
'inline-flex items-center justify-center',
'relative'
],
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red']
},
variant: {
solid: '',
outline: ['bg-transparent', 'border-2', 'border-solid'],
plain: ''
},
isDisabled: {
true: 'bg-opacity-70 cursor-not-allowed',
false: ''
},
isRounded: {
true: 'rounded-md',
false: ''
},
size: {
xs: ['text-xs', 'py-1', 'px-2'],
sm: ['text-sm', 'py-2', 'px-3'],
md: ['text-md', 'py-3', 'px-4'],
lg: ['text-lg', 'py-3', 'px-6']
}
},
compoundVariants: [
{
colorSchema: 'primary',
variant: 'outline',
className: 'text-primary hover:bg-primary hover:text-black'
},
{
colorSchema: 'secondary',
variant: 'outline',
className: 'hover:bg-mineshaft'
},
{
colorSchema: 'danger',
variant: 'outline',
className: 'text-red hover:bg-red hover:text-black'
},
{
colorSchema: 'primary',
variant: 'plain',
className: 'text-primary'
},
{
colorSchema: 'secondary',
variant: 'plain',
className: 'text-mineshaft'
},
{
colorSchema: 'danger',
variant: 'plain',
className: 'text-red'
},
{
colorSchema: ['danger', 'primary', 'secondary'],
variant: ['plain'],
className: 'bg-transparent py-1 px-1'
}
]
}
);
export type IconButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label'> &
VariantProps<typeof iconButtonVariants> &
Props;
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(
{
children,
ariaLabel,
isDisabled = false,
className,
size = 'md',
variant = 'solid',
isRounded = true,
colorSchema = 'primary',
...props
},
ref
): JSX.Element => (
<button
ref={ref}
aria-disabled={isDisabled}
type="button"
aria-label={ariaLabel}
className={twMerge(
iconButtonVariants({
className,
colorSchema,
size,
variant,
isRounded,
isDisabled
})
)}
disabled={isDisabled}
{...props}
>
{children}
</button>
)
);
IconButton.displayName = 'IconButton';
@@ -0,0 +1,2 @@
export type { IconButtonProps } from './IconButton';
export { IconButton } from './IconButton';
@@ -0,0 +1,66 @@
import { faEye, faMailBulk } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { Meta, StoryObj } from '@storybook/react';
import { Input } from './Input';
const meta: Meta<typeof Input> = {
title: 'Components/Input',
component: Input,
tags: ['v2'],
argTypes: {
isRounded: {
defaultValue: true,
type: 'boolean'
},
placeholder: {
defaultValue: 'Type anything',
type: 'string'
}
}
};
export default meta;
type Story = StoryObj<typeof Input>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Filled: Story = {
args: {}
};
export const Outline: Story = {
args: {
variant: 'outline'
}
};
export const Plain: Story = {
args: {
variant: 'plain'
}
};
export const Error: Story = {
args: {
isError: true
}
};
export const AutoWidth: Story = {
args: {
isFullWidth: false,
className: 'w-auto'
}
};
export const RightIcon: Story = {
args: {
rightIcon: <FontAwesomeIcon icon={faEye} />
}
};
export const LeftIcon: Story = {
args: {
leftIcon: <FontAwesomeIcon icon={faMailBulk} />
}
};
+102
View File
@@ -0,0 +1,102 @@
import { forwardRef, InputHTMLAttributes, ReactNode } from 'react';
import { cva, VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
type Props = {
placeholder?: string;
isFullWidth?: boolean;
isRequired?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
};
const inputVariants = cva(
'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50',
{
variants: {
size: {
xs: ['text-xs'],
sm: ['text-sm'],
md: ['text-md'],
lg: ['text-lg']
},
isRounded: {
true: ['rounded-md'],
false: ''
},
variant: {
filled: ['bg-bunker-800', 'text-gray-400'],
outline: ['bg-transparent'],
plain: 'bg-transparent outline-none'
},
isError: {
true: 'focus:ring-red/50 placeholder-red-300',
false: 'focus:ring-primary/50'
}
},
compoundVariants: []
}
);
const inputParentContainerVariants = cva('inline-flex font-inter items-center border relative', {
variants: {
isRounded: {
true: ['rounded-md'],
false: ''
},
isError: {
true: 'border-red',
false: 'border-mineshaft-400'
},
isFullWidth: {
true: 'w-full',
false: ''
},
variant: {
filled: ['bg-bunker-800', 'text-gray-400'],
outline: ['bg-transparent'],
plain: 'border-none'
}
}
});
export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> &
VariantProps<typeof inputVariants> &
Props;
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
className,
isRounded = true,
isFullWidth = true,
isError = false,
isRequired,
leftIcon,
rightIcon,
variant = 'filled',
size = 'md',
...props
},
ref
): JSX.Element => {
return (
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
{leftIcon && <span className="absolute left-0 ml-2">{leftIcon}</span>}
<input
{...props}
required={isRequired}
ref={ref}
className={twMerge(
leftIcon ? 'pl-9' : 'pl-4',
rightIcon ? 'pr-9' : 'pr-4',
inputVariants({ className, isError, size, isRounded, variant })
)}
/>
{rightIcon && <span className="absolute right-0 mr-2">{rightIcon}</span>}
</div>
);
}
);
Input.displayName = 'Input';
@@ -0,0 +1,2 @@
export type { InputProps } from './Input';
export { Input } from './Input';
@@ -0,0 +1,107 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { faKey, faUser } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Meta, StoryObj } from '@storybook/react';
import { Menu, MenuGroup, MenuItem } from './Menu';
const meta: Meta<typeof Menu> = {
title: 'Components/Menu',
component: Menu,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Menu>;
export const Basic: Story = {
render: (args) => (
<Menu {...args}>
<MenuItem>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
<MenuItem>Integrations</MenuItem>
</Menu>
),
args: {}
};
export const SelectedItem: Story = {
render: (args) => (
<Menu {...args}>
<MenuItem isSelected>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
<MenuItem>Integrations</MenuItem>
</Menu>
),
args: {}
};
export const GroupedItem: Story = {
render: (args) => (
<Menu {...args}>
<MenuGroup title="Group 1">
<MenuItem>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
</MenuGroup>
<MenuGroup title="Group 2">
<MenuItem>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
</MenuGroup>
</Menu>
),
args: {}
};
export const DisabledItem: Story = {
render: (args) => (
<Menu {...args}>
<MenuGroup title="Group 1">
<MenuItem isDisabled>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
</MenuGroup>
<MenuGroup title="Group 2">
<MenuItem>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
</MenuGroup>
</Menu>
),
args: {}
};
export const WithIcons: Story = {
render: (args) => (
<Menu {...args}>
<MenuGroup title="Group 1">
<MenuItem isDisabled icon={<FontAwesomeIcon icon={faKey} />}>
Secrets
</MenuItem>
<MenuItem icon={<FontAwesomeIcon icon={faUser} />}>Members</MenuItem>
</MenuGroup>
<MenuGroup title="Group 2">
<MenuItem icon={<FontAwesomeIcon icon={faKey} />}>Secrets</MenuItem>
<MenuItem icon={<FontAwesomeIcon icon={faKey} />}>Members</MenuItem>
</MenuGroup>
</Menu>
),
args: {}
};
export const WithDescription: Story = {
render: (args) => (
<Menu {...args}>
<MenuItem
isDisabled
icon={<FontAwesomeIcon icon={faKey} />}
description="Some random description"
>
Secrets
</MenuItem>
<MenuItem icon={<FontAwesomeIcon icon={faUser} />} description="Some random description">
Members
</MenuItem>
</Menu>
),
args: {}
};
+64
View File
@@ -0,0 +1,64 @@
import { ComponentPropsWithRef, ElementType, ReactNode, Ref } from 'react';
import { twMerge } from 'tailwind-merge';
export type MenuProps = {
children: ReactNode;
className?: string;
};
export const Menu = ({ children, className }: MenuProps): JSX.Element => {
return <ul className={twMerge('p-2', className)}>{children}</ul>;
};
export type MenuItemProps<T extends ElementType> = {
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
as?: T;
children: ReactNode;
icon?: ReactNode;
description?: ReactNode;
isDisabled?: boolean;
isSelected?: boolean;
className?: string;
inputRef?: Ref<T>;
};
export const MenuItem = <T extends ElementType = 'button'>({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = 'button',
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef
}: 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',
className
)}
>
<Item type="button" role="menuitem" class="flex items-center" ref={inputRef}>
{icon && <span className="mr-2">{icon}</span>}
<span className="flex-grow text-left">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}
</li>
);
MenuItem.displayName = 'MenuItem';
export type MenuGroupProps = {
children: ReactNode;
title: ReactNode;
};
export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => (
<>
<li className="p-2 text-xs text-gray-400">{title}</li>
{children}
</>
);
@@ -0,0 +1,2 @@
export type { MenuGroupProps, MenuItemProps, MenuProps } from './Menu';
export { Menu, MenuGroup, MenuItem } from './Menu';
@@ -0,0 +1,33 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { Meta, StoryObj } from '@storybook/react';
import { Button } from '../Button';
import { Modal, ModalContent, ModalContentProps, ModalTrigger } from './Modal';
const meta: Meta<typeof Modal> = {
title: 'Components/Modal',
component: Modal,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof ModalContent>;
const Template = (args: ModalContentProps) => (
<Modal>
<ModalTrigger asChild>
<Button>Open</Button>
</ModalTrigger>
<ModalContent {...args}>Hello world</ModalContent>
</Modal>
);
export const Basic: Story = {
render: (args) => <Template {...args} />,
args: {
title: 'Title',
subTitle: 'Something as subtitle',
footerContent: 'footer content'
}
};
@@ -0,0 +1,57 @@
import { forwardRef, ReactNode } from 'react';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { twMerge } from 'tailwind-merge';
import { Card, CardBody, CardFooter, CardTitle } from '../Card';
import { IconButton } from '../IconButton';
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
title?: ReactNode;
subTitle?: string;
footerContent?: ReactNode;
};
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
({ children, title, subTitle, className, footerContent, ...props }, forwardedRef) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className="fixed inset-0 w-full h-full animate-fadeIn"
style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}
/>
<DialogPrimitive.Content {...props} ref={forwardedRef}>
<Card
isRounded
className={twMerge(
'fixed max-w-md animate-popIn top-1/2 left-1/2 -translate-y-2/4 -translate-x-2/4',
className
)}
>
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
<CardBody>{children}</CardBody>
{footerContent && <CardFooter>{footerContent}</CardFooter>}
<DialogPrimitive.Close aria-label="Close" asChild>
<IconButton
variant="plain"
ariaLabel="close"
className="absolute top-2.5 right-2.5 text-white hover:bg-gray-600 rounded"
>
<FontAwesomeIcon icon={faTimes} size="sm" className="cursor-pointer" />
</IconButton>
</DialogPrimitive.Close>
</Card>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
)
);
ModalContent.displayName = 'ModalContent';
export type ModalProps = Omit<DialogPrimitive.DialogProps, 'open'> & { isOpen?: boolean };
export const Modal = ({ isOpen, ...props }: ModalProps) => (
<DialogPrimitive.Root open={isOpen} {...props} />
);
export const ModalTrigger = DialogPrimitive.Trigger;
export type ModalTriggerProps = DialogPrimitive.DialogTriggerProps;
@@ -0,0 +1,2 @@
export type { ModalContentProps, ModalProps, ModalTriggerProps } from './Modal';
export { Modal, ModalContent, ModalTrigger } from './Modal';
@@ -0,0 +1,113 @@
import { useEffect, useState } from 'react';
import { SelectProps } from '@radix-ui/react-select';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Meta, StoryObj } from '@storybook/react';
import { Select, SelectItem } from './Select';
const meta: Meta<typeof Select> = {
title: 'Components/Select',
component: Select,
tags: ['v2'],
argTypes: {
placeholder: {
defaultValue: 'Type something...'
}
}
};
export default meta;
type Story = StoryObj<typeof Select>;
export const Basic: Story = {
render: (args) => (
<div className="">
<Select placeholder="Type anything..." className="w-72" {...args}>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2">Peter</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
)
};
const Controlled = (args: SelectProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState('');
return (
<div className="">
<Select
defaultValue="1"
className="w-72"
open={isOpen}
onValueChange={(val) => setSelected(val)}
value={selected}
onOpenChange={(open) => setIsOpen(open)}
{...args}
>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2">Peter</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
);
};
export const Control: Story = {
render: (args) => <Controlled {...args} />
};
export const Disabled: Story = {
render: (args) => (
<div className="">
<Select defaultValue="1" className="w-72" {...args}>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2" isDisabled>
Peter
</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
)
};
export const Loading: Story = {
render: (args) => (
<div className="">
<Select defaultValue="1" className="w-72" isLoading {...args}>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2">Peter</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
)
};
const AsyncSelectOptions = () => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// eslint-disable-next-line no-new
new Promise<void>((resolve): void => {
setTimeout(() => {
setIsLoading(false);
resolve();
}, 1000);
});
}, []);
return (
<div className="">
<Select placeholder="Hello" className="w-72" isLoading={isLoading}>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2">Peter</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
);
};
export const Async: Story = {
render: (args) => <AsyncSelectOptions {...args} />
};
@@ -0,0 +1,96 @@
import { forwardRef, ReactNode } from 'react';
import { faCheck, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as SelectPrimitive from '@radix-ui/react-select';
import { twMerge } from 'tailwind-merge';
import { Spinner } from '../Spinner';
type Props = {
children: ReactNode;
placeholder?: string;
className?: string;
isLoading?: boolean;
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
({ children, placeholder, className, isLoading, ...props }, ref): JSX.Element => {
return (
<SelectPrimitive.Root {...props}>
<SelectPrimitive.Trigger
ref={ref}
className={twMerge(
`inline-flex items-center justify-between data-[placeholder]:text-gray-500
px-4 py-2.5 font-inter text-sm text-white rounded-md bg-mineshaft-800`,
className
)}
>
<SelectPrimitive.Value placeholder={placeholder} />
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon icon={faChevronDown} size="sm" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
position="popper"
sideOffset={5}
className="overflow-hidden text-white rounded-md shadow-md font-inter bg-mineshaft-800"
style={{ width: 'var(--radix-select-trigger-width)' }}
>
<SelectPrimitive.ScrollUpButton>
<FontAwesomeIcon icon={faChevronUp} size="sm" />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-2">
{isLoading ? (
<div className="flex items-center justify-center">
<Spinner size="xs" />
<span className="ml-2 text-xs text-gray-500">Loading...</span>
</div>
) : (
children
)}
</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton>
<FontAwesomeIcon icon={faChevronDown} size="sm" />
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
);
}
);
Select.displayName = 'Select';
export type SelectItemProps = Omit<SelectPrimitive.SelectItemProps, 'disabled'> & {
isDisabled?: boolean;
isSelected?: boolean;
};
export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
({ children, className, isSelected, isDisabled, ...props }, forwardedRef) => {
return (
<SelectPrimitive.Item
{...props}
className={twMerge(
`text-sm rounded-sm transition-all hover:text-primary
hover:bg-mineshaft-700 flex items-center pl-10 pr-4 py-2 cursor-pointer
select-none outline-none relative`,
isSelected && 'text-primary',
isDisabled && 'text-gray-600 hover:bg-transparent cursor-not-allowed hover:text-gray-600',
className
)}
ref={forwardedRef}
>
<SelectPrimitive.ItemIndicator className="absolute left-2">
<FontAwesomeIcon icon={faCheck} size="sm" />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
);
SelectItem.displayName = 'SelectItem';
@@ -0,0 +1,2 @@
export type { SelectItemProps, SelectProps } from './Select';
export { Select, SelectItem } from './Select';
@@ -0,0 +1,42 @@
import { twMerge } from 'tailwind-merge';
type SizeOptions = 'xs' | 'sm' | 'md' | 'lg';
type Props = {
className?: string;
size?: SizeOptions;
};
const sizeChart: Record<SizeOptions, string> = {
xs: 'w-4 h-4',
sm: 'w-6 h-6',
md: 'w-8 h-8',
lg: 'w-12 h-12'
};
export const Spinner = ({ className, size = 'md' }: Props): JSX.Element => {
return (
<div role="status">
<svg
aria-hidden="true"
className={twMerge(
' text-gray-200 animate-spin dark:text-gray-600 fill-primary m-1',
sizeChart[size],
className
)}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
);
};
@@ -0,0 +1 @@
export { Spinner } from './Spinner';
@@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Switch } from './Switch';
const meta: Meta<typeof Switch> = {
title: 'Components/Switch',
component: Switch,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Switch>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Simple: Story = {
args: {
children: 'Dark mode'
}
};
export const Disabled: Story = {
args: {
children: 'Dark mode',
isDisabled: true
}
};
export const Required: Story = {
args: {
children: 'Dark mode',
isRequired: true
}
};
@@ -0,0 +1,42 @@
import { ReactNode } from 'react';
import * as SwitchPrimitive from '@radix-ui/react-switch';
import { twMerge } from 'tailwind-merge';
export type SwitchProps = Omit<SwitchPrimitive.SwitchProps, 'checked' | 'disabled' | 'required'> & {
children: ReactNode;
id: string;
isChecked?: boolean;
isRequired?: boolean;
isDisabled?: boolean;
};
export const Switch = ({
children,
id,
className,
isChecked,
isDisabled,
isRequired,
...props
}: SwitchProps): JSX.Element => (
<div className="flex items-center text-bunker-300 font-inter">
<label className="text-sm" htmlFor={id}>
{children}
{isRequired && <span className="pl-1 text-red">*</span>}
</label>
<SwitchPrimitive.Root
{...props}
required={isRequired}
checked={isChecked}
disabled={isDisabled}
className={twMerge(
'h-5 ml-3 transition-all rounded-full w-9 bg-bunker-300 data-[state=checked]:bg-bunker-200',
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
className
)}
id={id}
>
<SwitchPrimitive.Thumb className="w-4 h-4 border-none will-change-transform rounded-full shadow bg-black block transition-all translate-x-0.5 data-[state=checked]:translate-x-[18px]" />
</SwitchPrimitive.Root>
</div>
);
@@ -0,0 +1,2 @@
export type { SwitchProps } from './Switch';
export { Switch } from './Switch';
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table';
const meta: Meta<typeof Table> = {
title: 'Components/Table',
component: Table,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Table>;
export const Basic: Story = {
render: (args) => (
<TableContainer>
<Table {...args}>
<THead>
<Tr>
<Th>Head#1</Th>
<Th>Head#2</Th>
<Th>Head#3</Th>
</Tr>
</THead>
<TBody>
<Tr>
<Td>Row#1</Td>
<Td>Row#2</Td>
<Td>Row#3</Td>
</Tr>
<Tr>
<Td>Row#1</Td>
<Td>Row#2</Td>
<Td>Row#3</Td>
</Tr>
</TBody>
</Table>
</TableContainer>
)
};
@@ -0,0 +1,93 @@
import { ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
export type TableContainerProps = {
children: ReactNode;
isRounded?: boolean;
className?: string;
};
export const TableContainer = ({
children,
className,
isRounded = true
}: TableContainerProps): JSX.Element => (
<div
className={twMerge(
'overflow-x-auto font-inter shadow-md relative border border-solid border-mineshaft-700',
isRounded && 'rounded-md',
className
)}
>
{children}
</div>
);
// main parent table
export type TableProps = {
className?: string;
children: ReactNode;
};
export const Table = ({ children, className }: TableProps): JSX.Element => (
<table
className={twMerge(
'bg-bunker-800 p-2 roun text-gray-300 w-full text-sm text-left rounded-md',
className
)}
>
{children}
</table>
);
// table head
export type THeadProps = {
children: ReactNode;
className?: string;
};
export const THead = ({ children, className }: THeadProps): JSX.Element => (
<thead className={twMerge('text-xs bg-bunker text-bunker-300 uppercase', className)}>
{children}
</thead>
);
// table rows
export type TrProps = {
children: ReactNode;
className?: string;
};
export const Tr = ({ children, className }: TrProps): JSX.Element => (
<tr className={twMerge('border border-solid border-mineshaft-700', className)}>{children}</tr>
);
// table head columns
export type ThProps = {
children: ReactNode;
className?: string;
};
export const Th = ({ children, className }: ThProps): JSX.Element => (
<th className={twMerge('px-6 py-3 font-medium', className)}>{children}</th>
);
// table body
export type TBodyProps = {
children: ReactNode;
className?: string;
};
export const TBody = ({ children, className }: TBodyProps): JSX.Element => (
<tbody className={twMerge(className)}>{children}</tbody>
);
// table body columns
export type TdProps = {
children: ReactNode;
className?: string;
};
export const Td = ({ children, className }: TdProps): JSX.Element => (
<td className={twMerge('text-left px-6 py-3', className)}>{children}</td>
);
@@ -0,0 +1,10 @@
export type {
TableContainerProps,
TableProps,
TBodyProps,
TdProps,
THeadProps,
ThProps,
TrProps
} from './Table';
export { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table';
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TextArea } from './TextArea';
const meta: Meta<typeof TextArea> = {
title: 'Components/TextArea',
component: TextArea,
tags: ['v2'],
argTypes: {
placeholder: {
defaultValue: 'Type anything',
type: 'string'
}
}
};
export default meta;
type Story = StoryObj<typeof TextArea>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Filled: Story = {
args: {}
};
export const Outline: Story = {
args: {
variant: 'outline'
}
};
export const Plain: Story = {
args: {
variant: 'plain'
}
};
export const Error: Story = {
args: {
isError: true
}
};
export const AutoWidth: Story = {
args: {
isFullWidth: false,
className: 'w-auto'
}
};
@@ -0,0 +1,74 @@
import { forwardRef, TextareaHTMLAttributes } from 'react';
import { cva, VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
type Props = {
placeholder?: string;
isFullWidth?: boolean;
isRequired?: boolean;
reSize?: 'none' | 'both' | 'vertical' | 'horizontal';
};
const textAreaVariants = cva(
'textarea w-full p-2 border border-solid text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50',
{
variants: {
size: {
xs: ['text-xs'],
sm: ['text-sm'],
md: ['text-md'],
lg: ['text-lg']
},
isRounded: {
true: ['rounded-md'],
false: ''
},
variant: {
filled: ['bg-bunker-800', 'text-gray-400'],
outline: ['bg-transparent'],
plain: 'bg-transparent outline-none'
},
isError: {
true: 'focus:ring-red/50 placeholder-red-300 border-red',
false: 'focus:ring-primary/50 border-mineshaft-400'
}
},
compoundVariants: [
{
variant: 'plain',
isError: [true, false],
className: 'border-none'
}
]
}
);
export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'size'> &
VariantProps<typeof textAreaVariants> &
Props;
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
(
{
className,
isRounded = true,
isError = false,
isRequired,
variant = 'filled',
size = 'md',
reSize = 'both',
...props
},
ref
): JSX.Element => (
<textarea
{...props}
style={{ resize: reSize }}
required={isRequired}
ref={ref}
className={twMerge(textAreaVariants({ className, isError, size, isRounded, variant }))}
/>
)
);
TextArea.displayName = 'TextArea';
@@ -0,0 +1,2 @@
export type { TextAreaProps } from './TextArea';
export { TextArea } from './TextArea';
+14
View File
@@ -0,0 +1,14 @@
export * from './Button';
export * from './Card';
export * from './Checkbox';
export * from './Dropdown';
export * from './FormControl';
export * from './IconButton';
export * from './Input';
export * from './Menu';
export * from './Modal';
export * from './Select';
export * from './Spinner';
export * from './Switch';
export * from './Table';
export * from './TextArea';
+22 -8
View File
@@ -1,5 +1,5 @@
/* eslint-disable no-nested-ternary */
import { Fragment, useCallback, useEffect, useState } from 'react';
import { UIEvent, useCallback, useEffect, useRef, useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import { useRouter } from 'next/router';
@@ -135,7 +135,14 @@ export default function Dashboard() {
name: '',
slug: ''
});
const [atSecretAreaTop, setAtSecretsAreaTop] = useState(true);
const secretsTop = useRef<HTMLDivElement>(null);
const onSecretsAreaScroll = (event: UIEvent<HTMLDivElement>) => {
if (event.currentTarget.scrollTop !== 0) {
setAtSecretsAreaTop(false);
}
};
// #TODO: fix save message for changing reroutes
// const beforeRouteHandler = (url) => {
// const warningText =
@@ -256,7 +263,6 @@ export default function Dashboard() {
const addRow = () => {
setIsNew(false);
setData([
...data!,
{
id: guidGenerator(),
idOverride: guidGenerator(),
@@ -265,8 +271,12 @@ export default function Dashboard() {
value: '',
valueOverride: undefined,
comment: ''
}
},
...data!
]);
if (!atSecretAreaTop) {
secretsTop.current?.scrollIntoView({ behavior: 'smooth' });
}
};
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
@@ -283,22 +293,22 @@ export default function Dashboard() {
};
const modifyValue = (value: string, pos: number) => {
setData((oldData) => oldData?.map((e, index) => (index === pos ? { ...e, value } : e)));
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
setButtonReady(true);
};
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
setData((oldData) => oldData?.map((e, index) => (index === pos ? { ...e, valueOverride } : e)));
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
setButtonReady(true);
};
const modifyKey = (key: string, pos: number) => {
setData((oldData) => oldData?.map((e, index) => (index === pos ? { ...e, key } : e)));
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
setButtonReady(true);
};
const modifyComment = (comment: string, pos: number) => {
setData((oldData) => oldData?.map((e, index) => (index === pos ? { ...e, comment } : e)));
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
setButtonReady(true);
};
@@ -736,7 +746,11 @@ export default function Dashboard() {
</div>
) : data?.length !== 0 ? (
<div className="flex flex-col w-full mt-1 mb-2">
<div className="max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
<div
onScroll={onSecretsAreaScroll}
className="max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar"
>
<div ref={secretsTop} />
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
{!snapshotData &&
data
+36 -39
View File
@@ -1,5 +1,4 @@
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faSlack } from '@fortawesome/free-brands-svg-icons';
@@ -40,47 +39,45 @@ const learningItem = ({
}: ItemProps): JSX.Element => {
if (link) {
return (
<Link href={link}>
<a
target={`${link.includes('https') ? '_blank' : '_self'}`}
rel="noopener noreferrer"
className="w-full"
href="#"
<a
target={`${link.includes('https') ? '_blank' : '_self'}`}
rel="noopener noreferrer"
className="w-full"
href={link}
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async () => {
if (userAction && userAction !== 'first_time_secrets_pushed') {
await registerUserAction({
action: userAction
});
}
}}
className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async () => {
if (userAction && userAction !== 'first_time_secrets_pushed') {
await registerUserAction({
action: userAction
});
}
}}
className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"
>
<div className="flex flex-row items-center mr-4">
<FontAwesomeIcon icon={icon} className="text-4xl mx-2 w-16" />
{complete && (
<div className="bg-bunker-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-green" />
</div>
)}
<div className="flex flex-col items-start">
<div className="text-xl font-semibold mt-0.5">{text}</div>
<div className="text-sm font-normal">{subText}</div>
<div className="flex flex-row items-center mr-4">
<FontAwesomeIcon icon={icon} className="text-4xl mx-2 w-16" />
{complete && (
<div className="bg-bunker-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-green" />
</div>
)}
<div className="flex flex-col items-start">
<div className="text-xl font-semibold mt-0.5">{text}</div>
<div className="text-sm font-normal">{subText}</div>
</div>
<div
className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-green'}`}
>
{complete ? 'Complete!' : `About ${time}`}
</div>
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
</a>
</Link>
<div
className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-green'}`}
>
{complete ? 'Complete!' : `About ${time}`}
</div>
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
</a>
);
}
return (
@@ -157,7 +154,7 @@ export default function Home() {
icon: faHandPeace,
time: '3 min',
userAction: 'intro_cta_clicked',
link: 'https://www.youtube.com/watch?v=JS3OKYU2078'
link: 'https://www.youtube.com/watch?v=3F7FNYX94zA'
})}
{learningItem({
text: 'Add your secrets',
-7
View File
@@ -68,13 +68,6 @@ export default function PasswordReset() {
});
if (!errorCheck) {
// const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
// text: privateKey,
// secret: newPassword
// .slice(0, 32)
// .padStart(32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), '0')
// }) as { ciphertext: string; iv: string; tag: string };
client.init(
{
username: email,
+22 -11
View File
@@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { useTranslation } from 'next-i18next';
import { plans as plansConstant } from 'public/data/frequentConstants';
import Plan from '@app/components/billing/Plan';
import NavHeader from '@app/components/navigation/NavHeader';
import { STRIPE_PRODUCT_PRO, STRIPE_PRODUCT_STARTER } from '@app/components/utilities/config';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription';
@@ -26,24 +26,34 @@ export default function SettingsBilling() {
subtext: t('billing:starter.subtext')!,
buttonTextMain: t('billing:downgrade')!,
buttonTextSecondary: t('billing:learn-more')!,
current: currentPlan === STRIPE_PRODUCT_STARTER
current: currentPlan === plansConstant.starter
},
{
key: 2,
name: t('billing:professional.name')!,
price: '$9',
name: 'Team',
price: '$7',
priceExplanation: t('billing:professional.price-explanation')!,
subtext: t('billing:professional.subtext')!,
text: t('billing:professional.text')!,
text: 'For teams that want to improve their efficiency and security.',
buttonTextMain: t('billing:upgrade')!,
buttonTextSecondary: t('billing:learn-more')!,
current: currentPlan === STRIPE_PRODUCT_PRO
current: currentPlan === plansConstant.team
},
{
key: 3,
name: t('billing:professional.name')!,
price: '$14',
priceExplanation: t('billing:professional.price-explanation')!,
text: t('billing:enterprise.text')!,
subtext: t('billing:professional.subtext')!,
buttonTextMain: t('billing:upgrade')!,
buttonTextSecondary: t('billing:learn-more')!,
current: currentPlan === plansConstant.professional
},
{
key: 4,
name: t('billing:enterprise.name')!,
price: t('billing:custom-pricing')!,
text: t('billing:enterprise.text')!,
text: 'Boost the security and efficiency of your engineering teams.',
buttonTextMain: t('billing:schedule-demo')!,
buttonTextSecondary: t('billing:learn-more')!,
current: false
@@ -57,7 +67,7 @@ export default function SettingsBilling() {
orgId
});
setCurrentPlan(subscriptions.data[0].plan.id);
setCurrentPlan(subscriptions.data[0].plan.product);
const orgUsers = await getOrganizationUsers({
orgId
});
@@ -80,9 +90,10 @@ export default function SettingsBilling() {
<p className="font-normal mr-4 text-gray-400 text-base">{t('billing:description')}</p>
</div>
</div>
<div className="flex flex-col ml-6 text-mineshaft-50">
<div className="flex flex-col ml-6 text-mineshaft-50 w-max">
<p className="text-xl font-semibold">{t('billing:subscription')}</p>
<div className="flex flex-row mt-4 overflow-x-auto">
<div className="mt-4 text-bunker-200 h-14 flex justify-center items-center rounded-md bg-bunker-600 mr-4"> If you are looking to get an annual plan, please reach out to <a className="ml-1.5 underline text-primary underline-offset-2" href="mailto:[email protected]">team@infisical.com</a></div>
<div className="grid grid-cols-2 grid-rows-2 gap-y-6 gap-x-3 mt-4 overflow-x-auto">
{plans.map((plan) => (
<Plan key={plan.name} plan={plan} />
))}
+9 -1
View File
@@ -5,10 +5,12 @@ import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faCheck, faMagnifyingGlass, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { plans } from 'public/data/frequentConstants';
import Button from '@app/components/basic/buttons/Button';
import AddIncidentContactDialog from '@app/components/basic/dialog/AddIncidentContactDialog';
import AddUserDialog from '@app/components/basic/dialog/AddUserDialog';
import UpgradePlanModal from '@app/components/basic/dialog/UpgradePlan';
import InputField from '@app/components/basic/InputField';
import UserTable from '@app/components/basic/table/UserTable';
import NavHeader from '@app/components/navigation/NavHeader';
@@ -29,6 +31,7 @@ import getWorkspaces from '../../api/workspace/getWorkspaces';
export default function SettingsOrg() {
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const host = window.location.origin;
const [orgName, setOrgName] = useState('');
const [emailUser, setEmailUser] = useState('');
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
@@ -212,7 +215,7 @@ export default function SettingsOrg() {
{t('section-members:org-members-description')}
</p>
<AddUserDialog
isOpen={isAddUserOpen}
isOpen={isAddUserOpen && (userList.length < 5 || currentPlan !== plans.starter || host !== 'https://app.infisical.com')}
closeModal={closeAddUserModal}
submitModal={submitAddUserModal}
email={emailUser}
@@ -220,6 +223,11 @@ export default function SettingsOrg() {
currentPlan={currentPlan}
orgName={orgName}
/>
<UpgradePlanModal
isOpen={isAddUserOpen && userList.length >= 5 && currentPlan === plans.starter && host === 'https://app.infisical.com'}
onClose={closeAddUserModal}
text="You can add more members if you switch to Infisical's Team plan."
/>
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
<div className="pb-1 w-full flex flex-row items-start max-w-6xl">
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex-row ">
+4
View File
@@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import '@fontsource/inter/400.css';
@import '@fontsource/inter/500.css';
@import '@fontsource/inter/700.css';