Skip to content

Commit fac56ff

Browse files
committed
Merge branch 'development'
2 parents 1519c7d + 71559c9 commit fac56ff

27 files changed

+1012
-58
lines changed

src/components/project/ProjectOverview.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { DeploymentContextType, useDeploymentContext } from '@lib/contexts/deplo
55
import { InteractionContextType, useInteractionContext } from '@lib/contexts/interaction';
66
import { routePath } from '@lib/routes/route-paths';
77
import db from '@lib/storage/db';
8+
import ActionButton from '@shared/ActionButton';
89
import { BorderedCard } from '@shared/cards/BorderedCard';
9-
import DeeployButton from '@shared/deployment/DeeployButton';
1010
import SupportFooter from '@shared/SupportFooter';
1111
import { FormType, Job, ProjectPage, type Project } from '@typedefs/deployment';
1212
import { useEffect } from 'react';
@@ -81,23 +81,23 @@ export default function ProjectOverview({ project, jobs }: { project: Project; j
8181
<ProjectIdentity project={project} />
8282

8383
<div className="row gap-2">
84-
<DeeployButton
84+
<ActionButton
8585
className="slate-button"
8686
color="default"
8787
as={Link}
8888
to={`${routePath.deeploys}/${routePath.dashboard}?tab=drafts`}
8989
>
9090
<div className="text-sm font-medium">Cancel</div>
91-
</DeeployButton>
91+
</ActionButton>
9292

93-
<DeeployButton className="bg-red-500" color="danger" onPress={() => onDeleteProject()}>
93+
<ActionButton className="bg-red-500" color="danger" onPress={() => onDeleteProject()}>
9494
<div className="row gap-1.5">
9595
<RiDeleteBin2Line className="text-lg" />
9696
<div className="text-sm">Delete Draft</div>
9797
</div>
98-
</DeeployButton>
98+
</ActionButton>
9999

100-
<DeeployButton
100+
<ActionButton
101101
color="success"
102102
variant="solid"
103103
isDisabled={jobs?.length === 0}
@@ -109,7 +109,7 @@ export default function ProjectOverview({ project, jobs }: { project: Project; j
109109
<RiWalletLine className="text-lg" />
110110
<div className="text-sm font-medium">Payment</div>
111111
</div>
112-
</DeeployButton>
112+
</ActionButton>
113113
</div>
114114
</div>
115115

@@ -126,7 +126,7 @@ export default function ProjectOverview({ project, jobs }: { project: Project; j
126126

127127
<div className="row gap-2">
128128
{options.map((option) => (
129-
<DeeployButton
129+
<ActionButton
130130
key={option.id}
131131
className="slate-button"
132132
color="default"
@@ -140,7 +140,7 @@ export default function ProjectOverview({ project, jobs }: { project: Project; j
140140
<div className={`text-xl ${option.color}`}>{option.icon}</div>
141141
<div className="text-sm">{option.title}</div>
142142
</div>
143-
</DeeployButton>
143+
</ActionButton>
144144
))}
145145
</div>
146146
</div>

src/components/project/ProjectPayment.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DeploymentContextType, useDeploymentContext } from '@lib/contexts/deployment';
22
import { getJobsTotalCost } from '@lib/utils';
3+
import ActionButton from '@shared/ActionButton';
34
import { BorderedCard } from '@shared/cards/BorderedCard';
4-
import DeeployButton from '@shared/deployment/DeeployButton';
55
import EmptyData from '@shared/EmptyData';
66
import SupportFooter from '@shared/SupportFooter';
77
import { FormType, Job, ProjectPage, type Project } from '@typedefs/deployment';
@@ -27,7 +27,7 @@ export default function ProjectPayment({ project, jobs }: { project: Project; jo
2727
<ProjectIdentity project={project} />
2828

2929
<div className="row gap-2">
30-
<DeeployButton
30+
<ActionButton
3131
className="slate-button"
3232
color="default"
3333
onPress={() => {
@@ -38,9 +38,9 @@ export default function ProjectPayment({ project, jobs }: { project: Project; jo
3838
<RiArrowLeftLine className="text-lg" />
3939
<div className="text-sm font-medium">Project</div>
4040
</div>
41-
</DeeployButton>
41+
</ActionButton>
4242

43-
<DeeployButton
43+
<ActionButton
4444
color="primary"
4545
variant="solid"
4646
onPress={() => {
@@ -52,7 +52,7 @@ export default function ProjectPayment({ project, jobs }: { project: Project; jo
5252
<RiBox3Line className="text-lg" />
5353
<div className="text-sm">Pay & Deeploy</div>
5454
</div>
55-
</DeeployButton>
55+
</ActionButton>
5656
</div>
5757
</div>
5858

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Form } from '@heroui/form';
2+
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, useDisclosure } from '@heroui/modal';
3+
import { createTunnel, renameTunnel } from '@lib/api/tunnels';
4+
import Label from '@shared/Label';
5+
import StyledInput from '@shared/StyledInput';
6+
import SubmitButton from '@shared/SubmitButton';
7+
import { Tunnel } from '@typedefs/tunnels';
8+
import { forwardRef, useImperativeHandle, useState } from 'react';
9+
import { toast } from 'react-hot-toast';
10+
11+
interface Props {
12+
action: 'rename' | 'create';
13+
}
14+
15+
interface TunnelAliasModalRef {
16+
trigger: (callback: () => any, tunnel?: Tunnel) => void;
17+
}
18+
19+
const TunnelAliasModal = forwardRef<TunnelAliasModalRef, Props>(({ action }, ref) => {
20+
const [isLoading, setLoading] = useState<boolean>(false);
21+
const [alias, setAlias] = useState<string>('');
22+
23+
const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure();
24+
25+
const [tunnel, setTunnel] = useState<Tunnel>();
26+
const [callback, setCallback] = useState<(() => any) | null>(null);
27+
28+
const trigger = (callback: () => any, tunnel?: Tunnel) => {
29+
setLoading(false);
30+
setTunnel(tunnel);
31+
32+
if (tunnel) {
33+
setAlias(tunnel.alias);
34+
}
35+
36+
setCallback(() => callback);
37+
onOpen();
38+
};
39+
40+
useImperativeHandle(ref, () => ({
41+
trigger,
42+
}));
43+
44+
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
45+
e.preventDefault();
46+
47+
if (callback) {
48+
setLoading(true);
49+
50+
try {
51+
if (action === 'rename' && tunnel) {
52+
await renameTunnel(tunnel.id, alias.trim());
53+
} else {
54+
await createTunnel(alias.trim());
55+
}
56+
57+
toast.success(`Tunnel ${action === 'rename' ? 'renamed' : 'created'} successfully.`);
58+
callback();
59+
onClose();
60+
} catch (error) {
61+
console.error('Error renaming tunnel:', error);
62+
toast.error('Error renaming tunnel.');
63+
} finally {
64+
setLoading(false);
65+
}
66+
}
67+
};
68+
69+
const getModalTitle = () => {
70+
return action === 'create' ? 'Create Tunnel' : 'Rename Tunnel';
71+
};
72+
73+
const getButtonLabel = () => {
74+
return action === 'create' ? 'Create' : 'Rename';
75+
};
76+
77+
return (
78+
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="sm" shouldBlockScroll={false}>
79+
<Form className="w-full" validationBehavior="native" onSubmit={onSubmit}>
80+
<ModalContent>
81+
<ModalHeader>{getModalTitle()}</ModalHeader>
82+
83+
<ModalBody>
84+
<div className="col w-full gap-2">
85+
<Label value="Alias" />
86+
87+
<StyledInput
88+
autoFocus
89+
value={alias}
90+
onValueChange={(value) => setAlias(value)}
91+
validate={(value) => {
92+
const trimmedValue = value?.trim();
93+
94+
if (!trimmedValue) {
95+
return 'Alias is required';
96+
}
97+
98+
if (trimmedValue.length < 3) {
99+
return 'Alias must be at least 3 characters';
100+
}
101+
102+
return null;
103+
}}
104+
placeholder={action === 'create' ? 'My Tunnel' : 'Enter a new alias'}
105+
/>
106+
</div>
107+
</ModalBody>
108+
109+
<ModalFooter>
110+
<div className="flex justify-end pb-0.5">
111+
<SubmitButton label={getButtonLabel()} isLoading={isLoading} />
112+
</div>
113+
</ModalFooter>
114+
</ModalContent>
115+
</Form>
116+
</Modal>
117+
);
118+
});
119+
120+
TunnelAliasModal.displayName = 'TunnelAliasModal';
121+
122+
export default TunnelAliasModal;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { deleteTunnel } from '@lib/api/tunnels';
2+
import { InteractionContextType, useInteractionContext } from '@lib/contexts/interaction';
3+
import { TunnelsContextType, useTunnelsContext } from '@lib/contexts/tunnels';
4+
import { routePath } from '@lib/routes/route-paths';
5+
import { BorderedCard } from '@shared/cards/BorderedCard';
6+
import ContextMenuWithTrigger from '@shared/ContextMenuWithTrigger';
7+
import { SmallTag } from '@shared/SmallTag';
8+
import { Tunnel } from '@typedefs/tunnels';
9+
import toast from 'react-hot-toast';
10+
import { RiExternalLinkLine, RiLinkM } from 'react-icons/ri';
11+
import { Link, useNavigate } from 'react-router-dom';
12+
13+
export default function TunnelCard({ tunnel, fetchTunnels }: { tunnel: Tunnel; fetchTunnels: () => Promise<void> }) {
14+
const { openTunnelRenameModal, openTunnelTokenModal } = useTunnelsContext() as TunnelsContextType;
15+
const confirm = useInteractionContext() as InteractionContextType;
16+
17+
const navigate = useNavigate();
18+
19+
const onDeleteTunnel = async () => {
20+
try {
21+
await confirm(
22+
<div className="col gap-3">
23+
<div>Are you sure you want to delete the following tunnel?</div>
24+
<div className="font-medium">{tunnel.alias}</div>
25+
</div>,
26+
async () => {
27+
await deleteTunnel(tunnel.id);
28+
fetchTunnels();
29+
toast.success('Tunnel deleted successfully.');
30+
},
31+
);
32+
} catch (error) {
33+
console.error('Error deleting tunnel:', error);
34+
toast.error('Failed to delete tunnel.');
35+
}
36+
};
37+
38+
return (
39+
<div onClick={() => navigate(`${routePath.tunnels}/${tunnel.id}`)}>
40+
<BorderedCard isHoverable>
41+
<div className="row justify-between gap-3 bg-white lg:gap-6">
42+
<div className="col gap-1">
43+
<div className="font-medium">{tunnel.alias}</div>
44+
45+
<Link
46+
to={`https://${tunnel.url}`}
47+
target="_blank"
48+
onClick={(e) => e.stopPropagation()}
49+
className="cursor-pointer transition-all hover:opacity-60"
50+
>
51+
<div className="row gap-1 text-primary">
52+
<div className="font-robotoMono text-sm">{tunnel.url}</div>
53+
<RiExternalLinkLine className="mb-[1px] text-[17px]" />
54+
</div>
55+
</Link>
56+
</div>
57+
58+
<div className="row gap-3">
59+
{tunnel.custom_hostnames.length > 0 && (
60+
<SmallTag variant="green">
61+
<div className="row gap-0.5">
62+
<RiLinkM className="text-lg" />
63+
<div className="text-sm font-medium">Linked</div>
64+
</div>
65+
</SmallTag>
66+
)}
67+
68+
<ContextMenuWithTrigger
69+
items={[
70+
{
71+
key: 'rename',
72+
label: 'Rename',
73+
onPress: () => {
74+
openTunnelRenameModal(tunnel, () => fetchTunnels());
75+
},
76+
},
77+
{
78+
key: 'viewToken',
79+
label: 'View Token',
80+
onPress: () => {
81+
openTunnelTokenModal(tunnel.token as string, tunnel.alias);
82+
},
83+
},
84+
{
85+
key: 'delete',
86+
label: 'Delete',
87+
onPress: onDeleteTunnel,
88+
},
89+
]}
90+
/>
91+
</div>
92+
</div>
93+
</BorderedCard>
94+
</div>
95+
);
96+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Button } from '@heroui/button';
2+
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, useDisclosure } from '@heroui/modal';
3+
import { forwardRef, useImperativeHandle, useState } from 'react';
4+
5+
interface TunnelDNSModalRef {
6+
trigger: (hostname: string, url: string) => void;
7+
}
8+
9+
const TunnelDNSModal = forwardRef<TunnelDNSModalRef>((_, ref) => {
10+
const [hostname, setHostname] = useState<string>();
11+
const [url, setUrl] = useState<string>();
12+
13+
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
14+
15+
const trigger = (hostname: string, url: string) => {
16+
setHostname(hostname);
17+
setUrl(url);
18+
onOpen();
19+
};
20+
21+
useImperativeHandle(ref, () => ({
22+
trigger,
23+
}));
24+
25+
return (
26+
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="md" shouldBlockScroll={false}>
27+
<ModalContent>
28+
<ModalHeader>DNS Record</ModalHeader>
29+
30+
<ModalBody>
31+
<div className="col w-full gap-3 text-sm">
32+
<div>
33+
To link <span className="font-medium">{hostname}</span> to{' '}
34+
<span className="font-medium">{url}</span>, add the following DNS record:
35+
</div>
36+
37+
<div className="col gap-1 rounded bg-slate-100 p-3">
38+
<div>
39+
<span className="font-medium">Type:</span> CNAME
40+
</div>
41+
<div>
42+
<span className="font-medium">Host:</span> {hostname}
43+
</div>
44+
<div>
45+
<span className="font-medium">Value:</span> {url}
46+
</div>
47+
</div>
48+
49+
<div className="italic text-slate-500">
50+
After updating your DNS, it may take some time for the changes to propagate.
51+
</div>
52+
</div>
53+
</ModalBody>
54+
55+
<ModalFooter>
56+
<div className="flex justify-end pb-0.5">
57+
<Button className="slate-button" color="default" variant="flat" onPress={onClose}>
58+
<div className="text-sm font-medium">Close</div>
59+
</Button>
60+
</div>
61+
</ModalFooter>
62+
</ModalContent>
63+
</Modal>
64+
);
65+
});
66+
67+
TunnelDNSModal.displayName = 'TunnelDNSModal';
68+
69+
export default TunnelDNSModal;

0 commit comments

Comments
 (0)