feat: add deployment lines

This commit is contained in:
Sushan Yadav 2024-02-29 18:33:12 +05:45
parent 6a108c1a1b
commit 2c1a2fc0e8
14 changed files with 566 additions and 253 deletions

View File

@ -1,6 +1,7 @@
{
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
"tv\\('([^)]*)\\')",
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
]
}

View File

@ -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)}
/>
</>
);
};

View File

@ -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 './DeploymenMenu';
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>
);
};

View File

@ -44,7 +44,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
}, [value]);
return (
<div className="grid grid-cols-8 gap-2 text-sm text-gray-600">
<div className="grid items-center grid-cols-8 gap-2 text-sm text-gray-600">
<div className="col-span-4">
<SearchBar
placeholder="Search branches"

View File

@ -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>
);
};

View File

@ -0,0 +1,41 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const RefreshIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
d="M7.125 10.875V15.375H2.625M6.75 14.9666C4.33948 14.0571 2.625 11.7288 2.625 9C2.625 5.47918 5.47918 2.625 9 2.625C9.93578 2.625 10.8245 2.82663 11.625 3.1888"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.3125 15.1877C10.3125 15.4984 10.0607 15.7502 9.75 15.7502C9.43934 15.7502 9.1875 15.4984 9.1875 15.1877C9.1875 14.8771 9.43934 14.6252 9.75 14.6252C10.0607 14.6252 10.3125 14.8771 10.3125 15.1877Z"
fill="currentColor"
/>
<path
d="M13.2304 13.7025C13.3857 13.9716 13.2936 14.3156 13.0245 14.4709C12.7555 14.6262 12.4115 14.5341 12.2561 14.265C12.1008 13.996 12.193 13.652 12.462 13.4966C12.7311 13.3413 13.0751 13.4335 13.2304 13.7025Z"
fill="currentColor"
/>
<path
d="M15.0147 10.9573C15.2838 11.1126 15.3759 11.4567 15.2206 11.7257C15.0653 11.9947 14.7213 12.0869 14.4522 11.9316C14.1832 11.7763 14.091 11.4322 14.2463 11.1632C14.4017 10.8942 14.7457 10.802 15.0147 10.9573Z"
fill="currentColor"
/>
<path
d="M14.2657 5.74407C13.9967 5.8994 13.6527 5.80722 13.4973 5.53818C13.342 5.26914 13.4342 4.92512 13.7032 4.76979C13.9723 4.61446 14.3163 4.70664 14.4716 4.97568C14.6269 5.24472 14.5348 5.58874 14.2657 5.74407Z"
fill="currentColor"
/>
<path
d="M15.75 8.25023C15.75 8.56089 15.4982 8.81273 15.1875 8.81273C14.8768 8.81273 14.625 8.56089 14.625 8.25023C14.625 7.93957 14.8768 7.68773 15.1875 7.68773C15.4982 7.68773 15.75 7.93957 15.75 8.25023Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -41,6 +41,10 @@ export * from './BranchStrokeIcon';
export * from './StorageIcon';
export * from './LinkIcon';
export * from './CursorBoxIcon';
export * from './CommitIcon';
export * from './RocketIcon';
export * from './RefreshIcon';
export * from './UndoIcon';
// Templates
export * from './templates';

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export * from './OverflownText';

View File

@ -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'],
},

View File

@ -88,17 +88,17 @@ const DeploymentsTabPanel = () => {
setFilterValue(DEFAULT_FILTER_VALUE);
}, []);
const onUpdateDeploymenToProd = async () => {
const onUpdateDeploymentToProd = async () => {
await fetchDeployments();
};
return (
<div className="p-4">
<div className="max-w-[1440px]">
<FilterForm
value={filterValue}
onChange={(value) => setFilterValue(value)}
/>
<div className="mt-2">
<div className="mt-3">
{Boolean(filteredDeployments.length) ? (
filteredDeployments.map((deployment, key) => {
return (
@ -106,7 +106,7 @@ const DeploymentsTabPanel = () => {
deployment={deployment}
key={key}
currentDeployment={currentDeployment!}
onUpdate={onUpdateDeploymenToProd}
onUpdate={onUpdateDeploymentToProd}
project={project}
prodBranchDomains={prodBranchDomains}
/>

View File

@ -12,6 +12,9 @@ export default withMT({
zIndex: {
tooltip: '52',
},
letterSpacing: {
tight: '-0.084px',
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Inter Display', 'sans-serif'],