Skip to content

Commit

Permalink
Merge pull request #354 from akhilmhdh/feat/table-loader
Browse files Browse the repository at this point in the history
Table loading state and empty states
  • Loading branch information
vmatsiiako authored Feb 22, 2023
2 parents d02bc06 + 05a77e6 commit 75a2ab6
Show file tree
Hide file tree
Showing 20 changed files with 467 additions and 309 deletions.
2 changes: 1 addition & 1 deletion frontend/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export const parameters = {
}
},
darkMode: {
dark: { ...themes.dark, appContentBg: '#0e1014', appBg: '#0e1014' }
dark: { ...themes.dark, appContentBg: 'rgb(14,16,20)', appBg: 'rgb(14,16,20)' }
}
};
20 changes: 20 additions & 0 deletions frontend/src/components/v2/EmptyState/EmptyState.stories.tsx
Original file line number Diff line number Diff line change
@@ -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} />
};
21 changes: 21 additions & 0 deletions frontend/src/components/v2/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
1 change: 1 addition & 0 deletions frontend/src/components/v2/EmptyState/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EmptyState } from './EmptyState';
17 changes: 17 additions & 0 deletions frontend/src/components/v2/Skeleton/Skeleton.stories.tsx
Original file line number Diff line number Diff line change
@@ -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} />
};
12 changes: 12 additions & 0 deletions frontend/src/components/v2/Skeleton/Skeleton.tsx
Original file line number Diff line number Diff line change
@@ -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)} />
);
1 change: 1 addition & 0 deletions frontend/src/components/v2/Skeleton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Skeleton } from './Skeleton';
21 changes: 20 additions & 1 deletion frontend/src/components/v2/Table/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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> = {
title: 'Components/Table',
Expand Down Expand Up @@ -39,3 +39,22 @@ export const Basic: Story = {
</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>
)
};
33 changes: 30 additions & 3 deletions frontend/src/components/v2/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react';
import { twMerge } from 'tailwind-merge';

import { Skeleton } from '../Skeleton';

export type TableContainerProps = {
children: ReactNode;
isRounded?: boolean;
Expand Down Expand Up @@ -32,7 +34,7 @@ export type TableProps = {
export const Table = ({ children, className }: TableProps): JSX.Element => (
<table
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
)}
>
Expand All @@ -59,7 +61,10 @@ export type TrProps = {
} & HTMLAttributes<HTMLTableRowElement>;

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}
</tr>
);
Expand All @@ -71,7 +76,7 @@ export type ThProps = {
};

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
Expand All @@ -95,3 +100,25 @@ export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
{children}
</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>
))}
</>
);
2 changes: 1 addition & 1 deletion frontend/src/components/v2/Table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ export type {
ThProps,
TrProps
} from './Table';
export { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table';
export { Table, TableContainer, TableSkeleton,TBody, Td, Th, THead, Tr } from './Table';
2 changes: 2 additions & 0 deletions frontend/src/components/v2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ export * from './Card';
export * from './Checkbox';
export * from './DeleteActionModal';
export * from './Dropdown';
export * from './EmptyState';
export * from './FormControl';
export * from './IconButton';
export * from './Input';
export * from './Menu';
export * from './Modal';
export * from './Select';
export * from './Skeleton';
export * from './Spinner';
export * from './Switch';
export * from './Table';
Expand Down
19 changes: 10 additions & 9 deletions frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ export const OrgSettingsPage = () => {
const { createNotification } = useNotificationContext();

const orgId = currentOrg?._id || '';
const { data: orgUsers } = useGetOrgUsers(orgId);
const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId);
const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId);
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
useGetUserWorkspaceMemberships(orgId);
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
const { data: incidentContact } = useGetOrgIncidentContact(orgId);
const { data: incidentContact, isLoading: IsIncidentContactLoading } =
useGetOrgIncidentContact(orgId);

const renameOrg = useRenameOrg();
const removeUserOrgMembership = useDeleteOrgMembership();
Expand Down Expand Up @@ -197,9 +199,9 @@ export const OrgSettingsPage = () => {

/**
* 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 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 userWorkspaces = await getWorkspaces();
Expand Down Expand Up @@ -230,13 +232,11 @@ export const OrgSettingsPage = () => {
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
<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">
<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')}
</p>
<p className="mr-4 mt-2 mb-2 text-gray-400">
{t('section-members:org-members-description')}
</p>
<OrgMembersTable
isLoading={isOrgUserLoading || IsWsMembershipLoading}
isMoreUserNotAllowed={isMoreUsersNotAllowed}
orgName={currentOrg?.name || ''}
members={orgUsers}
Expand All @@ -261,6 +261,7 @@ export const OrgSettingsPage = () => {
</div>
<div className="w-full">
<OrgIncidentContactsTable
isLoading={IsIncidentContactLoading}
contacts={incidentContact}
onRemoveContact={onRemoveIncidentContact}
onAddContact={onAddIncidentContact}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
import { useState } from 'react';
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 { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

import {
Button,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from '@app/components/v2';
Tr} from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { IncidentContact } from '@app/hooks/api/types';

type Props = {
isLoading?: boolean;
contacts?: IncidentContact[];
onRemoveContact: (email: string) => Promise<void>;
onAddContact: (email: string) => Promise<void>;
Expand All @@ -39,7 +46,8 @@ type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
export const OrgIncidentContactsTable = ({
contacts = [],
onAddContact,
onRemoveContact
onRemoveContact,
isLoading
}: Props) => {
const [searchContact, setSearchContact] = useState('');
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
Expand All @@ -66,6 +74,10 @@ export const OrgIncidentContactsTable = ({
handlePopUpClose('removeContact');
};

const filteredContacts = contacts.filter(({ email }) =>
email.toLocaleLowerCase().includes(searchContact)
);

return (
<div className="w-full">
<div className="mb-4 flex">
Expand Down Expand Up @@ -96,28 +108,25 @@ export const OrgIncidentContactsTable = ({
</Tr>
</THead>
<TBody>
{contacts
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
?.map(({ email }) => (
<Tr key={email}>
<Td className="w-full">{email}</Td>
<Td className="mr-4">
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen('removeContact', { email })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
{filteredContacts?.map(({ email }) => (
<Tr key={email}>
<Td className="w-full">{email}</Td>
<Td className="mr-4">
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen('removeContact', { email })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
))}
</TBody>
</Table>
{contacts
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
?.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>
{filteredContacts?.length === 0 && !isLoading && (
<EmptyState title="No incident contacts found" icon={faContactBook} />
)}
</TableContainer>
</div>
Expand Down
Loading

0 comments on commit 75a2ab6

Please sign in to comment.