Merge pull request #354 from akhilmhdh/feat/table-loader

Table loading state and empty states
This commit is contained in:
mv-turtle
2023-02-22 14:57:23 -08:00
committed by GitHub
20 changed files with 471 additions and 313 deletions

View File

@@ -10,6 +10,6 @@ export const parameters = {
} }
}, },
darkMode: { darkMode: {
dark: { ...themes.dark, appContentBg: '#0e1014', appBg: '#0e1014' } dark: { ...themes.dark, appContentBg: 'rgb(14,16,20)', appBg: 'rgb(14,16,20)' }
} }
}; };

View File

@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from '@storybook/react';
import { EmptyState } from './EmptyState';
const meta: Meta<typeof EmptyState> = {
title: 'Components/EmptyState',
component: EmptyState,
tags: ['v2'],
argTypes: {},
args: {
title: 'No members found'
}
};
export default meta;
type Story = StoryObj<typeof EmptyState>;
export const Basic: Story = {
render: (args) => <EmptyState {...args} />
};

View File

@@ -0,0 +1,21 @@
import { ReactNode } from 'react';
import { faCubesStacked, IconDefinition } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { twMerge } from 'tailwind-merge';
type Props = {
title: ReactNode;
className?: string;
children?: ReactNode;
icon?: IconDefinition;
};
export const EmptyState = ({ title, className, children, icon = faCubesStacked }: Props) => (
<div className={twMerge('flex w-full flex-col items-center px-2 pt-6 text-bunker-300', className)}>
<FontAwesomeIcon icon={icon} size="2x" className='mr-4' />
<div className='flex flex-row items-center py-4'>
<div className="text-bunker-300 text-sm">{title}</div>
<div>{children}</div>
</div>
</div>
);

View File

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

View File

@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Skeleton } from './Skeleton';
const meta: Meta<typeof Skeleton> = {
title: 'Components/Skeleton',
component: Skeleton,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Skeleton>;
export const Basic: Story = {
render: (args) => <Skeleton {...args} />
};

View File

@@ -0,0 +1,12 @@
import { twMerge } from 'tailwind-merge';
export type Props = {
className?: string;
};
// To show something is coming up
// Can be used with cards
// Tables etc
export const Skeleton = ({ className }: Props) => (
<div className={twMerge('h-6 w-full animate-pulse rounded-md bg-mineshaft-800', className)} />
);

View File

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

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table'; import { Table, TableContainer, TableSkeleton, TBody, Td, Th, THead, Tr } from './Table';
const meta: Meta<typeof Table> = { const meta: Meta<typeof Table> = {
title: 'Components/Table', title: 'Components/Table',
@@ -39,3 +39,22 @@ export const Basic: Story = {
</TableContainer> </TableContainer>
) )
}; };
export const Loading: Story = {
render: (args) => (
<TableContainer>
<Table {...args}>
<THead>
<Tr>
<Th>Head#1</Th>
<Th>Head#2</Th>
<Th>Head#3</Th>
</Tr>
</THead>
<TBody>
<TableSkeleton columns={3} key="story-book-table" />
</TBody>
</Table>
</TableContainer>
)
};

View File

@@ -1,6 +1,8 @@
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react'; import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { Skeleton } from '../Skeleton';
export type TableContainerProps = { export type TableContainerProps = {
children: ReactNode; children: ReactNode;
isRounded?: boolean; isRounded?: boolean;
@@ -32,7 +34,7 @@ export type TableProps = {
export const Table = ({ children, className }: TableProps): JSX.Element => ( export const Table = ({ children, className }: TableProps): JSX.Element => (
<table <table
className={twMerge( className={twMerge(
'w-full rounded rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300', 'w-full rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
className className
)} )}
> >
@@ -59,7 +61,10 @@ export type TrProps = {
} & HTMLAttributes<HTMLTableRowElement>; } & HTMLAttributes<HTMLTableRowElement>;
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => ( export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
<tr className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)} {...props}> <tr
className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)}
{...props}
>
{children} {children}
</tr> </tr>
); );
@@ -71,7 +76,7 @@ export type ThProps = {
}; };
export const Th = ({ children, className }: ThProps): JSX.Element => ( export const Th = ({ children, className }: ThProps): JSX.Element => (
<th className={twMerge('px-5 pt-4 pb-3.5 font-medium font-semibold bg-bunker-500', className)}>{children}</th> <th className={twMerge('bg-bunker-500 px-5 pt-4 pb-3.5 font-semibold', className)}>{children}</th>
); );
// table body // table body
@@ -95,3 +100,25 @@ export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
{children} {children}
</td> </td>
); );
export type TBodyLoader = {
rows?: number;
columns: number;
className?: string;
// unique key for mapping
key: string;
};
export const TableSkeleton = ({ rows = 3, columns, key, className }: TBodyLoader): JSX.Element => (
<>
{Array.apply(0, Array(rows)).map((_x, i) => (
<Tr key={`${key}-skeleton-rows-${i + 1}`}>
{Array.apply(0, Array(columns)).map((_y, j) => (
<Td key={`${key}-skeleton-rows-${i + 1}-column-${j + 1}`}>
<Skeleton className={className} />
</Td>
))}
</Tr>
))}
</>
);

