feat(domains): DomainCard and WebhookCard styling start (#225)

### TL;DR

Refactored the `DomainCard`, `EditDomainDialog`, and `WebhookCard` components to improve code readability and enhance UI using new shared components like `Tag`, `Heading`, `Button`, and `CustomIcon`.

### What changed?

- `DomainCard` component:
  - Replaced `Chip` with `Tag` component.
  - Used `Heading`, `Button`, and `CustomIcon` components.
  - Updated refresh icon to show `LoadingIcon` when checking.
- `EditDomainDialog` component:
  - Used `useToast` hook for toast messages.
- `WebhookCard` component:
  - Used `Input`, `Button`, and `CustomIcon` components for better UI.
- Added Storybook stories for the updated components.

### How to test?

1. Go to the project settings page.
2. Verify the `DomainCard` UI updates.
3. Edit a domain and check the toasts.
4. Verify the `WebhookCard` UI and functionality.
5. Run Storybook and inspect the added stories for the components.

### Why make this change?

To improve the consistency and user experience of the project settings UI, and to make the components more maintainable by using shared components.

---
This commit is contained in:
Vivian Phung 2024-06-24 19:22:20 -04:00 committed by GitHub
parent 1b038476c7
commit 9a1c0e8338
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 197 additions and 30 deletions

View File

@ -2,7 +2,6 @@ import { useState } from 'react';
import { Domain, DomainStatus, Project } from 'gql-client'; import { Domain, DomainStatus, Project } from 'gql-client';
import { import {
Chip,
Typography, Typography,
Menu, Menu,
MenuHandler, MenuHandler,
@ -15,6 +14,15 @@ import EditDomainDialog from './EditDomainDialog';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog'; import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { Tag } from 'components/shared/Tag';
import {
CheckIcon,
CrossIcon,
GearIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { Heading } from 'components/shared/Heading';
import { Button } from 'components/shared/Button';
enum RefreshStatus { enum RefreshStatus {
IDLE, IDLE,
@ -79,22 +87,29 @@ const DomainCard = ({
<> <>
<div className="flex justify-between py-3"> <div className="flex justify-between py-3">
<div className="flex justify-start gap-1"> <div className="flex justify-start gap-1">
<Typography variant="h6"> <Heading as="h6" className="flex-col">
<i>^</i> {domain.name} {domain.name}{' '}
</Typography> <Tag
<Chip type={
className="w-fit capitalize" domain.status === DomainStatus.Live ? 'positive' : 'negative'
value={domain.status} }
color={domain.status === DomainStatus.Live ? 'green' : 'orange'} leftIcon={
variant="ghost" domain.status === DomainStatus.Live ? (
icon={<i>^</i>} <CheckIcon />
/> ) : (
<CrossIcon />
)
}
>
{domain.status}
</Tag>
</Heading>
</div> </div>
<div className="flex justify-start gap-1"> <div className="flex justify-start gap-1">
<i <i
id="refresh" id="refresh"
className="cursor-pointer w-8 h-8" className="cursor-pointer"
onClick={() => { onClick={() => {
SetRefreshStatus(RefreshStatus.CHECKING); SetRefreshStatus(RefreshStatus.CHECKING);
setTimeout(() => { setTimeout(() => {
@ -102,11 +117,17 @@ const DomainCard = ({
}, CHECK_FAIL_TIMEOUT); }, CHECK_FAIL_TIMEOUT);
}} }}
> >
{refreshStatus === RefreshStatus.CHECKING ? 'L' : 'R'} {refreshStatus === RefreshStatus.CHECKING ? (
<LoadingIcon className="animate-spin" />
) : (
'L'
)}
</i> </i>
<Menu placement="bottom-end"> <Menu placement="bottom-end">
<MenuHandler> <MenuHandler>
<button className="border-2 rounded-full w-8 h-8">...</button> <Button iconOnly>
<GearIcon />
</Button>
</MenuHandler> </MenuHandler>
<MenuList> <MenuList>
<MenuItem <MenuItem
@ -143,13 +164,13 @@ const DomainCard = ({
{domain.status === DomainStatus.Pending && ( {domain.status === DomainStatus.Pending && (
<Card className="bg-slate-100 p-4 text-sm"> <Card className="bg-slate-100 p-4 text-sm">
{refreshStatus === RefreshStatus.IDLE ? ( {refreshStatus === RefreshStatus.IDLE ? (
<Typography variant="small"> <Heading>
^ Add these records to your domain and refresh to check ^ Add these records to your domain and refresh to check
</Typography> </Heading>
) : refreshStatus === RefreshStatus.CHECKING ? ( ) : refreshStatus === RefreshStatus.CHECKING ? (
<Typography variant="small" className="text-blue-500"> <Heading className="text-blue-500">
^ Checking records for {domain.name} ^ Checking records for {domain.name}
</Typography> </Heading>
) : ( ) : (
<div className="flex gap-2 text-red-500 mb-2"> <div className="flex gap-2 text-red-500 mb-2">
<div className="grow"> <div className="grow">

View File

@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { Controller, useForm, SubmitHandler } from 'react-hook-form'; import { Controller, useForm, SubmitHandler } from 'react-hook-form';
import toast from 'react-hot-toast';
import { Domain } from 'gql-client'; import { Domain } from 'gql-client';
import { import {
@ -9,10 +8,11 @@ import {
Option, Option,
} from '@snowballtools/material-tailwind-react-fork'; } from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from '../../../../context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { Modal } from 'components/shared/Modal'; import { Modal } from 'components/shared/Modal';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { Input } from 'components/shared/Input'; import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast';
const DEFAULT_REDIRECT_OPTIONS = ['none']; const DEFAULT_REDIRECT_OPTIONS = ['none'];
@ -40,6 +40,7 @@ const EditDomainDialog = ({
onUpdate, onUpdate,
}: EditDomainDialogProp) => { }: EditDomainDialogProp) => {
const client = useGQLClient(); const client = useGQLClient();
const { toast, dismiss } = useToast();
const getRedirectUrl = (domain: Domain) => { const getRedirectUrl = (domain: Domain) => {
const redirectDomain = domain.redirectTo; const redirectDomain = domain.redirectTo;
@ -99,10 +100,20 @@ const EditDomainDialog = ({
if (updateDomain) { if (updateDomain) {
await onUpdate(); await onUpdate();
toast.success(`Domain ${domain.name} has been updated`); toast({
id: 'domain_id_updated',
title: `Domain ${domain.name} has been updated`,
variant: 'success',
onDismiss: dismiss,
});
} else { } else {
reset(); reset();
toast.error(`Error updating domain ${domain.name}`); toast({
id: 'domain_id_error_update',
title: `Error updating domain ${domain.name}`,
variant: 'error',
onDismiss: dismiss,
});
} }
handleOpen(); handleOpen();

View File

@ -3,6 +3,8 @@ import { useState } from 'react';
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog'; import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { Input } from 'components/shared/Input';
import { CopyIcon, TrashIcon } from 'components/shared/CustomIcon';
interface WebhookCardProps { interface WebhookCardProps {
webhookUrl: string; webhookUrl: string;
@ -14,11 +16,12 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
return ( return (
<div className="flex justify-between w-full mb-3"> <div className="flex justify-between w-full mb-3 gap-3">
{webhookUrl} <Input value={webhookUrl} disabled />
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button
size="sm" iconOnly
size="md"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(webhookUrl); navigator.clipboard.writeText(webhookUrl);
toast({ toast({
@ -29,16 +32,17 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
}); });
}} }}
> >
Copy <CopyIcon />
</Button> </Button>
<Button <Button
size="sm" iconOnly
size="md"
variant="danger" variant="danger"
onClick={() => { onClick={() => {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
}} }}
> >
X <TrashIcon />
</Button> </Button>
</div> </div>
<DeleteWebhookDialog <DeleteWebhookDialog

View File

@ -11,6 +11,7 @@ import {
DomainStatus, DomainStatus,
Domain, Domain,
Environment, Environment,
Permission,
} from 'gql-client'; } from 'gql-client';
export const user: User = { export const user: User = {
@ -44,7 +45,7 @@ export const organization: Organization = {
export const member: ProjectMember = { export const member: ProjectMember = {
id: '1', id: '1',
member: user, member: user,
permissions: [], permissions: [Permission.Edit],
isPending: false, isPending: false,
createdAt: '2021-08-01T00:00:00.000Z', createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z', updatedAt: '2021-08-01T00:00:00.000Z',
@ -70,7 +71,7 @@ export const environmentVariable1: EnvironmentVariable = {
export const domain0: Domain = { export const domain0: Domain = {
id: '1', id: '1',
name: 'Domain', name: 'domain.com',
createdAt: '2021-08-01T00:00:00.000Z', createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z', updatedAt: '2021-08-01T00:00:00.000Z',
branch: 'Branch', branch: 'Branch',
@ -78,6 +79,16 @@ export const domain0: Domain = {
redirectTo: null, redirectTo: null,
}; };
export const domain1: Domain = {
id: '2',
name: 'www.domain.com',
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
branch: 'Branch',
status: DomainStatus.Live,
redirectTo: domain0,
};
export const deployment0: Deployment = { export const deployment0: Deployment = {
id: '1', id: '1',
url: 'https://deployment.com', url: 'https://deployment.com',

View File

@ -0,0 +1,48 @@
import { StoryObj, Meta } from '@storybook/react';
import DomainCard from 'components/projects/project/settings/DomainCard';
import { domain0, domain1, project } from '../../MockStoriesData';
const meta: Meta<typeof DomainCard> = {
title: 'Project/Settings/DomainCard',
component: DomainCard,
tags: ['autodocs'],
argTypes: {
domains: {
control: {
type: 'object',
},
},
domain: {
control: {
type: 'object',
},
},
branches: {
control: {
type: 'object',
},
},
project: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
};
export default meta;
type Story = StoryObj<typeof DomainCard>;
export const Default: Story = {
args: {
domains: [domain0, domain1],
domain: domain0,
branches: ['main'],
project: project,
},
};

View File

@ -0,0 +1,43 @@
import { StoryObj, Meta } from '@storybook/react';
import EditDomainDialog from 'components/projects/project/settings/EditDomainDialog';
const meta: Meta<typeof EditDomainDialog> = {
title: 'Components/EditDomainDialog',
component: EditDomainDialog,
tags: ['autodocs'],
argTypes: {
domains: {
control: {
type: 'object',
},
},
open: {
control: {
type: 'boolean',
},
},
handleOpen: {
action: 'open',
},
domain: {
control: {
type: 'object',
},
},
branches: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
};
export default meta;
type Story = StoryObj<typeof EditDomainDialog>;
export const Default: Story = {};

View File

@ -0,0 +1,29 @@
import { Meta, StoryObj } from '@storybook/react';
import WebhookCard from 'components/projects/project/settings/WebhookCard';
const meta: Meta<typeof WebhookCard> = {
title: 'Project/Settings/WebhookCard',
component: WebhookCard,
tags: ['autodocs'],
argTypes: {
webhookUrl: {
control: {
type: 'text',
},
},
onDelete: {
action: 'delete',
},
},
};
export default meta;
type Story = StoryObj<typeof WebhookCard>;
export const Default: Story = {
args: {
webhookUrl: 'https://api.retool.com',
},
};