forked from cerc-io/snowballtools-base
Merge branch 'main' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4917-project-deployments-layout-and-empty-state
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
@@ -8,6 +8,7 @@
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
|
||||
@@ -18,14 +18,14 @@ export default [
|
||||
name: 'Kotlin',
|
||||
icon: 'kotlin',
|
||||
repoFullName: '',
|
||||
isComingSoon: false,
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'React Native',
|
||||
icon: 'react-native',
|
||||
repoFullName: '',
|
||||
isComingSoon: false,
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { Duration } from 'luxon';
|
||||
import React from 'react';
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
const FormatMillisecond = ({ time }: { time: number }) => {
|
||||
export interface FormatMilliSecondProps
|
||||
extends ComponentPropsWithoutRef<'div'> {
|
||||
time: number;
|
||||
}
|
||||
|
||||
const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => {
|
||||
const formatTime = Duration.fromMillis(time)
|
||||
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
||||
.toObject();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
{...props}
|
||||
className={cn('text-sm text-elements-mid-em', props?.className)}
|
||||
>
|
||||
{formatTime.days !== 0 && <span>{formatTime.days}d </span>}
|
||||
{formatTime.hours !== 0 && <span>{formatTime.hours}h </span>}
|
||||
{formatTime.minutes !== 0 && <span>{formatTime.minutes}m </span>}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useStopwatch } from 'react-timer-hook';
|
||||
|
||||
import FormatMillisecond from './FormatMilliSecond';
|
||||
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||
|
||||
const setStopWatchOffset = (time: string) => {
|
||||
const providedTime = new Date(time);
|
||||
@@ -11,13 +11,17 @@ const setStopWatchOffset = (time: string) => {
|
||||
return currentTime;
|
||||
};
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp }: { offsetTimestamp: Date }) => {
|
||||
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||
offsetTimestamp: Date;
|
||||
}
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
||||
const { totalSeconds } = useStopwatch({
|
||||
autoStart: true,
|
||||
offsetTimestamp: offsetTimestamp,
|
||||
});
|
||||
|
||||
return <FormatMillisecond time={totalSeconds * 1000} />;
|
||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||
};
|
||||
|
||||
export { Stopwatch, setStopWatchOffset };
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
import { Typography } from '@material-tailwind/react';
|
||||
|
||||
import { DeployStep, DeployStatus } from './DeployStep';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import ConfirmDialog from 'components/shared/ConfirmDialog';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { ClockOutlineIcon, WarningIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const TIMEOUT_DURATION = 5000;
|
||||
const Deploy = () => {
|
||||
@@ -31,27 +34,27 @@ const Deploy = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between mb-6">
|
||||
<div>
|
||||
<h4>Deployment started ...</h4>
|
||||
<div className="flex">
|
||||
^
|
||||
<div className="space-y-7">
|
||||
<div className="flex justify-between">
|
||||
<div className="space-y-1.5">
|
||||
<Heading as="h4" className="md:text-lg font-medium">
|
||||
Deployment started ...
|
||||
</Heading>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||
<Stopwatch
|
||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
leftIcon={<WarningIcon size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Cancel deployment?"
|
||||
handleOpen={handleOpen}
|
||||
@@ -66,28 +69,31 @@ const Deploy = () => {
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
<DeployStep
|
||||
title="Building"
|
||||
status={DeployStatus.COMPLETE}
|
||||
step="1"
|
||||
processTime="72000"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Deployment summary"
|
||||
status={DeployStatus.PROCESSING}
|
||||
step="2"
|
||||
startTime={Date.now().toString()}
|
||||
/>
|
||||
<DeployStep
|
||||
title="Running checks"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="3"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Assigning domains"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="4"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<DeployStep
|
||||
title="Building"
|
||||
status={DeployStatus.COMPLETE}
|
||||
step="1"
|
||||
processTime="72000"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Deployment summary"
|
||||
status={DeployStatus.PROCESSING}
|
||||
step="2"
|
||||
startTime={Date.now().toString()}
|
||||
/>
|
||||
<DeployStep
|
||||
title="Running checks"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="3"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Assigning domains"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Collapse, Button, Typography } from '@material-tailwind/react';
|
||||
import { Collapse } from '@material-tailwind/react';
|
||||
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import FormatMillisecond from '../../FormatMilliSecond';
|
||||
import processLogs from '../../../assets/process-logs.json';
|
||||
import { cn } from 'utils/classnames';
|
||||
import {
|
||||
CheckRoundFilledIcon,
|
||||
ClockOutlineIcon,
|
||||
CopyIcon,
|
||||
LoaderIcon,
|
||||
MinusCircleIcon,
|
||||
PlusIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { useIntersectionObserver } from 'usehooks-ts';
|
||||
|
||||
enum DeployStatus {
|
||||
PROCESSING = 'progress',
|
||||
@@ -28,61 +39,110 @@ const DeployStep = ({
|
||||
startTime,
|
||||
processTime,
|
||||
}: DeployStepsProps) => {
|
||||
const [collapse, setCollapse] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { toast, dismiss } = useToast();
|
||||
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
|
||||
threshold: 1,
|
||||
});
|
||||
|
||||
const disableCollapse = status !== DeployStatus.COMPLETE;
|
||||
|
||||
return (
|
||||
<div className="border-b-2">
|
||||
<div className="flex justify-between p-2 gap-2">
|
||||
{status === DeployStatus.NOT_STARTED && <div>{step}</div>}
|
||||
{status === DeployStatus.PROCESSING && <div>O</div>}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCollapse(!collapse);
|
||||
}}
|
||||
>
|
||||
{collapse ? '-' : '+'}
|
||||
</button>
|
||||
<div className="border-b border-border-separator">
|
||||
{/* Collapisble trigger */}
|
||||
<button
|
||||
className={cn(
|
||||
'flex justify-between w-full py-5 gap-2',
|
||||
disableCollapse && 'cursor-auto',
|
||||
)}
|
||||
tabIndex={disableCollapse ? -1 : undefined}
|
||||
onClick={() => {
|
||||
if (!disableCollapse) {
|
||||
setIsOpen((val) => !val);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={cn('grow flex items-center gap-3')}>
|
||||
{/* Icon */}
|
||||
<div className="w-6 h-6 grid place-content-center">
|
||||
{status === DeployStatus.NOT_STARTED && (
|
||||
<div className="grid place-content-center w-6 h-6 rounded-[48px] bg-base-bg-emphasized font-mono text-xs">
|
||||
{step}
|
||||
</div>
|
||||
)}
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<LoaderIcon className="animate-spin text-elements-link" />
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<div className="text-controls-primary">
|
||||
{!isOpen && <PlusIcon size={24} />}
|
||||
{isOpen && <MinusCircleIcon size={24} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<span
|
||||
className={cn(
|
||||
'text-left text-sm md:text-base',
|
||||
status === DeployStatus.PROCESSING && 'text-elements-link',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Timer */}
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
||||
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="grow">{title}</div>
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<>
|
||||
^<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</>
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<>
|
||||
^<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CheckRoundFilledIcon className="text-elements-success" size={18} />
|
||||
<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Collapse open={collapse}>
|
||||
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
|
||||
</button>
|
||||
|
||||
{/* Collapsible */}
|
||||
<Collapse open={isOpen}>
|
||||
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
|
||||
{/* Logs */}
|
||||
{processLogs.map((log, key) => {
|
||||
return (
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
key={key}
|
||||
placeholder={''}
|
||||
>
|
||||
<p className="font-mono" key={key}>
|
||||
{log}
|
||||
</Typography>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
<div className="sticky bottom-0 left-1/2 flex justify-center">
|
||||
|
||||
{/* End of logs ref used for hiding gradient overlay */}
|
||||
<div ref={ref} />
|
||||
|
||||
{/* Overflow gradient overlay */}
|
||||
{!hideGradientOverlay && (
|
||||
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
|
||||
)}
|
||||
|
||||
{/* Copy log button */}
|
||||
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
|
||||
<Button
|
||||
size="sm"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(processLogs.join('\n'));
|
||||
toast.success('Logs copied');
|
||||
toast({
|
||||
title: 'Logs copied',
|
||||
variant: 'success',
|
||||
id: 'logs',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}}
|
||||
color="blue"
|
||||
placeholder={''}
|
||||
leftIcon={<CopyIcon size={16} />}
|
||||
>
|
||||
^ Copy log
|
||||
Copy log
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
@@ -6,8 +7,7 @@ import {
|
||||
TemplateIconType,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
@@ -24,9 +24,13 @@ export interface TemplateCardProps extends ComponentPropsWithoutRef<'div'> {
|
||||
isGitAuth: boolean;
|
||||
}
|
||||
|
||||
export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
template,
|
||||
isGitAuth,
|
||||
}: TemplateCardProps) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (template?.isComingSoon) {
|
||||
@@ -38,7 +42,9 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
});
|
||||
}
|
||||
if (isGitAuth) {
|
||||
return navigate(`/template?templateId=${template.id}`);
|
||||
return navigate(
|
||||
`/${orgSlug}/projects/create/template?templateId=${template.id}`,
|
||||
);
|
||||
}
|
||||
return toast({
|
||||
id: 'connect-git-account',
|
||||
@@ -46,7 +52,7 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}, [isGitAuth, navigate, template, toast]);
|
||||
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
+103
-246
@@ -1,31 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import React from 'react';
|
||||
import {
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
Domain,
|
||||
Environment,
|
||||
Project,
|
||||
Domain,
|
||||
DeploymentStatus,
|
||||
Deployment,
|
||||
} from 'gql-client';
|
||||
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Typography,
|
||||
Chip,
|
||||
ChipProps,
|
||||
Tooltip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
||||
import AssignDomainDialog from './AssignDomainDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
BranchStrokeIcon,
|
||||
CheckRoundFilledIcon,
|
||||
ClockOutlineIcon,
|
||||
CommitIcon,
|
||||
LoadingIcon,
|
||||
WarningIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { OverflownText } from 'components/shared/OverflownText';
|
||||
import { Tag, TagTheme } from 'components/shared/Tag';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
import { DeploymentMenu } from './DeploymentMenu';
|
||||
|
||||
interface DeployDetailsCardProps {
|
||||
deployment: Deployment;
|
||||
@@ -35,10 +32,12 @@ interface DeployDetailsCardProps {
|
||||
prodBranchDomains: Domain[];
|
||||
}
|
||||
|
||||
const STATUS_COLORS: { [key in DeploymentStatus]: ChipProps['color'] } = {
|
||||
[DeploymentStatus.Building]: 'blue',
|
||||
[DeploymentStatus.Ready]: 'green',
|
||||
[DeploymentStatus.Error]: 'red',
|
||||
const STATUS_COLORS: {
|
||||
[key in DeploymentStatus]: TagTheme['type'];
|
||||
} = {
|
||||
[DeploymentStatus.Building]: 'emphasized',
|
||||
[DeploymentStatus.Ready]: 'positive',
|
||||
[DeploymentStatus.Error]: 'negative',
|
||||
};
|
||||
|
||||
const DeploymentDetailsCard = ({
|
||||
@@ -48,241 +47,99 @@ const DeploymentDetailsCard = ({
|
||||
project,
|
||||
prodBranchDomains,
|
||||
}: DeployDetailsCardProps) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
||||
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
||||
|
||||
const updateDeployment = async () => {
|
||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||
if (isUpdated) {
|
||||
await onUpdate();
|
||||
toast.success('Deployment changed to production');
|
||||
} else {
|
||||
toast.error('Unable to change deployment to production');
|
||||
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
||||
if (status === DeploymentStatus.Building) {
|
||||
return <LoadingIcon className="animate-spin" />;
|
||||
}
|
||||
};
|
||||
|
||||
const redeployToProd = async () => {
|
||||
const isRedeployed = await client.redeployToProd(deployment.id);
|
||||
if (isRedeployed) {
|
||||
await onUpdate();
|
||||
toast.success('Redeployed to production');
|
||||
} else {
|
||||
toast.error('Unable to redeploy to production');
|
||||
if (status === DeploymentStatus.Ready) {
|
||||
return <CheckRoundFilledIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const rollbackDeploymentHandler = async () => {
|
||||
const isRollbacked = await client.rollbackDeployment(
|
||||
project.id,
|
||||
deployment.id,
|
||||
);
|
||||
if (isRollbacked) {
|
||||
await onUpdate();
|
||||
toast.success('Deployment rolled back');
|
||||
} else {
|
||||
toast.error('Unable to rollback deployment');
|
||||
if (status === DeploymentStatus.Error) {
|
||||
return <WarningIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-8 gap-2 border-b border-gray-300 p-3 my-2">
|
||||
<div className="col-span-3">
|
||||
<div className="flex">
|
||||
{deployment.url && (
|
||||
<Typography className="basis-3/4" placeholder={''}>
|
||||
<div className="flex lg:flex gap-2 lg:gap-2 2xl:gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent ">
|
||||
<div className="flex-1 max-w-[30%] space-y-2">
|
||||
{/* DEPLOYMENT URL */}
|
||||
{deployment.url && (
|
||||
<Heading
|
||||
className="text-sm font-medium text-elements-high-em tracking-tight"
|
||||
as="h2"
|
||||
>
|
||||
<OverflownText content={deployment.url}>
|
||||
{deployment.url}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<Typography color="gray" placeholder={''}>
|
||||
</OverflownText>
|
||||
</Heading>
|
||||
)}
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</Typography>
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Chip
|
||||
value={deployment.status}
|
||||
color={STATUS_COLORS[deployment.status] ?? 'gray'}
|
||||
variant="ghost"
|
||||
icon={<i>^</i>}
|
||||
|
||||
{/* DEPLOYMENT STATUS */}
|
||||
<div className="w-[10%] max-w-[110px]">
|
||||
<Tag
|
||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||
size="xs"
|
||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||
>
|
||||
{deployment.status}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* DEPLOYMENT COMMIT DETAILS */}
|
||||
<div className="text-sm w-[25%] space-y-2 text-elements-low-em">
|
||||
<span className="flex gap-1.5 items-center">
|
||||
<BranchStrokeIcon className="h-4 w-4" />
|
||||
{deployment.branch}
|
||||
</span>
|
||||
<span className="flex gap-2 items-center">
|
||||
<CommitIcon />
|
||||
<OverflownText content={deployment.commitMessage}>
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</OverflownText>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* DEPLOYMENT INFOs */}
|
||||
<div className="ml-auto max-w-[312px] w-[30%] gap-1 2xl:gap-5 flex items-center justify-between text-elements-low-em text-sm">
|
||||
<div className="flex w-[70%] items-center gap-0.5 2xl:gap-2 flex-1">
|
||||
<ClockOutlineIcon className="h-4 w-4" />
|
||||
<OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}>
|
||||
{relativeTimeMs(deployment.createdAt)}
|
||||
</OverflownText>
|
||||
<div>
|
||||
<Avatar
|
||||
type="orange"
|
||||
initials={getInitials(deployment.createdBy.name ?? '')}
|
||||
className="lg:size-5 2xl:size-6"
|
||||
// TODO: Add avatarUrl
|
||||
// imageSrc={deployment.createdBy.avatarUrl}
|
||||
></Avatar>
|
||||
</div>
|
||||
|
||||
<OverflownText
|
||||
// className="min-w-[200px]"
|
||||
content={formatAddress(deployment.createdBy?.name ?? '')}
|
||||
>
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</OverflownText>
|
||||
</div>
|
||||
<DeploymentMenu
|
||||
className="ml-auto"
|
||||
deployment={deployment}
|
||||
currentDeployment={currentDeployment}
|
||||
onUpdate={onUpdate}
|
||||
project={project}
|
||||
prodBranchDomains={prodBranchDomains}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Typography color="gray" placeholder={''}>
|
||||
^ {deployment.branch}
|
||||
</Typography>
|
||||
<Typography color="gray" placeholder={''}>
|
||||
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<Typography color="gray" className="grow" placeholder={''}>
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||
<Tooltip content={deployment.createdBy.name}>
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
<Menu placement="bottom-start">
|
||||
<MenuHandler>
|
||||
<button className="self-start">...</button>
|
||||
</MenuHandler>
|
||||
<MenuList placeholder={''}>
|
||||
<a href={deployment.url} target="_blank" rel="noreferrer">
|
||||
<MenuItem disabled={!Boolean(deployment.url)} placeholder={''}>
|
||||
^ Visit
|
||||
</MenuItem>
|
||||
</a>
|
||||
<MenuItem
|
||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Assign domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => setChangeToProduction(!changeToProduction)}
|
||||
disabled={!(deployment.environment !== Environment.Production)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Change to production
|
||||
</MenuItem>
|
||||
<hr className="my-3" />
|
||||
<MenuItem
|
||||
onClick={() => setRedeployToProduction(!redeployToProduction)}
|
||||
disabled={
|
||||
!(
|
||||
deployment.environment === Environment.Production &&
|
||||
deployment.isCurrent
|
||||
)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Redeploy to production
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => setRollbackDeployment(!rollbackDeployment)}
|
||||
disabled={
|
||||
deployment.isCurrent ||
|
||||
deployment.environment !== Environment.Production ||
|
||||
!Boolean(currentDeployment)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Rollback to this version
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Change to production?"
|
||||
handleOpen={() => setChangeToProduction((preVal) => !preVal)}
|
||||
open={changeToProduction}
|
||||
confirmButtonTitle="Change"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await updateDeployment();
|
||||
setChangeToProduction((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will be changed to production.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
The new deployment will be associated with these domains:
|
||||
</Typography>
|
||||
{prodBranchDomains.length > 0 &&
|
||||
prodBranchDomains.map((value) => {
|
||||
return (
|
||||
<Typography
|
||||
variant="small"
|
||||
color="blue"
|
||||
key={value.id}
|
||||
placeholder={''}
|
||||
>
|
||||
^ {value.name}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Redeploy to production?"
|
||||
handleOpen={() => setRedeployToProduction((preVal) => !preVal)}
|
||||
open={redeployToProduction}
|
||||
confirmButtonTitle="Redeploy"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await redeployToProd();
|
||||
setRedeployToProduction((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, new deployment will be created with the same
|
||||
source code as current deployment.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
{deployment.domain?.name && (
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
{deployment.domain?.name}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
{Boolean(currentDeployment) && (
|
||||
<ConfirmDialog
|
||||
dialogTitle="Rollback to this deployment?"
|
||||
handleOpen={() => setRollbackDeployment((preVal) => !preVal)}
|
||||
open={rollbackDeployment}
|
||||
confirmButtonTitle="Rollback"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await rollbackDeploymentHandler();
|
||||
setRollbackDeployment((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will replace your current
|
||||
deployment
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={currentDeployment}
|
||||
chip={{
|
||||
value: 'Live Deployment',
|
||||
color: 'green',
|
||||
}}
|
||||
/>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={deployment}
|
||||
chip={{
|
||||
value: 'New Deployment',
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
^ {currentDeployment.domain?.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
)}
|
||||
<AssignDomainDialog
|
||||
open={assignDomainDialog}
|
||||
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Deployment, Domain, Environment, Project } from 'gql-client';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
GlobeIcon,
|
||||
HorizontalDotIcon,
|
||||
LinkIcon,
|
||||
RefreshIcon,
|
||||
RocketIcon,
|
||||
UndoIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
import { ComponentPropsWithRef } from 'react';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import AssignDomainDialog from './AssignDomainDialog';
|
||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
||||
import { Typography } from '@material-tailwind/react';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
||||
deployment: Deployment;
|
||||
currentDeployment: Deployment;
|
||||
onUpdate: () => Promise<void>;
|
||||
project: Project;
|
||||
prodBranchDomains: Domain[];
|
||||
}
|
||||
|
||||
export const DeploymentMenu = ({
|
||||
deployment,
|
||||
currentDeployment,
|
||||
onUpdate,
|
||||
project,
|
||||
prodBranchDomains,
|
||||
className,
|
||||
...props
|
||||
}: DeploymentMenuProps) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
||||
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
||||
|
||||
const updateDeployment = async () => {
|
||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||
if (isUpdated) {
|
||||
await onUpdate();
|
||||
toast.success('Deployment changed to production');
|
||||
} else {
|
||||
toast.error('Unable to change deployment to production');
|
||||
}
|
||||
};
|
||||
|
||||
const redeployToProd = async () => {
|
||||
const isRedeployed = await client.redeployToProd(deployment.id);
|
||||
if (isRedeployed) {
|
||||
await onUpdate();
|
||||
toast.success('Redeployed to production');
|
||||
} else {
|
||||
toast.error('Unable to redeploy to production');
|
||||
}
|
||||
};
|
||||
|
||||
const rollbackDeploymentHandler = async () => {
|
||||
const isRollbacked = await client.rollbackDeployment(
|
||||
project.id,
|
||||
deployment.id,
|
||||
);
|
||||
if (isRollbacked) {
|
||||
await onUpdate();
|
||||
toast.success('Deployment rolled back');
|
||||
} else {
|
||||
toast.error('Unable to rollback deployment');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('max-w-[32px]', className)} {...props}>
|
||||
<Menu placement="bottom-start">
|
||||
<MenuHandler>
|
||||
<Button
|
||||
shape="default"
|
||||
size="xs"
|
||||
variant="unstyled"
|
||||
className={cn(
|
||||
'h-8 w-8 rounded-full border border-transparent transition-colors background-transparent',
|
||||
'[&[aria-expanded=true]]:border [&[aria-expanded=true]]:border-border-interactive [&[aria-expanded=true]]:bg-controls-tertiary [&[aria-expanded=true]]:shadow-button',
|
||||
)}
|
||||
leftIcon={<HorizontalDotIcon />}
|
||||
aria-label="Toggle Menu"
|
||||
/>
|
||||
</MenuHandler>
|
||||
<MenuList className="text-elements-high-em" placeholder={''}>
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized"
|
||||
disabled={!Boolean(deployment.url)}
|
||||
placeholder={''}
|
||||
>
|
||||
<a
|
||||
className="flex items-center gap-3"
|
||||
href={deployment.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<LinkIcon /> Visit
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
placeholder={''}
|
||||
>
|
||||
<GlobeIcon /> Assign domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setChangeToProduction(!changeToProduction)}
|
||||
disabled={!(deployment.environment !== Environment.Production)}
|
||||
placeholder={''}
|
||||
>
|
||||
<RocketIcon /> Change to production
|
||||
</MenuItem>
|
||||
<hr className="my-3" />
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setRedeployToProduction(!redeployToProduction)}
|
||||
disabled={
|
||||
!(
|
||||
deployment.environment === Environment.Production &&
|
||||
deployment.isCurrent
|
||||
)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
<RefreshIcon /> Redeploy to production
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setRollbackDeployment(!rollbackDeployment)}
|
||||
disabled={
|
||||
deployment.isCurrent ||
|
||||
deployment.environment !== Environment.Production ||
|
||||
!Boolean(currentDeployment)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
<UndoIcon /> Rollback to this version
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
{/* Dialogs */}
|
||||
<ConfirmDialog
|
||||
dialogTitle="Change to production?"
|
||||
handleOpen={() => setChangeToProduction((preVal) => !preVal)}
|
||||
open={changeToProduction}
|
||||
confirmButtonTitle="Change"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await updateDeployment();
|
||||
setChangeToProduction((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will be changed to production.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
The new deployment will be associated with these domains:
|
||||
</Typography>
|
||||
{prodBranchDomains.length > 0 &&
|
||||
prodBranchDomains.map((value) => {
|
||||
return (
|
||||
<Typography
|
||||
variant="small"
|
||||
color="blue"
|
||||
key={value.id}
|
||||
placeholder={''}
|
||||
>
|
||||
^ {value.name}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Redeploy to production?"
|
||||
handleOpen={() => setRedeployToProduction((preVal) => !preVal)}
|
||||
open={redeployToProduction}
|
||||
confirmButtonTitle="Redeploy"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await redeployToProd();
|
||||
setRedeployToProduction((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, new deployment will be created with the same
|
||||
source code as current deployment.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
{deployment.domain?.name && (
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
{deployment.domain?.name}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
{Boolean(currentDeployment) && (
|
||||
<ConfirmDialog
|
||||
dialogTitle="Rollback to this deployment?"
|
||||
handleOpen={() => setRollbackDeployment((preVal) => !preVal)}
|
||||
open={rollbackDeployment}
|
||||
confirmButtonTitle="Rollback"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await rollbackDeploymentHandler();
|
||||
setRollbackDeployment((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will replace your current
|
||||
deployment
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={currentDeployment}
|
||||
chip={{
|
||||
value: 'Live Deployment',
|
||||
color: 'green',
|
||||
}}
|
||||
/>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={deployment}
|
||||
chip={{
|
||||
value: 'New Deployment',
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
^ {currentDeployment.domain?.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
)}
|
||||
<AssignDomainDialog
|
||||
open={assignDomainDialog}
|
||||
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CommitIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_755_4058)">
|
||||
<path
|
||||
d="M15.5 7.50001H11.4637C11.343 6.66752 10.9264 5.90636 10.2904 5.35589C9.65427 4.80541 8.84121 4.50244 8 4.50244C7.15879 4.50244 6.34573 4.80541 5.70964 5.35589C5.07355 5.90636 4.65701 6.66752 4.53625 7.50001H0.5C0.367392 7.50001 0.240215 7.55269 0.146447 7.64646C0.0526784 7.74023 0 7.8674 0 8.00001C0 8.13262 0.0526784 8.2598 0.146447 8.35357C0.240215 8.44733 0.367392 8.50001 0.5 8.50001H4.53625C4.65701 9.33251 5.07355 10.0937 5.70964 10.6441C6.34573 11.1946 7.15879 11.4976 8 11.4976C8.84121 11.4976 9.65427 11.1946 10.2904 10.6441C10.9264 10.0937 11.343 9.33251 11.4637 8.50001H15.5C15.6326 8.50001 15.7598 8.44733 15.8536 8.35357C15.9473 8.2598 16 8.13262 16 8.00001C16 7.8674 15.9473 7.74023 15.8536 7.64646C15.7598 7.55269 15.6326 7.50001 15.5 7.50001ZM8 10.5C7.50555 10.5 7.0222 10.3534 6.61107 10.0787C6.19995 9.80398 5.87952 9.41354 5.6903 8.95672C5.50108 8.49991 5.45157 7.99724 5.54804 7.51229C5.6445 7.02733 5.8826 6.58188 6.23223 6.23224C6.58186 5.88261 7.02732 5.64451 7.51227 5.54805C7.99723 5.45158 8.49989 5.50109 8.95671 5.69031C9.41352 5.87953 9.80397 6.19996 10.0787 6.61109C10.3534 7.02221 10.5 7.50556 10.5 8.00001C10.5 8.66305 10.2366 9.29894 9.76777 9.76778C9.29893 10.2366 8.66304 10.5 8 10.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_755_4058">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CopyIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.33301 2.66665C1.33301 1.93027 1.92996 1.33331 2.66634 1.33331H9.33301C10.0694 1.33331 10.6663 1.93027 10.6663 2.66665V5.33331H13.333C14.0694 5.33331 14.6663 5.93027 14.6663 6.66665V13.3333C14.6663 14.0697 14.0694 14.6666 13.333 14.6666H6.66634C5.92996 14.6666 5.33301 14.0697 5.33301 13.3333V10.6666H2.66634C1.92996 10.6666 1.33301 10.0697 1.33301 9.33331V2.66665ZM9.66634 5.33331H6.66634C5.92996 5.33331 5.33301 5.93027 5.33301 6.66665V9.66665H2.66634C2.48225 9.66665 2.33301 9.51741 2.33301 9.33331V2.66665C2.33301 2.48255 2.48225 2.33331 2.66634 2.33331H9.33301C9.5171 2.33331 9.66634 2.48255 9.66634 2.66665V5.33331Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LoaderIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12.5003 3V6M12.5003 18V21M6.13634 5.63604L8.25766 7.75736M16.7429 16.2426L18.8643 18.364M3.5 12.0007H6.5M18.5 12.0007H21.5M6.13634 18.364L8.25766 16.2426M16.7429 7.75736L18.8643 5.63604"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const MinusCircleIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.5 12C2.5 6.47715 6.97715 2 12.5 2C18.0228 2 22.5 6.47715 22.5 12C22.5 17.5228 18.0228 22 12.5 22C6.97715 22 2.5 17.5228 2.5 12ZM16.5 12.9999C17.0523 12.9999 17.5 12.5522 17.5 11.9999C17.5 11.4476 17.0523 10.9999 16.5 10.9999L8.49997 11.0001C7.94769 11.0001 7.49998 11.4479 7.5 12.0001C7.50002 12.5524 7.94774 13.0001 8.50003 13.0001L16.5 12.9999Z"
|
||||
fill="#0F86F5"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const RocketIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4.87626 10.1252H3.34633C2.77976 10.1252 2.41778 9.52109 2.68496 9.02148L4.07738 6.41779C4.33833 5.92983 4.84675 5.62517 5.40011 5.62517H8.40751M4.87626 10.1252L7.87626 13.1252M4.87626 10.1252L8.40751 5.62517M7.87626 13.1252V14.6551C7.87626 15.2217 8.48034 15.5836 8.97995 15.3165L11.5836 13.924C12.0716 13.6631 12.3763 13.1547 12.3763 12.6013V9.66486M7.87626 13.1252L12.3763 9.66486M12.3763 9.66486C14.5604 7.66276 15.8988 5.43328 16.0998 2.62499C16.1294 2.21183 15.7895 1.87198 15.3764 1.90172C12.5784 2.10308 10.4093 3.4414 8.40751 5.62517M3.37626 16.1252H1.875V14.6258C1.875 13.797 2.54717 13.1252 3.37594 13.1252C4.20437 13.1252 4.87626 13.7967 4.87626 14.6252C4.87626 15.4536 4.20469 16.1252 3.37626 16.1252Z"
|
||||
stroke="currentColor"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const UndoIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4.49989 3.75L2.03022 6.21967C1.73732 6.51256 1.73732 6.98744 2.03022 7.28033L4.49989 9.75M2.24989 6.75H13.1249C14.7817 6.75 16.1249 8.09315 16.1249 9.75V10.875C16.1249 12.5319 14.7817 13.875 13.1249 13.875H8.99989"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -43,6 +43,13 @@ export * from './LinkIcon';
|
||||
export * from './CursorBoxIcon';
|
||||
export * from './CrossCircleIcon';
|
||||
export * from './RefreshIcon';
|
||||
export * from './CommitIcon';
|
||||
export * from './RocketIcon';
|
||||
export * from './RefreshIcon';
|
||||
export * from './UndoIcon';
|
||||
export * from './LoaderIcon';
|
||||
export * from './MinusCircleIcon';
|
||||
export * from './CopyIcon';
|
||||
|
||||
// Templates
|
||||
export * from './templates';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { cn } from 'utils/classnames';
|
||||
import { Tooltip, TooltipProps } from 'components/shared/Tooltip';
|
||||
import { debounce } from 'lodash';
|
||||
import React, {
|
||||
ComponentPropsWithRef,
|
||||
PropsWithChildren,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { PolymorphicProps } from 'types/common';
|
||||
|
||||
interface OverflownTextProps extends ComponentPropsWithRef<'span'> {
|
||||
tooltipProps?: TooltipProps;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
type ElementType = 'span' | 'div';
|
||||
|
||||
// This component is used to truncate text and show a tooltip if the text is overflown.
|
||||
export const OverflownText = <Element extends ElementType>({
|
||||
tooltipProps,
|
||||
children,
|
||||
content,
|
||||
className,
|
||||
as,
|
||||
...props
|
||||
}: PropsWithChildren<PolymorphicProps<Element, OverflownTextProps>>) => {
|
||||
const ref = useRef(null);
|
||||
const [isOverflown, setIsOverflown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current as HTMLElement | null;
|
||||
if (!element) return;
|
||||
|
||||
setIsOverflown(element.scrollWidth > element.clientWidth);
|
||||
|
||||
const handleResize = () => {
|
||||
const isOverflown = element.scrollWidth > element.clientWidth;
|
||||
setIsOverflown(isOverflown);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', debounce(handleResize, 500));
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const Component = as || 'span';
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={content ?? children}
|
||||
delayDuration={500}
|
||||
contentProps={{
|
||||
className: 'text-xs',
|
||||
}}
|
||||
open={isOverflown ? undefined : false}
|
||||
{...tooltipProps}
|
||||
>
|
||||
<Component
|
||||
ref={ref}
|
||||
className={cn('truncate block', className)} // line-clamp-1 won't work here
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './OverflownText';
|
||||
@@ -4,7 +4,7 @@ import type { VariantProps } from 'tailwind-variants';
|
||||
export const tagTheme = tv(
|
||||
{
|
||||
slots: {
|
||||
wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'],
|
||||
wrapper: ['inline-flex', 'gap-1.5', 'rounded-lg', 'border'],
|
||||
icon: [],
|
||||
label: ['font-inter', 'text-xs'],
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { CrossIcon } from 'components/shared/CustomIcon';
|
||||
import { cn } from 'utils/classnames';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
|
||||
export interface CreateProjectLayoutProps
|
||||
extends ComponentPropsWithoutRef<'section'> {}
|
||||
@@ -16,24 +17,77 @@ export const CreateProjectLayout = ({
|
||||
}: CreateProjectLayoutProps) => {
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
const closeBtnLink = `/${orgSlug}`;
|
||||
|
||||
const heading = (
|
||||
<Heading as="h2" className="flex-1 text-xl md:text-2xl font-medium">
|
||||
Create new project
|
||||
</Heading>
|
||||
);
|
||||
|
||||
return (
|
||||
<section {...props} className={cn('h-full flex flex-col', className)}>
|
||||
<div className="sticky top-0">
|
||||
<div className="flex px-6 py-4 bg-base-bg items-center gap-4">
|
||||
<Heading as="h2" className="flex-1 text-[24px] font-medium">
|
||||
Create new project
|
||||
</Heading>
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<Button iconOnly variant="tertiary">
|
||||
<CrossIcon size={18} />
|
||||
</Button>
|
||||
</Link>
|
||||
<>
|
||||
{/* Desktop */}
|
||||
<section
|
||||
{...props}
|
||||
className={cn('h-full flex-col hidden md:flex', className)}
|
||||
>
|
||||
<div className="sticky top-0">
|
||||
<div className="flex px-6 py-4 bg-base-bg items-center gap-4">
|
||||
{heading}
|
||||
|
||||
{/* Cannot save btn as variable since responsive variant don't work with compoundVariant */}
|
||||
<Link to={closeBtnLink}>
|
||||
<Button
|
||||
iconOnly
|
||||
variant="tertiary"
|
||||
leftIcon={<CrossIcon />}
|
||||
aria-label="close"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<WavyBorder />
|
||||
</div>
|
||||
<WavyBorder />
|
||||
</div>
|
||||
<section className="px-6 h-full flex-1 py-6 overflow-y-auto">
|
||||
<Outlet />
|
||||
|
||||
<section className="px-6 h-full flex-1 py-6 overflow-y-auto">
|
||||
<Outlet />
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{/* Mobile */}
|
||||
{/* Setting modal={false} so even if modal is active on desktop, it doesn't block clicks */}
|
||||
<Dialog.Root modal={false} open={true}>
|
||||
<Dialog.Portal>
|
||||
{/* Not using <Dialog.Overlay> since modal={false} disables it and its content will not show */}
|
||||
<div className="bg-base-canvas fixed inset-0 md:hidden overflow-y-auto p-1">
|
||||
<Dialog.Content className="min-h-full overflow-hidden rounded-2xl bg-base-bg shadow-card focus:outline-none">
|
||||
{/* Heading */}
|
||||
<div className="flex px-6 py-4 h-20 items-center gap-4">
|
||||
{heading}
|
||||
<Dialog.Close asChild>
|
||||
<Link to={closeBtnLink}>
|
||||
<Button
|
||||
iconOnly
|
||||
variant="tertiary"
|
||||
leftIcon={<CrossIcon />}
|
||||
aria-label="close"
|
||||
size="sm"
|
||||
/>
|
||||
</Link>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
{/* Border */}
|
||||
<WavyBorder />
|
||||
|
||||
{/* Page content */}
|
||||
<div className="px-4 py-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ const DeploymentsTabPanel = () => {
|
||||
setFilterValue(DEFAULT_FILTER_VALUE);
|
||||
}, []);
|
||||
|
||||
const onUpdateDeploymenToProd = async () => {
|
||||
const onUpdateDeploymentToProd = async () => {
|
||||
await fetchDeployments();
|
||||
};
|
||||
|
||||
@@ -113,7 +113,7 @@ const DeploymentsTabPanel = () => {
|
||||
deployment={deployment}
|
||||
key={key}
|
||||
currentDeployment={currentDeployment!}
|
||||
onUpdate={onUpdateDeploymenToProd}
|
||||
onUpdate={onUpdateDeploymentToProd}
|
||||
project={project}
|
||||
prodBranchDomains={prodBranchDomains}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,9 @@ export default withMT({
|
||||
zIndex: {
|
||||
tooltip: '52',
|
||||
},
|
||||
letterSpacing: {
|
||||
tight: '-0.084px',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
display: ['Inter Display', 'sans-serif'],
|
||||
@@ -84,6 +87,7 @@ export default withMT({
|
||||
900: '#0a3a5c',
|
||||
},
|
||||
base: {
|
||||
canvas: '#ECF6FE',
|
||||
bg: '#ffffff',
|
||||
'bg-alternate': '#f8fafc',
|
||||
'bg-emphasized': '#f1f5f9',
|
||||
|
||||
Reference in New Issue
Block a user