View File

@@ -7,4 +7,4 @@ export type {
ThProps, ThProps,
TrProps TrProps
} from './Table'; } from './Table';
export { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table'; export { Table, TableContainer, TableSkeleton,TBody, Td, Th, THead, Tr } from './Table';

View File

@@ -3,12 +3,14 @@ export * from './Card';
export * from './Checkbox'; export * from './Checkbox';
export * from './DeleteActionModal'; export * from './DeleteActionModal';
export * from './Dropdown'; export * from './Dropdown';
export * from './EmptyState';
export * from './FormControl'; export * from './FormControl';
export * from './IconButton'; export * from './IconButton';
export * from './Input'; export * from './Input';
export * from './Menu'; export * from './Menu';
export * from './Modal'; export * from './Modal';
export * from './Select'; export * from './Select';
export * from './Skeleton';
export * from './Spinner'; export * from './Spinner';
export * from './Switch'; export * from './Switch';
export * from './Table'; export * from './Table';

View File

@@ -36,10 +36,12 @@ export const OrgSettingsPage = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const orgId = currentOrg?._id || ''; const orgId = currentOrg?._id || '';
const { data: orgUsers } = useGetOrgUsers(orgId); const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId);
const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId); const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
useGetUserWorkspaceMemberships(orgId);
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || ''); const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
const { data: incidentContact } = useGetOrgIncidentContact(orgId); const { data: incidentContact, isLoading: IsIncidentContactLoading } =
useGetOrgIncidentContact(orgId);
const renameOrg = useRenameOrg(); const renameOrg = useRenameOrg();
const removeUserOrgMembership = useDeleteOrgMembership(); const removeUserOrgMembership = useDeleteOrgMembership();
@@ -197,9 +199,9 @@ export const OrgSettingsPage = () => {
/** /**
* This function deleted a workspace. * This function deleted a workspace.
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete * It first checks if there is more than one workspace available. Otherwise, it doesn't delete
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete. * It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
* It then deletes the workspace and forwards the user to another aviable workspace. * It then deletes the workspace and forwards the user to another available workspace.
*/ */
// const executeDeletingWorkspace = async () => { // const executeDeletingWorkspace = async () => {
// const userWorkspaces = await getWorkspaces(); // const userWorkspaces = await getWorkspaces();
@@ -230,13 +232,11 @@ export const OrgSettingsPage = () => {
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50"> <div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
<OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} /> <OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} />
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6"> <div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
<p className="mr-4 text-xl font-semibold text-white"> <p className="mr-4 mb-4 text-xl font-semibold text-white">
{t('section-members:org-members')} {t('section-members:org-members')}
</p> </p>
<p className="mr-4 mt-2 mb-2 text-gray-400">
{t('section-members:org-members-description')}
</p>
<OrgMembersTable <OrgMembersTable
isLoading={isOrgUserLoading || IsWsMembershipLoading}
isMoreUserNotAllowed={isMoreUsersNotAllowed} isMoreUserNotAllowed={isMoreUsersNotAllowed}
orgName={currentOrg?.name || ''} orgName={currentOrg?.name || ''}
members={orgUsers} members={orgUsers}
@@ -261,6 +261,7 @@ export const OrgSettingsPage = () => {
</div> </div>
<div className="w-full"> <div className="w-full">
<OrgIncidentContactsTable <OrgIncidentContactsTable
isLoading={IsIncidentContactLoading}
contacts={incidentContact} contacts={incidentContact}
onRemoveContact={onRemoveIncidentContact} onRemoveContact={onRemoveIncidentContact}
onAddContact={onAddIncidentContact} onAddContact={onAddIncidentContact}

View File

@@ -1,6 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'; import {
faContactBook,
faMagnifyingGlass,
faPlus,
faTrash
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -8,6 +13,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -15,16 +21,17 @@ import {
ModalContent, ModalContent,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
THead, THead,
Tr Tr} from '@app/components/v2';
} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { IncidentContact } from '@app/hooks/api/types'; import { IncidentContact } from '@app/hooks/api/types';
type Props = { type Props = {
isLoading?: boolean;
contacts?: IncidentContact[]; contacts?: IncidentContact[];
onRemoveContact: (email: string) => Promise<void>; onRemoveContact: (email: string) => Promise<void>;
onAddContact: (email: string) => Promise<void>; onAddContact: (email: string) => Promise<void>;
@@ -39,7 +46,8 @@ type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
export const OrgIncidentContactsTable = ({ export const OrgIncidentContactsTable = ({
contacts = [], contacts = [],
onAddContact, onAddContact,
onRemoveContact onRemoveContact,
isLoading
}: Props) => { }: Props) => {
const [searchContact, setSearchContact] = useState(''); const [searchContact, setSearchContact] = useState('');
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -66,6 +74,10 @@ export const OrgIncidentContactsTable = ({
handlePopUpClose('removeContact'); handlePopUpClose('removeContact');
}; };
const filteredContacts = contacts.filter(({ email }) =>
email.toLocaleLowerCase().includes(searchContact)
);
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex"> <div className="mb-4 flex">
@@ -96,28 +108,25 @@ export const OrgIncidentContactsTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{contacts {isLoading && <TableSkeleton columns={2} key="incident-contact" />}
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact)) {filteredContacts?.map(({ email }) => (
?.map(({ email }) => ( <Tr key={email}>
<Tr key={email}> <Td className="w-full">{email}</Td>
<Td className="w-full">{email}</Td> <Td className="mr-4">
<Td className="mr-4"> <IconButton
<IconButton ariaLabel="delete"
ariaLabel="delete" colorSchema="danger"
colorSchema="danger" onClick={() => handlePopUpOpen('removeContact', { email })}
onClick={() => handlePopUpOpen('removeContact', { email })} >
> <FontAwesomeIcon icon={faTrash} />
<FontAwesomeIcon icon={faTrash} /> </IconButton>
</IconButton> </Td>
</Td> </Tr>
</Tr> ))}
))}
</TBody> </TBody>
</Table> </Table>
{contacts {filteredContacts?.length === 0 && !isLoading && (
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact)) <EmptyState title="No incident contacts found" icon={faContactBook} />
?.length === 0 && (
<div className='py-4 bg-bunker-800 text-sm text-center text-bunker-400 w-full mx-auto flex justify-center'>No incident contacts found</div>
)} )}
</TableContainer> </TableContainer>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'; import { faMagnifyingGlass, faPlus, faTrash, faUsers } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -8,6 +8,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -17,14 +18,14 @@ import {
SelectItem, SelectItem,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
Tag, Tag,
TBody, TBody,
Td, Td,
Th, Th,
THead, THead,
Tr, Tr,
UpgradePlanModal UpgradePlanModal} from '@app/components/v2';
} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { OrgUser, Workspace } from '@app/hooks/api/types'; import { OrgUser, Workspace } from '@app/hooks/api/types';
@@ -32,6 +33,7 @@ type Props = {
members?: OrgUser[]; members?: OrgUser[];
workspaceMemberships?: Record<string, Workspace[]>; workspaceMemberships?: Record<string, Workspace[]>;
orgName: string; orgName: string;
isLoading?: boolean;
isMoreUserNotAllowed: boolean; isMoreUserNotAllowed: boolean;
onRemoveMember: (userId: string) => Promise<void>; onRemoveMember: (userId: string) => Promise<void>;
onInviteMember: (email: string) => Promise<void>; onInviteMember: (email: string) => Promise<void>;
@@ -56,7 +58,8 @@ export const OrgMembersTable = ({
onInviteMember, onInviteMember,
onGrantAccess, onGrantAccess,
onRoleChange, onRoleChange,
userId userId,
isLoading
}: Props) => { }: Props) => {
const [searchMemberFilter, setSearchMemberFilter] = useState(''); const [searchMemberFilter, setSearchMemberFilter] = useState('');
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -72,8 +75,8 @@ export const OrgMembersTable = ({
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) }); } = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
const onAddMember = ({ email }: TAddMemberForm) => { const onAddMember = async ({ email }: TAddMemberForm) => {
onInviteMember(email); await onInviteMember(email);
handlePopUpClose('addMember'); handlePopUpClose('addMember');
reset(); reset();
}; };
@@ -140,73 +143,79 @@ export const OrgMembersTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => { {isLoading && <TableSkeleton columns={5} key="org-members" />}
const name = user ? `${user.firstName} ${user.lastName}` : '-'; {!isLoading &&
const email = user?.email || inviteEmail; filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
const userWs = workspaceMemberships?.[user?._id]; const name = user ? `${user.firstName} ${user.lastName}` : '-';
const email = user?.email || inviteEmail;
const userWs = workspaceMemberships?.[user?._id];
return ( return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full"> <Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td> <Td>{name}</Td>
<Td>{email}</Td> <Td>{email}</Td>
<Td> <Td>
{status === 'accepted' && ( {status === 'accepted' && (
<Select <Select
defaultValue={role} defaultValue={role}
isDisabled={userId === user?._id} isDisabled={userId === user?._id}
className="w-full bg-mineshaft-600" className="w-full bg-mineshaft-600"
onValueChange={(selectedRole) => onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole) onRoleChange(orgMembershipId, selectedRole)
} }
> >
{(isIamOwner || role === 'owner') && ( {(isIamOwner || role === 'owner') && (
<SelectItem value="owner">owner</SelectItem> <SelectItem value="owner">owner</SelectItem>
)} )}
<SelectItem value="admin">admin</SelectItem> <SelectItem value="admin">admin</SelectItem>
<SelectItem value="member">member</SelectItem> <SelectItem value="member">member</SelectItem>
</Select> </Select>
)} )}
{(status === 'invited' || status === 'verified') && ( {(status === 'invited' || status === 'verified') && (
<Button colorSchema="secondary" onClick={() => onInviteMember(email)}> <Button colorSchema="secondary" onClick={() => onInviteMember(email)}>
Resent Invite Resent Invite
</Button> </Button>
)} )}
{status === 'completed' && ( {status === 'completed' && (
<Button <Button
colorSchema="secondary" colorSchema="secondary"
onClick={() => onGrantAccess(user?._id, user?.publicKey)} onClick={() => onGrantAccess(user?._id, user?.publicKey)}
> >
Grant Access Grant Access
</Button> </Button>
)} )}
</Td> </Td>
<Td> <Td>
{userWs ? ( {userWs ? (
userWs?.map(({ name: wsName, _id }) => ( userWs?.map(({ name: wsName, _id }) => (
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1"> <Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
{wsName} {wsName}
</Tag> </Tag>
)) ))
) : ( ) : (
<Tag colorSchema="red">This user isn&apos;t part of any projects yet</Tag> <Tag colorSchema="red">This user isn&apos;t part of any projects yet</Tag>
)} )}
</Td> </Td>
<Td> <Td>
{userId !== user?._id && <IconButton {userId !== user?._id && (
ariaLabel="delete" <IconButton
colorSchema="danger" ariaLabel="delete"
isDisabled={userId === user?._id} colorSchema="danger"
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })} isDisabled={userId === user?._id}
> onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
<FontAwesomeIcon icon={faTrash} /> >
</IconButton>} <FontAwesomeIcon icon={faTrash} />
</Td> </IconButton>
</Tr> )}
); </Td>
})} </Tr>
);
})}
</TBody> </TBody>
</Table> </Table>
{filterdUser.length === 0 && <tr className='bg-bunker-800 text-sm py-4 text-center text-bunker-400 w-full mx-auto flex justify-center'><td className='col-span-5'>No project members found</td></tr>} {!isLoading && filterdUser?.length === 0 && (
<EmptyState title="No project members found" icon={faUsers} />
)}
</TableContainer> </TableContainer>
</div> </div>
<Modal <Modal

