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:
parent
1b038476c7
commit
9a1c0e8338
@ -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">
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
@ -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 = {};
|
@ -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',
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user