Project Deployments - Deployed line items (#147)
* feat: add deployment lines * fix: typo DeploymentMenu
This commit is contained in:
parent
65f64a3dcd
commit
409b654f9b
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
// IntelliSense for taiwind variants
|
// IntelliSense for taiwind variants
|
||||||
"tailwindCSS.experimental.classRegex": [
|
"tailwindCSS.experimental.classRegex": [
|
||||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
"tv\\('([^)]*)\\')",
|
||||||
|
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,28 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import {
|
import {
|
||||||
|
Deployment,
|
||||||
|
DeploymentStatus,
|
||||||
|
Domain,
|
||||||
Environment,
|
Environment,
|
||||||
Project,
|
Project,
|
||||||
Domain,
|
|
||||||
DeploymentStatus,
|
|
||||||
Deployment,
|
|
||||||
} from 'gql-client';
|
} from 'gql-client';
|
||||||
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
import {
|
import {
|
||||||
Menu,
|
BranchStrokeIcon,
|
||||||
MenuHandler,
|
CheckRoundFilledIcon,
|
||||||
MenuList,
|
ClockOutlineIcon,
|
||||||
MenuItem,
|
CommitIcon,
|
||||||
Typography,
|
LoadingIcon,
|
||||||
Chip,
|
WarningIcon,
|
||||||
ChipProps,
|
} from 'components/shared/CustomIcon';
|
||||||
Tooltip,
|
import { Heading } from 'components/shared/Heading';
|
||||||
} from '@material-tailwind/react';
|
import { OverflownText } from 'components/shared/OverflownText';
|
||||||
|
import { Tag, TagTheme } from 'components/shared/Tag';
|
||||||
import { relativeTimeMs } from '../../../../utils/time';
|
import { getInitials } from 'utils/geInitials';
|
||||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
import { relativeTimeMs } from 'utils/time';
|
||||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
|
||||||
import AssignDomainDialog from './AssignDomainDialog';
|
|
||||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
|
||||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||||
import { formatAddress } from '../../../../utils/format';
|
import { formatAddress } from '../../../../utils/format';
|
||||||
|
import { DeploymentMenu } from './DeploymentMenu';
|
||||||
|
|
||||||
interface DeployDetailsCardProps {
|
interface DeployDetailsCardProps {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
@ -35,10 +32,12 @@ interface DeployDetailsCardProps {
|
|||||||
prodBranchDomains: Domain[];
|
prodBranchDomains: Domain[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_COLORS: { [key in DeploymentStatus]: ChipProps['color'] } = {
|
const STATUS_COLORS: {
|
||||||
[DeploymentStatus.Building]: 'blue',
|
[key in DeploymentStatus]: TagTheme['type'];
|
||||||
[DeploymentStatus.Ready]: 'green',
|
} = {
|
||||||
[DeploymentStatus.Error]: 'red',
|
[DeploymentStatus.Building]: 'emphasized',
|
||||||
|
[DeploymentStatus.Ready]: 'positive',
|
||||||
|
[DeploymentStatus.Error]: 'negative',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeploymentDetailsCard = ({
|
const DeploymentDetailsCard = ({
|
||||||
@ -48,241 +47,99 @@ const DeploymentDetailsCard = ({
|
|||||||
project,
|
project,
|
||||||
prodBranchDomains,
|
prodBranchDomains,
|
||||||
}: DeployDetailsCardProps) => {
|
}: DeployDetailsCardProps) => {
|
||||||
const client = useGQLClient();
|
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
||||||
|
if (status === DeploymentStatus.Building) {
|
||||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
return <LoadingIcon className="animate-spin" />;
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
};
|
if (status === DeploymentStatus.Ready) {
|
||||||
|
return <CheckRoundFilledIcon />;
|
||||||
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 () => {
|
if (status === DeploymentStatus.Error) {
|
||||||
const isRollbacked = await client.rollbackDeployment(
|
return <WarningIcon />;
|
||||||
project.id,
|
|
||||||
deployment.id,
|
|
||||||
);
|
|
||||||
if (isRollbacked) {
|
|
||||||
await onUpdate();
|
|
||||||
toast.success('Deployment rolled back');
|
|
||||||
} else {
|
|
||||||
toast.error('Unable to rollback deployment');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-8 gap-2 border-b border-gray-300 p-3 my-2">
|
<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="col-span-3">
|
<div className="flex-1 max-w-[30%] space-y-2">
|
||||||
<div className="flex">
|
{/* DEPLOYMENT URL */}
|
||||||
{deployment.url && (
|
{deployment.url && (
|
||||||
<Typography className="basis-3/4" placeholder={''}>
|
<Heading
|
||||||
|
className="text-sm font-medium text-elements-high-em tracking-tight"
|
||||||
|
as="h2"
|
||||||
|
>
|
||||||
|
<OverflownText content={deployment.url}>
|
||||||
{deployment.url}
|
{deployment.url}
|
||||||
</Typography>
|
</OverflownText>
|
||||||
|
</Heading>
|
||||||
)}
|
)}
|
||||||
</div>
|
<span className="text-sm text-elements-low-em tracking-tight">
|
||||||
<Typography color="gray" placeholder={''}>
|
|
||||||
{deployment.environment === Environment.Production
|
{deployment.environment === Environment.Production
|
||||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||||
: 'Preview'}
|
: 'Preview'}
|
||||||
</Typography>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1">
|
|
||||||
<Chip
|
{/* DEPLOYMENT STATUS */}
|
||||||
value={deployment.status}
|
<div className="w-[10%] max-w-[110px]">
|
||||||
color={STATUS_COLORS[deployment.status] ?? 'gray'}
|
<Tag
|
||||||
variant="ghost"
|
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||||
icon={<i>^</i>}
|
size="xs"
|
||||||
/>
|
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||||
|
>
|
||||||
|
{deployment.status}
|
||||||
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
|
||||||
<Typography color="gray" placeholder={''}>
|
{/* DEPLOYMENT COMMIT DETAILS */}
|
||||||
^ {deployment.branch}
|
<div className="text-sm w-[25%] space-y-2 text-elements-low-em">
|
||||||
</Typography>
|
<span className="flex gap-1.5 items-center">
|
||||||
<Typography color="gray" placeholder={''}>
|
<BranchStrokeIcon className="h-4 w-4" />
|
||||||
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
{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}
|
{deployment.commitMessage}
|
||||||
</Typography>
|
</OverflownText>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex items-center">
|
|
||||||
<Typography color="gray" className="grow" placeholder={''}>
|
{/* DEPLOYMENT INFOs */}
|
||||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
<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">
|
||||||
<Tooltip content={deployment.createdBy.name}>
|
<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 ?? '')}
|
{formatAddress(deployment.createdBy.name ?? '')}
|
||||||
</Tooltip>
|
</OverflownText>
|
||||||
</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>
|
</div>
|
||||||
<ConfirmDialog
|
<DeploymentMenu
|
||||||
dialogTitle="Change to production?"
|
className="ml-auto"
|
||||||
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}
|
deployment={deployment}
|
||||||
chip={{
|
currentDeployment={currentDeployment}
|
||||||
value: 'New Deployment',
|
onUpdate={onUpdate}
|
||||||
color: 'orange',
|
project={project}
|
||||||
}}
|
prodBranchDomains={prodBranchDomains}
|
||||||
/>
|
/>
|
||||||
<Typography variant="small" placeholder={''}>
|
|
||||||
These domains will point to your new deployment:
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="small" color="blue" placeholder={''}>
|
|
||||||
^ {currentDeployment.domain?.name}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
</ConfirmDialog>
|
|
||||||
)}
|
|
||||||
<AssignDomainDialog
|
|
||||||
open={assignDomainDialog}
|
|
||||||
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
|
||||||
/>
|
|
||||||
</div>
|
</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)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -44,7 +44,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
|||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
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">
|
<div className="col-span-4">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
placeholder="Search branches"
|
placeholder="Search branches"
|
||||||
|
@ -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,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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -41,6 +41,10 @@ export * from './BranchStrokeIcon';
|
|||||||
export * from './StorageIcon';
|
export * from './StorageIcon';
|
||||||
export * from './LinkIcon';
|
export * from './LinkIcon';
|
||||||
export * from './CursorBoxIcon';
|
export * from './CursorBoxIcon';
|
||||||
|
export * from './CommitIcon';
|
||||||
|
export * from './RocketIcon';
|
||||||
|
export * from './RefreshIcon';
|
||||||
|
export * from './UndoIcon';
|
||||||
|
|
||||||
// Templates
|
// Templates
|
||||||
export * from './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(
|
export const tagTheme = tv(
|
||||||
{
|
{
|
||||||
slots: {
|
slots: {
|
||||||
wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'],
|
wrapper: ['inline-flex', 'gap-1.5', 'rounded-lg', 'border'],
|
||||||
icon: [],
|
icon: [],
|
||||||
label: ['font-inter', 'text-xs'],
|
label: ['font-inter', 'text-xs'],
|
||||||
},
|
},
|
||||||
|
@ -88,17 +88,17 @@ const DeploymentsTabPanel = () => {
|
|||||||
setFilterValue(DEFAULT_FILTER_VALUE);
|
setFilterValue(DEFAULT_FILTER_VALUE);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onUpdateDeploymenToProd = async () => {
|
const onUpdateDeploymentToProd = async () => {
|
||||||
await fetchDeployments();
|
await fetchDeployments();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="max-w-[1440px]">
|
||||||
<FilterForm
|
<FilterForm
|
||||||
value={filterValue}
|
value={filterValue}
|
||||||
onChange={(value) => setFilterValue(value)}
|
onChange={(value) => setFilterValue(value)}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2">
|
<div className="mt-3">
|
||||||
{Boolean(filteredDeployments.length) ? (
|
{Boolean(filteredDeployments.length) ? (
|
||||||
filteredDeployments.map((deployment, key) => {
|
filteredDeployments.map((deployment, key) => {
|
||||||
return (
|
return (
|
||||||
@ -106,7 +106,7 @@ const DeploymentsTabPanel = () => {
|
|||||||
deployment={deployment}
|
deployment={deployment}
|
||||||
key={key}
|
key={key}
|
||||||
currentDeployment={currentDeployment!}
|
currentDeployment={currentDeployment!}
|
||||||
onUpdate={onUpdateDeploymenToProd}
|
onUpdate={onUpdateDeploymentToProd}
|
||||||
project={project}
|
project={project}
|
||||||
prodBranchDomains={prodBranchDomains}
|
prodBranchDomains={prodBranchDomains}
|
||||||
/>
|
/>
|
||||||
|
@ -12,6 +12,9 @@ export default withMT({
|
|||||||
zIndex: {
|
zIndex: {
|
||||||
tooltip: '52',
|
tooltip: '52',
|
||||||
},
|
},
|
||||||
|
letterSpacing: {
|
||||||
|
tight: '-0.084px',
|
||||||
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'sans-serif'],
|
sans: ['Inter', 'sans-serif'],
|
||||||
display: ['Inter Display', 'sans-serif'],
|
display: ['Inter Display', 'sans-serif'],
|
||||||
|
Loading…
Reference in New Issue
Block a user