View File

@@ -45,11 +45,9 @@ import {
export const ProjectSettingsPage = () => { export const ProjectSettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentWorkspace, workspaces } = useWorkspace(); const { currentWorkspace, workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
const router = useRouter(); const router = useRouter();
const { data: serviceTokens } = useGetUserWsServiceTokens({
workspaceID: currentWorkspace?._id || ''
});
const workspaceID = currentWorkspace?._id || ''; const workspaceID = currentWorkspace?._id || '';
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
// delete action worksapce // delete action worksapce
@@ -66,12 +64,15 @@ export const ProjectSettingsPage = () => {
const deleteWsEnv = useDeleteWsEnvironment(); const deleteWsEnv = useDeleteWsEnvironment();
// service token // service token
const { data: serviceTokens, isLoading: isServiceTokenLoading } = useGetUserWsServiceTokens({
workspaceID: currentWorkspace?._id || ''
});
const { data: latestFileKey } = useGetUserWsKey(workspaceID); const { data: latestFileKey } = useGetUserWsKey(workspaceID);
const createServiceToken = useCreateServiceToken(); const createServiceToken = useCreateServiceToken();
const deleteServiceToken = useDeleteServiceToken(); const deleteServiceToken = useDeleteServiceToken();
// tag // tag
const { data: wsTags } = useGetWsTags(workspaceID); const { data: wsTags, isLoading: isTagLoading } = useGetWsTags(workspaceID);
const createWsTag = useCreateWsTag(); const createWsTag = useCreateWsTag();
const deleteWsTag = useDeleteWsTag(); const deleteWsTag = useDeleteWsTag();
@@ -300,7 +301,7 @@ export const ProjectSettingsPage = () => {
}; };
return ( return (
<div className="container mx-auto flex flex-col px-8 text-mineshaft-50 dark dark:[color-scheme:dark]"> <div className="dark container mx-auto flex flex-col px-8 text-mineshaft-50 dark:[color-scheme:dark]">
{/* TODO(akhilmhdh): Remove this right when layout is refactored */} {/* TODO(akhilmhdh): Remove this right when layout is refactored */}
<div className="relative right-5"> <div className="relative right-5">
<NavHeader pageName={t('settings-project:title')} isProjectRelated /> <NavHeader pageName={t('settings-project:title')} isProjectRelated />
@@ -319,6 +320,7 @@ export const ProjectSettingsPage = () => {
/> />
<CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} /> <CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} />
<EnvironmentSection <EnvironmentSection
isLoading={isWorkspaceLoading}
environments={currentWorkspace?.environments || []} environments={currentWorkspace?.environments || []}
onCreate={onCreateWsEnv} onCreate={onCreateWsEnv}
onDelete={onDeleteWsEnv} onDelete={onDeleteWsEnv}
@@ -326,6 +328,7 @@ export const ProjectSettingsPage = () => {
isEnvServiceAllowed={isEnvServiceAllowed} isEnvServiceAllowed={isEnvServiceAllowed}
/> />
<ServiceTokenSection <ServiceTokenSection
isLoading={isServiceTokenLoading}
tokens={serviceTokens || []} tokens={serviceTokens || []}
environments={currentWorkspace?.environments || []} environments={currentWorkspace?.environments || []}
onDeleteToken={onDeleteServiceToken} onDeleteToken={onDeleteServiceToken}
@@ -333,6 +336,7 @@ export const ProjectSettingsPage = () => {
onCreateToken={onCreateServiceToken} onCreateToken={onCreateServiceToken}
/> />
<SecretTagsSection <SecretTagsSection
isLoading={isTagLoading}
tags={wsTags || []} tags={wsTags || []}
onDeleteTag={onDeleteTag} onDeleteTag={onDeleteTag}
workspaceName={currentWorkspace?.name || ''} workspaceName={currentWorkspace?.name || ''}

View File

@@ -13,22 +13,18 @@ export const AutoCapitalizationSection = ({
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<form> <div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2"> <p className="mb-4 mt-2 text-xl font-semibold">{t('settings-project:auto-capitalization')}</p>
<p className="mb-4 mt-2 text-xl font-semibold"> <Checkbox
{t('settings-project:auto-capitalization')} className="data-[state=checked]:bg-primary"
</p> id="autoCapitalization"
<Checkbox isChecked={workspaceAutoCapitalization}
className="data-[state=checked]:bg-primary" onCheckedChange={(state) => {
id="autoCapitalization" onAutoCapitalizationChange(state as boolean);
isChecked={workspaceAutoCapitalization} }}
onCheckedChange={(state) => { >
onAutoCapitalizationChange(state as boolean); {t('settings-project:auto-capitalization-description')}
}} </Checkbox>
> </div>
{t('settings-project:auto-capitalization-description')}
</Checkbox>
</div>
</form>
); );
}; };

View File

@@ -7,6 +7,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -14,6 +15,7 @@ import {
ModalContent, ModalContent,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
@@ -25,6 +27,7 @@ import { usePopUp } from '@app/hooks/usePopUp';
type Props = { type Props = {
environments: Array<{ name: string; slug: string }>; environments: Array<{ name: string; slug: string }>;
isLoading?: boolean;
isEnvServiceAllowed: boolean; isEnvServiceAllowed: boolean;
onCreate: (data: CreateUpdateEnvFormData) => Promise<void>; onCreate: (data: CreateUpdateEnvFormData) => Promise<void>;
onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise<void>; onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise<void>;
@@ -43,6 +46,7 @@ export const EnvironmentSection = ({
isEnvServiceAllowed, isEnvServiceAllowed,
onCreate, onCreate,
onDelete, onDelete,
isLoading,
onUpdate onUpdate
}: Props): JSX.Element => { }: Props): JSX.Element => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -116,7 +120,8 @@ export const EnvironmentSection = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{environments?.length > 0 ? ( {isLoading && <TableSkeleton columns={3} key="project-envs" />}
{!isLoading &&
environments.map(({ name, slug }) => ( environments.map(({ name, slug }) => (
<Tr key={name}> <Tr key={name}>
<Td>{name}</Td> <Td>{name}</Td>
@@ -152,11 +157,11 @@ export const EnvironmentSection = ({
</IconButton> </IconButton>
</Td> </Td>
</Tr> </Tr>
)) ))}
) : ( {!isLoading && environments?.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="pt-7 pb-5 text-center text-bunker-400"> <Td colSpan={3}>
No environments found <EmptyState title="No environments found" />
</Td> </Td>
</Tr> </Tr>
)} )}

View File

@@ -1,5 +1,5 @@
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons'; import { faPlus, faTags, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -7,6 +7,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -16,23 +17,24 @@ import {
ModalTrigger, ModalTrigger,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
THead, THead,
Tr, Tr} from '@app/components/v2';
} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { WorkspaceTag } from '@app/hooks/api/types'; import { WorkspaceTag } from '@app/hooks/api/types';
const createTagSchema = yup.object({ const createTagSchema = yup.object({
name: yup.string().required().label('Tag Name'), name: yup.string().required().label('Tag Name')
}); });
export type CreateWsTag = yup.InferType<typeof createTagSchema>; export type CreateWsTag = yup.InferType<typeof createTagSchema>;
type Props = { type Props = {
tags: WorkspaceTag[]; tags: WorkspaceTag[];
isLoading?: boolean;
workspaceName: string; workspaceName: string;
onDeleteTag: (tagID: string) => Promise<void>; onDeleteTag: (tagID: string) => Promise<void>;
onCreateTag: (data: CreateWsTag) => Promise<string>; onCreateTag: (data: CreateWsTag) => Promise<string>;
@@ -42,6 +44,7 @@ type DeleteModalData = { name: string; id: string };
export const SecretTagsSection = ({ export const SecretTagsSection = ({
tags = [], tags = [],
isLoading,
onDeleteTag, onDeleteTag,
workspaceName, workspaceName,
onCreateTag onCreateTag
@@ -76,7 +79,10 @@ export const SecretTagsSection = ({
<div className="flex w-full flex-row justify-between"> <div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Secret Tags</p> <p className="mb-3 text-xl font-semibold">Secret Tags</p>
<p className="text-sm text-gray-400">Every secret can be assigned to one or more tags. Here you can add and remove tags for the current project.</p> <p className="text-sm text-gray-400">
Every secret can be assigned to one or more tags. Here you can add and remove tags for
the current project.
</p>
</div> </div>
<div> <div>
<Modal <Modal
@@ -92,8 +98,8 @@ export const SecretTagsSection = ({
</Button> </Button>
</ModalTrigger> </ModalTrigger>
<ModalContent <ModalContent
title={`Add a tag for ${ workspaceName}`} title={`Add a tag for ${workspaceName}`}
subTitle='Specify your tag name, and the slug will be created automatically.' subTitle="Specify your tag name, and the slug will be created automatically."
> >
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
@@ -102,7 +108,7 @@ export const SecretTagsSection = ({
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label='Tag Name' label="Tag Name"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
@@ -130,7 +136,7 @@ export const SecretTagsSection = ({
</Modal> </Modal>
</div> </div>
</div> </div>
<TableContainer className='mt-4'> <TableContainer className="mt-4">
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
@@ -140,7 +146,8 @@ export const SecretTagsSection = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{tags?.length > 0 ? ( {isLoading && <TableSkeleton columns={3} key="secret-tags" />}
{!isLoading &&
tags.map(({ _id, name, slug }) => ( tags.map(({ _id, name, slug }) => (
<Tr key={name}> <Tr key={name}>
<Td>{name}</Td> <Td>{name}</Td>
@@ -149,7 +156,7 @@ export const SecretTagsSection = ({
<IconButton <IconButton
onClick={() => onClick={() =>
handlePopUpOpen('deleteTagConfirmation', { handlePopUpOpen('deleteTagConfirmation', {
name, name,
id: _id id: _id
}) })
} }
@@ -160,11 +167,11 @@ export const SecretTagsSection = ({
</IconButton> </IconButton>
</Td> </Td>
</Tr> </Tr>
)) ))}
) : ( {!isLoading && tags?.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="py-6 text-center text-bunker-400"> <Td colSpan={3}>
No tags found for this project <EmptyState title="No secret tags found" icon={faTags} />
</Td> </Td>
</Tr> </Tr>
)} )}

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { faCheck, faCopy, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faCopy, faKey, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -9,6 +9,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -20,6 +21,7 @@ import {
SelectItem, SelectItem,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
@@ -47,6 +49,7 @@ export type CreateServiceToken = yup.InferType<typeof createServiceTokenSchema>;
type Props = { type Props = {
tokens: ServiceToken[]; tokens: ServiceToken[];
isLoading?: boolean;
workspaceName: string; workspaceName: string;
environments: WorkspaceEnv[]; environments: WorkspaceEnv[];
onDeleteToken: (serviceTokenID: string) => Promise<void>; onDeleteToken: (serviceTokenID: string) => Promise<void>;
@@ -57,6 +60,7 @@ type DeleteModalData = { name: string; id: string };
export const ServiceTokenSection = ({ export const ServiceTokenSection = ({
tokens = [], tokens = [],
isLoading,
onDeleteToken, onDeleteToken,
workspaceName, workspaceName,
environments = [], environments = [],
@@ -252,7 +256,7 @@ export const ServiceTokenSection = ({
isOpen={popUp.deleteAPITokenConfirmation.isOpen} isOpen={popUp.deleteAPITokenConfirmation.isOpen}
title={`Delete ${ title={`Delete ${
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || ' ' (popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || ' '
} api key?`} } service token?`}
onChange={(isOpen) => handlePopUpToggle('deleteAPITokenConfirmation', isOpen)} onChange={(isOpen) => handlePopUpToggle('deleteAPITokenConfirmation', isOpen)}
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name} deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose('deleteAPITokenConfirmation')} onClose={() => handlePopUpClose('deleteAPITokenConfirmation')}
@@ -269,7 +273,8 @@ export const ServiceTokenSection = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{tokens?.length > 0 ? ( {isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
{!isLoading &&
tokens.map((row) => ( tokens.map((row) => (
<Tr key={row._id}> <Tr key={row._id}>
<Td>{row.name}</Td> <Td>{row.name}</Td>
@@ -290,11 +295,11 @@ export const ServiceTokenSection = ({
</IconButton> </IconButton>
</Td> </Td>
</Tr> </Tr>
)) ))}
) : ( {!isLoading && tokens?.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="py-6 text-center text-bunker-400"> <Td colSpan={4} className="py-6 text-center text-bunker-400">
No service tokens found <EmptyState title="No service tokens found" icon={faKey} />
</Td> </Td>
</Tr> </Tr>
)} )}

View File

@@ -1340,163 +1340,165 @@ module.exports = {
900: '#176437', 900: '#176437',
DEFAULT: '#2ecc71' DEFAULT: '#2ecc71'
} }
},
keyframes: {
type: {
'0%': { transform: 'translateX(0ch)' },
'5%, 10%': { transform: 'translateX(1ch)' },
'15%, 20%': { transform: 'translateX(2ch)' },
'25%, 30%': { transform: 'translateX(3ch)' },
'35%, 40%': { transform: 'translateX(4ch)' },
'45%, 50%': { transform: 'translateX(5ch)' },
'55%, 60%': { transform: 'translateX(6ch)' },
'65%, 70%': { transform: 'translateX(7ch)' },
'75%, 80%': { transform: 'translateX(8ch)' },
'85%, 90%': { transform: 'translateX(9ch)' },
'95%, 100%': { transform: 'translateX(11ch)' }
},
// REQUIRED BY DESIGN COMPONENT
// MODAL
fadeIn: {
'0%': { opacity: 0 },
'100%': { opacity: 1 }
},
popIn: {
from: {
opacity: 0,
transform: 'translate(-50%, -48%) scale(0.96)'
},
to: {
opacity: 1,
transform: 'translate(-50%, -50%) scale(1)'
}
},
// Dropdown
slideUpAndFade: {
from: {
opacity: 0,
transform: ' translateY(2px)'
},
to: {
opacity: 1,
transform: ' translateY(0)'
}
},
slideRightAndFade: {
from: {
opacity: 0,
transform: ' translateX(-2px)'
},
to: {
opacity: 1,
transform: ' translateX(0)'
}
},
slideDownAndFade: {
from: {
opacity: 0,
transform: ' translateY(-2px)'
},
to: {
opacity: 1,
transform: ' translateY(0)'
}
},
slideLeftAndFade: {
from: {
opacity: 0,
transform: ' translateX(2px)'
},
to: {
opacity: 1,
transform: ' translateX(0)'
}
},
// END
spin: {
'0%': { transform: 'rotate(0deg)' },
'40%': { transform: 'rotate(360deg)' },
'100%': { transform: 'rotate(360deg)' }
},
bounce: {
'0%': { transform: 'translateY(-90%)' },
'100%': { transform: 'translateY(-100%)' }
},
wiggle: {
'0%, 100%': { transform: 'rotate(-3deg)' },
'50%': { transform: 'rotate(3deg)' }
},
ping: {
'75%, 100%': {
transform: 'scale(2)',
opacity: 0
}
},
popup: {
'0%': {
transform: 'scale(0.2)',
opacity: 0
// transform: "translateY(120%)",
},
'100%': {
transform: 'scale(1)',
opacity: 1
// transform: "translateY(100%)",
}
},
popright: {
'0%': {
transform: 'translateX(-100%)'
},
'100%': {
transform: 'translateX(0%)'
}
},
popleft: {
'0%': {
transform: 'translateX(100%)'
},
'100%': {
transform: 'translateX(0%)'
}
},
popdown: {
'0%': {
transform: 'scale(0.2)',
opacity: 0
// transform: "translateY(80%)",
},
'100%': {
transform: 'scale(1)',
opacity: 1
// transform: "translateY(100%)",
}
}
},
animation: {
// Design Lib
// MODAL
fadeIn: 'fadeIn 100ms cubic-bezier(0.16, 1, 0.3, 1)',
popIn: 'popIn 150ms cubic-bezier(0.16, 1, 0.3, 1);',
// Dropdown
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
// END
// TODO:(akhilmhdh) remove all these unused and keep the config file as small as possible
// Make the whole color pallelte into simpler
bounce: 'bounce 1000ms ease-in-out infinite',
spin: 'spin 4000ms ease-in-out infinite',
cursor: 'cursor .6s linear infinite alternate',
type: 'type 2.7s ease-out .8s infinite alternate both',
'type-reverse': 'type 1.8s ease-out 0s infinite alternate-reverse both',
wiggle: 'wiggle 200ms ease-in-out',
ping: 'ping 1000ms ease-in-out infinite',
popup: 'popup 300ms ease-in-out',
popdown: 'popdown 300ms ease-in-out',
popright: 'popright 100ms ease-in-out',
popleft: 'popleft 100ms ease-in-out'
} }
}, },
keyframes: {
type: {
'0%': { transform: 'translateX(0ch)' },
'5%, 10%': { transform: 'translateX(1ch)' },
'15%, 20%': { transform: 'translateX(2ch)' },
'25%, 30%': { transform: 'translateX(3ch)' },
'35%, 40%': { transform: 'translateX(4ch)' },
'45%, 50%': { transform: 'translateX(5ch)' },
'55%, 60%': { transform: 'translateX(6ch)' },
'65%, 70%': { transform: 'translateX(7ch)' },
'75%, 80%': { transform: 'translateX(8ch)' },
'85%, 90%': { transform: 'translateX(9ch)' },
'95%, 100%': { transform: 'translateX(11ch)' }
},
// REQUIRED BY DEISGN COMPONENT
// MODAL
fadeIn: {
'0%': { opacity: 0 },
'100%': { opacity: 1 }
},
popIn: {
from: {
opacity: 0,
transform: 'translate(-50%, -48%) scale(0.96)'
},
to: {
opacity: 1,
transform: 'translate(-50%, -50%) scale(1)'
}
},
// Dropdown
slideUpAndFade: {
from: {
opacity: 0,
transform: ' translateY(2px)'
},
to: {
opacity: 1,
transform: ' translateY(0)'
}
},
slideRightAndFade: {
from: {
opacity: 0,
transform: ' translateX(-2px)'
},
to: {
opacity: 1,
transform: ' translateX(0)'
}
},
slideDownAndFade: {
from: {
opacity: 0,
transform: ' translateY(-2px)'
},
to: {
opacity: 1,
transform: ' translateY(0)'
}
},
slideLeftAndFade: {
from: {
opacity: 0,
transform: ' translateX(2px)'
},
to: {
opacity: 1,
transform: ' translateX(0)'
}
},
// END
spin: {
'0%': { transform: 'rotate(0deg)' },
'40%': { transform: 'rotate(360deg)' },
'100%': { transform: 'rotate(360deg)' }
},
bounce: {
'0%': { transform: 'translateY(-90%)' },
'100%': { transform: 'translateY(-100%)' }
},
wiggle: {
'0%, 100%': { transform: 'rotate(-3deg)' },
'50%': { transform: 'rotate(3deg)' }
},
ping: {
'75%, 100%': {
transform: 'scale(2)',
opacity: 0
}
},
popup: {
'0%': {
transform: 'scale(0.2)',
opacity: 0
// transform: "translateY(120%)",
},
'100%': {
transform: 'scale(1)',
opacity: 1
// transform: "translateY(100%)",
}
},
popright: {
'0%': {
transform: 'translateX(-100%)'
},
'100%': {
transform: 'translateX(0%)'
}
},
popleft: {
'0%': {
transform: 'translateX(100%)'
},
'100%': {
transform: 'translateX(0%)'
}
},
popdown: {
'0%': {
transform: 'scale(0.2)',
opacity: 0
// transform: "translateY(80%)",
},
'100%': {
transform: 'scale(1)',
opacity: 1
// transform: "translateY(100%)",
}
}
},
animation: {
// Design Lib
// MODAL
fadeIn: 'fadeIn 100ms cubic-bezier(0.16, 1, 0.3, 1)',
popIn: 'popIn 150ms cubic-bezier(0.16, 1, 0.3, 1);',
// Dropdown
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
// END
bounce: 'bounce 1000ms ease-in-out infinite',
spin: 'spin 4000ms ease-in-out infinite',
cursor: 'cursor .6s linear infinite alternate',
type: 'type 2.7s ease-out .8s infinite alternate both',
'type-reverse': 'type 1.8s ease-out 0s infinite alternate-reverse both',
wiggle: 'wiggle 200ms ease-in-out',
ping: 'ping 1000ms ease-in-out infinite',
popup: 'popup 300ms ease-in-out',
popdown: 'popdown 300ms ease-in-out',
popright: 'popright 100ms ease-in-out',
popleft: 'popleft 100ms ease-in-out'
},
fontSize: { fontSize: {
xxxs: '.23rem', xxxs: '.23rem',
xxs: '.5rem', xxs: '.5rem',