forked from cerc-io/snowballtools-base
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f8bfcfb91 | ||
|
|
2c1a2fc0e8 | ||
|
|
6a108c1a1b | ||
|
|
0fb378386a | ||
|
|
563059e887 | ||
|
|
f34eb48d1e | ||
|
|
acf5aab26f | ||
|
|
6b6582c287 | ||
|
|
b43ee3b7bb | ||
|
|
b1bf47d104 |
Vendored
+2
-1
@@ -1,6 +1,7 @@
|
||||
{
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
"tv\\('([^)]*)\\')",
|
||||
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export const projectCardTheme = tv({
|
||||
'rounded-2xl',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'group',
|
||||
],
|
||||
upperContent: ['px-4', 'py-4', 'flex', 'items-start', 'gap-3', 'relative'],
|
||||
content: ['flex', 'flex-col', 'gap-1', 'flex-1'],
|
||||
@@ -20,13 +21,15 @@ export const projectCardTheme = tv({
|
||||
description: ['text-xs', 'text-elements-low-em'],
|
||||
icons: ['flex', 'items-center', 'gap-1'],
|
||||
lowerContent: [
|
||||
'bg-surface-card-hovered',
|
||||
'transition-colors',
|
||||
'duration-150',
|
||||
'px-4',
|
||||
'py-4',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-2',
|
||||
'rounded-b-2xl',
|
||||
'group-hover:bg-surface-card-hovered',
|
||||
],
|
||||
latestDeployment: ['flex', 'items-center', 'gap-2'],
|
||||
deploymentStatusContainer: [
|
||||
@@ -46,6 +49,12 @@ export const projectCardTheme = tv({
|
||||
'items-center',
|
||||
'gap-2',
|
||||
],
|
||||
wavyBorder: [
|
||||
'bg-surface-card',
|
||||
'transition-colors',
|
||||
'duration-150',
|
||||
'group-hover:bg-surface-card-hovered',
|
||||
],
|
||||
},
|
||||
variants: {
|
||||
status: {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import React, { ComponentPropsWithoutRef, MouseEvent } from 'react';
|
||||
import React, {
|
||||
ComponentPropsWithoutRef,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||
import { Project } from 'gql-client';
|
||||
import { Button } from 'components/shared/Button';
|
||||
@@ -11,7 +15,7 @@ import {
|
||||
WarningDiamondIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import {
|
||||
@@ -27,6 +31,8 @@ export interface ProjectCardProps
|
||||
project: Project;
|
||||
}
|
||||
|
||||
// TODO: Update the whole component to use `Link` from `react-router-dom` and remove the `useNavigate` hook,
|
||||
// currently it's not possible to use `Link` because the dot menu is not a direct child of the `Link` component
|
||||
export const ProjectCard = ({
|
||||
className,
|
||||
project,
|
||||
@@ -38,14 +44,24 @@ export const ProjectCard = ({
|
||||
// TODO: Update this to use the actual status from the API
|
||||
const hasError = status === 'failure';
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOptionsClick = (
|
||||
e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>,
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
navigate(`projects/${project.id}`);
|
||||
}, [project.id, navigate]);
|
||||
|
||||
return (
|
||||
<div {...props} className={theme.wrapper({ className })}>
|
||||
<div
|
||||
{...props}
|
||||
className={theme.wrapper({ className })}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Upper content */}
|
||||
<div className={theme.upperContent()}>
|
||||
{/* Icon container */}
|
||||
@@ -54,14 +70,13 @@ export const ProjectCard = ({
|
||||
imageSrc={project.icon}
|
||||
initials={getInitials(project.name)}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* Title and website */}
|
||||
<Link to={`projects/${project.id}`} className={theme.content()}>
|
||||
<div className={theme.content()}>
|
||||
<p className={theme.title()}>{project.name}</p>
|
||||
<p className={theme.description()}>
|
||||
{project.deployments[0]?.domain?.name ?? 'No domain'}
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
{/* Icons */}
|
||||
<div className={theme.icons()}>
|
||||
{hasError && <WarningDiamondIcon className="text-elements-danger" />}
|
||||
@@ -87,10 +102,7 @@ export const ProjectCard = ({
|
||||
</div>
|
||||
</div>
|
||||
{/* Wave */}
|
||||
<WavyBorder
|
||||
variant="stroke-and-fill"
|
||||
className="bg-surface-card-hovered"
|
||||
/>
|
||||
<WavyBorder variant="stroke-and-fill" className={theme.wavyBorder()} />
|
||||
{/* Lower content */}
|
||||
<div className={theme.lowerContent()}>
|
||||
{/* Latest deployment */}
|
||||
|
||||
+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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import { GitCommitWithBranch } from 'types';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import ActivityCard from './ActivityCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
|
||||
export const Activity = ({
|
||||
activities,
|
||||
}: {
|
||||
activities: GitCommitWithBranch[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="col-span-2 mr-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||
<Button variant="tertiary" size="sm">
|
||||
See all
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
{activities.map((activity, index) => {
|
||||
return <ActivityCard activity={activity} key={`activity-${index}`} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GitCommitWithBranch } from '../../../types';
|
||||
import { GitCommitWithBranch } from '../../../../../types';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ArrowRightCircleFilledIcon,
|
||||
BranchIcon,
|
||||
BranchStrokeIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
@@ -50,7 +50,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
<span className="w-0.5 h-0.5 rounded-full bg-border-interactive-hovered"></span>
|
||||
<span className="flex justify-center items-center gap-1.5">
|
||||
<div>
|
||||
<BranchIcon />
|
||||
<BranchStrokeIcon className="w-3 h-3" />
|
||||
</div>
|
||||
<span
|
||||
title={activity.branch.name}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Activity';
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { cloneElement } from 'utils/cloneElement';
|
||||
|
||||
interface OverviewInfoProps {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export const OverviewInfo = ({
|
||||
label,
|
||||
icon,
|
||||
children,
|
||||
}: PropsWithChildren<OverviewInfoProps>) => {
|
||||
const styledIcon = cloneElement({
|
||||
element: icon,
|
||||
className: 'w-4 h-4',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-2 py-3 text-sm items-center">
|
||||
<div className="flex gap-2 items-center text-elements-high-em">
|
||||
{styledIcon}
|
||||
{label}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const BranchStrokeIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4.66667 4.99984C5.58714 4.99984 6.33333 4.25365 6.33333 3.33317C6.33333 2.4127 5.58714 1.6665 4.66667 1.6665C3.74619 1.6665 3 2.4127 3 3.33317C3 4.25365 3.74619 4.99984 4.66667 4.99984ZM4.66667 4.99984V10.9998M4.66667 10.9998C3.74619 10.9998 3 11.746 3 12.6665C3 13.587 3.74619 14.3332 4.66667 14.3332C5.58714 14.3332 6.33333 13.587 6.33333 12.6665C6.33333 11.746 5.58714 10.9998 4.66667 10.9998ZM4.66667 10.9998V9.33317C4.66667 8.59679 5.26362 7.99984 6 7.99984H10C10.7364 7.99984 11.3333 7.40288 11.3333 6.6665V4.99984M11.3333 4.99984C12.2538 4.99984 13 4.25365 13 3.33317C13 2.4127 12.2538 1.6665 11.3333 1.6665C10.4129 1.6665 9.66667 2.4127 9.66667 3.33317C9.66667 4.25365 10.4129 4.99984 11.3333 4.99984Z"
|
||||
stroke="currentColor"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CalendarDaysIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M2.3335 5.00016H13.6668M4.33349 13.6668H11.6668C12.7714 13.6668 13.6668 12.7714 13.6668 11.6668V4.3335C13.6668 3.22893 12.7714 2.3335 11.6668 2.3335H4.3335C3.22893 2.3335 2.3335 3.22893 2.3335 4.33349V11.6668C2.3335 12.7714 3.22893 13.6668 4.33349 13.6668Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.6665 7.8335C4.6665 7.55735 4.89036 7.3335 5.1665 7.3335C5.44265 7.3335 5.6665 7.55735 5.6665 7.8335C5.6665 8.10964 5.44265 8.3335 5.1665 8.3335C4.89036 8.3335 4.6665 8.10964 4.6665 7.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.6665 10.8335C4.6665 10.5574 4.89036 10.3335 5.1665 10.3335C5.44265 10.3335 5.6665 10.5574 5.6665 10.8335C5.6665 11.1096 5.44265 11.3335 5.1665 11.3335C4.89036 11.3335 4.6665 11.1096 4.6665 10.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.49984 10.8335C7.49984 10.5574 7.72369 10.3335 7.99984 10.3335C8.27598 10.3335 8.49984 10.5574 8.49984 10.8335C8.49984 11.1096 8.27598 11.3335 7.99984 11.3335C7.72369 11.3335 7.49984 11.1096 7.49984 10.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.49984 7.8335C7.49984 7.55735 7.72369 7.3335 7.99984 7.3335C8.27598 7.3335 8.49984 7.55735 8.49984 7.8335C8.49984 8.10964 8.27598 8.3335 7.99984 8.3335C7.72369 8.3335 7.49984 8.10964 7.49984 7.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M10.3332 7.8335C10.3332 7.55735 10.557 7.3335 10.8332 7.3335C11.1093 7.3335 11.3332 7.55735 11.3332 7.8335C11.3332 8.10964 11.1093 8.3335 10.8332 8.3335C10.557 8.3335 10.3332 8.10964 10.3332 7.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -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,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CursorBoxIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M13.5 7V3.83333C13.5 3.09695 12.903 2.5 12.1667 2.5H3.83333C3.09695 2.5 2.5 3.09695 2.5 3.83333V12.1667C2.5 12.903 3.09695 13.5 3.83333 13.5H7M9.43391 14.0852L7.5218 7.9391C7.44202 7.68269 7.68269 7.44202 7.9391 7.5218L14.0852 9.43391C14.3658 9.52122 14.4043 9.90262 14.1468 10.0443L11.5848 11.4534C11.5294 11.4838 11.4838 11.5294 11.4534 11.5848L10.0443 14.1468C9.90262 14.4043 9.52122 14.3658 9.43391 14.0852Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.05263"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const GithubStrokeIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M13.0194 4.73001C13.1722 4.23701 13.2212 3.71768 13.1634 3.20479C13.1055 2.6919 12.942 2.19656 12.6831 1.75001C12.6392 1.67398 12.5761 1.61085 12.5001 1.56697C12.424 1.52308 12.3378 1.49999 12.25 1.50001C11.6676 1.49879 11.093 1.6338 10.5721 1.89425C10.0512 2.1547 9.59845 2.53337 9.25 3.00001H7.75C7.40155 2.53337 6.94878 2.1547 6.42788 1.89425C5.90698 1.6338 5.33238 1.49879 4.75 1.50001C4.66221 1.49999 4.57597 1.52308 4.49994 1.56697C4.4239 1.61085 4.36077 1.67398 4.31687 1.75001C4.05803 2.19656 3.89452 2.6919 3.83664 3.20479C3.77877 3.71768 3.8278 4.23701 3.98062 4.73001C3.6717 5.26921 3.50623 5.87862 3.5 6.50001V7.00001C3.50105 7.84601 3.80817 8.66306 4.36464 9.30029C4.92111 9.93752 5.68935 10.3519 6.5275 10.4669C6.18538 10.9047 5.99968 11.4444 6 12V12.5H4.5C4.10218 12.5 3.72064 12.342 3.43934 12.0607C3.15804 11.7794 3 11.3978 3 11C3 10.6717 2.93534 10.3466 2.8097 10.0433C2.68406 9.73999 2.49991 9.46439 2.26777 9.23224C2.03562 9.00009 1.76002 8.81595 1.45671 8.69031C1.15339 8.56467 0.828305 8.50001 0.5 8.50001C0.367392 8.50001 0.240215 8.55269 0.146447 8.64645C0.0526784 8.74022 0 8.8674 0 9.00001C0 9.13262 0.0526784 9.25979 0.146447 9.35356C0.240215 9.44733 0.367392 9.50001 0.5 9.50001C0.897825 9.50001 1.27936 9.65804 1.56066 9.93935C1.84196 10.2207 2 10.6022 2 11C2 11.663 2.26339 12.2989 2.73223 12.7678C3.20107 13.2366 3.83696 13.5 4.5 13.5H6V14.5C6 14.6326 6.05268 14.7598 6.14645 14.8536C6.24021 14.9473 6.36739 15 6.5 15C6.63261 15 6.75979 14.9473 6.85355 14.8536C6.94732 14.7598 7 14.6326 7 14.5V12C7 11.6022 7.15804 11.2207 7.43934 10.9393C7.72064 10.658 8.10218 10.5 8.5 10.5C8.89782 10.5 9.27936 10.658 9.56066 10.9393C9.84196 11.2207 10 11.6022 10 12V14.5C10 14.6326 10.0527 14.7598 10.1464 14.8536C10.2402 14.9473 10.3674 15 10.5 15C10.6326 15 10.7598 14.9473 10.8536 14.8536C10.9473 14.7598 11 14.6326 11 14.5V12C11.0003 11.4444 10.8146 10.9047 10.4725 10.4669C11.3107 10.3519 12.0789 9.93752 12.6354 9.30029C13.1918 8.66306 13.4989 7.84601 13.5 7.00001V6.50001C13.4938 5.87862 13.3283 5.26921 13.0194 4.73001ZM12.5 7.00001C12.5 7.66305 12.2366 8.29893 11.7678 8.76778C11.2989 9.23662 10.663 9.50001 10 9.50001H7C6.33696 9.50001 5.70107 9.23662 5.23223 8.76778C4.76339 8.29893 4.5 7.66305 4.5 7.00001V6.50001C4.50613 6.00002 4.65582 5.51233 4.93125 5.09501C4.98259 5.02733 5.01585 4.94769 5.02788 4.8636C5.03991 4.77951 5.03031 4.69374 5 4.61438C4.86976 4.27851 4.80709 3.92023 4.81556 3.56009C4.82403 3.19995 4.90349 2.84501 5.04938 2.51563C5.45857 2.55965 5.85262 2.6952 6.2023 2.91224C6.55198 3.12928 6.84832 3.42223 7.06938 3.76938C7.11442 3.83982 7.17641 3.89784 7.24968 3.93813C7.32294 3.97842 7.40514 3.9997 7.48875 4.00001H9.51062C9.59455 4.00001 9.67713 3.97889 9.75075 3.93858C9.82437 3.89828 9.88666 3.84009 9.93188 3.76938C10.1529 3.4222 10.4492 3.12922 10.7989 2.91218C11.1486 2.69514 11.5427 2.55961 11.9519 2.51563C12.0976 2.8451 12.1768 3.20007 12.1851 3.56021C12.1933 3.92036 12.1304 4.27859 12 4.61438C11.9698 4.69298 11.9597 4.77788 11.9706 4.86138C11.9815 4.94487 12.0131 5.02432 12.0625 5.09251C12.3407 5.50984 12.4926 5.99853 12.5 6.50001V7.00001Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LinkIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.99634 4.3125C5.99634 4.00184 6.24818 3.75 6.55884 3.75H12.1875C13.3266 3.75 14.25 4.67341 14.25 5.8125V12.1844C14.25 12.495 13.9982 12.7469 13.6875 12.7469C13.3769 12.7469 13.125 12.495 13.125 12.1844V5.8125C13.125 5.76745 13.1218 5.72315 13.1157 5.6798L5.27275 13.5227C5.05308 13.7424 4.69692 13.7424 4.47725 13.5227C4.25758 13.3031 4.25758 12.9469 4.47725 12.7273L12.3202 4.88432C12.2768 4.87818 12.2326 4.875 12.1875 4.875H6.55884C6.24818 4.875 5.99634 4.62316 5.99634 4.3125Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</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 StorageIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M13 3.42576C13 4.39738 10.7614 5.18502 8 5.18502C5.23858 5.18502 3 4.39738 3 3.42576M13 3.42576C13 2.45415 10.7614 1.6665 8 1.6665C5.23858 1.6665 3 2.45415 3 3.42576M13 3.42576V12.5739C13 13.5455 10.7614 14.3332 8 14.3332C5.23858 14.3332 3 13.5455 3 12.5739V3.42576M13 7.92562C13 8.89723 10.7614 9.68488 8 9.68488C5.23858 9.68488 3 8.89723 3 7.92562"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -36,6 +36,15 @@ export * from './WarningDiamondIcon';
|
||||
export * from './ArrowRightCircleIcon';
|
||||
export * from './ClockOutlineIcon';
|
||||
export * from './ArrowRightCircleFilledIcon';
|
||||
export * from './GithubStrokeIcon';
|
||||
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';
|
||||
|
||||
@@ -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';
|
||||
@@ -8,7 +8,6 @@ export const tabsTheme = tv({
|
||||
triggerWrapper: [
|
||||
// Horizontal – default
|
||||
'px-1',
|
||||
'pb-5',
|
||||
'cursor-default',
|
||||
'select-none',
|
||||
'text-elements-low-em',
|
||||
@@ -55,9 +54,13 @@ export const tabsTheme = tv({
|
||||
'outline-none',
|
||||
'leading-none',
|
||||
'tracking-[-0.006em]',
|
||||
'text-sm',
|
||||
'rounded-md',
|
||||
// Horizontal – default
|
||||
'data-[orientation=horizontal]:focus-ring',
|
||||
'data-[orientation=horizontal]:h-10',
|
||||
// select direct child of data-[orientation=horizontal]
|
||||
'[&[data-orientation=horizontal]_>_*]:h-full',
|
||||
// Vertical
|
||||
'data-[orientation=vertical]:gap-2',
|
||||
'data-[orientation=vertical]:justify-start',
|
||||
|
||||
@@ -4,26 +4,26 @@ import type { VariantProps } from 'tailwind-variants';
|
||||
export const tagTheme = tv(
|
||||
{
|
||||
slots: {
|
||||
wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'],
|
||||
icon: ['h-4', 'w-4'],
|
||||
wrapper: ['inline-flex', 'gap-1.5', 'rounded-lg', 'border'],
|
||||
icon: [],
|
||||
label: ['font-inter', 'text-xs'],
|
||||
},
|
||||
variants: {
|
||||
type: {
|
||||
attention: {
|
||||
icon: ['text-elements-warning'],
|
||||
wrapper: ['text-elements-warning'],
|
||||
},
|
||||
negative: {
|
||||
icon: ['text-elements-danger'],
|
||||
wrapper: ['text-elements-danger'],
|
||||
},
|
||||
positive: {
|
||||
icon: ['text-elements-success'],
|
||||
wrapper: ['text-elements-success'],
|
||||
},
|
||||
emphasized: {
|
||||
icon: ['text-elements-on-secondary'],
|
||||
wrapper: ['text-elements-on-secondary'],
|
||||
},
|
||||
neutral: {
|
||||
icon: ['text-elements-mid-em'],
|
||||
wrapper: ['text-elements-mid-em'],
|
||||
},
|
||||
},
|
||||
style: {
|
||||
@@ -36,9 +36,11 @@ export const tagTheme = tv(
|
||||
size: {
|
||||
sm: {
|
||||
wrapper: ['px-2', 'py-2'],
|
||||
icon: ['h-4', 'w-4'],
|
||||
},
|
||||
xs: {
|
||||
wrapper: ['px-2', 'py-1.5'],
|
||||
wrapper: ['px-2', 'py-1'],
|
||||
icon: ['h-3', 'w-3'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,6 +27,8 @@ export const Tag = ({
|
||||
type = 'attention',
|
||||
style = 'default',
|
||||
size = 'sm',
|
||||
className,
|
||||
...props
|
||||
}: TagProps) => {
|
||||
const {
|
||||
wrapper: wrapperCls,
|
||||
@@ -51,7 +53,7 @@ export const Tag = ({
|
||||
}, [cloneIcon, iconCls, rightIcon]);
|
||||
|
||||
return (
|
||||
<div className={wrapperCls()}>
|
||||
<div className={wrapperCls({ className })} {...props}>
|
||||
{renderLeftIcon}
|
||||
<p className={labelCls()}>{children}</p>
|
||||
{renderRightIcon}
|
||||
|
||||
@@ -8,18 +8,13 @@ import {
|
||||
} from 'react-router-dom';
|
||||
import { Project as ProjectType } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Tab,
|
||||
Tabs,
|
||||
TabsBody,
|
||||
TabsHeader,
|
||||
Typography,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import HorizontalLine from '../../../components/HorizontalLine';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import { useOctokit } from '../../../context/OctokitContext';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { ChevronLeft } from 'components/shared/CustomIcon';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Tabs } from 'components/shared/Tabs';
|
||||
|
||||
const Id = () => {
|
||||
const { id } = useParams();
|
||||
@@ -69,96 +64,61 @@ const Id = () => {
|
||||
<div className="h-full">
|
||||
{project ? (
|
||||
<>
|
||||
<div className="flex p-4 gap-4 items-center">
|
||||
<Button
|
||||
variant="outlined"
|
||||
className="rounded-full"
|
||||
onClick={() => navigate(-1)}
|
||||
placeholder={''}
|
||||
>
|
||||
{'<'}
|
||||
</Button>
|
||||
<Typography variant="h3" className="grow" placeholder={''}>
|
||||
{project?.name}
|
||||
</Typography>
|
||||
<Link to={repoUrl} target="_blank">
|
||||
<div className="px-6 py-4 flex justify-between items-center gap-4">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button
|
||||
className="rounded-full"
|
||||
variant="outlined"
|
||||
placeholder={''}
|
||||
>
|
||||
Open Repo
|
||||
</Button>
|
||||
</Link>
|
||||
<Button className="rounded-full" color="blue" placeholder={''}>
|
||||
Go to app
|
||||
</Button>
|
||||
variant="tertiary"
|
||||
className="rounded-full h-11 w-11 p-0"
|
||||
aria-label="Go back"
|
||||
leftIcon={<ChevronLeft />}
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
<Heading className="text-2xl font-medium">
|
||||
{project?.name}
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Link to={repoUrl} target="_blank">
|
||||
<Button className="h-11 transition-colors" variant="tertiary">
|
||||
Open repo
|
||||
</Button>
|
||||
</Link>
|
||||
<Button className="h-11 transition-colors">Go to app</Button>
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalLine />
|
||||
<div className="p-4">
|
||||
<Tabs value={currentTab}>
|
||||
<TabsHeader
|
||||
className="rounded-none border-b border-blue-gray-50 bg-transparent p-0"
|
||||
indicatorProps={{
|
||||
className:
|
||||
'bg-transparent border-b-2 border-gray-900 shadow-none rounded-none',
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
<Link to="">
|
||||
<Tab
|
||||
value=""
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Overview
|
||||
</Tab>
|
||||
</Link>
|
||||
<Link to="deployments">
|
||||
<Tab
|
||||
value="deployments"
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Deployments
|
||||
</Tab>
|
||||
</Link>
|
||||
<Link to="database">
|
||||
<Tab
|
||||
value="database"
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Database
|
||||
</Tab>
|
||||
</Link>
|
||||
<Link to="integrations">
|
||||
<Tab
|
||||
value="integrations"
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Integrations
|
||||
</Tab>
|
||||
</Link>
|
||||
<Link to="settings">
|
||||
<Tab
|
||||
value="settings"
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Settings
|
||||
</Tab>
|
||||
</Link>
|
||||
</TabsHeader>
|
||||
<TabsBody placeholder={''}>
|
||||
<WavyBorder />
|
||||
<div className="px-6 ">
|
||||
<Tabs value={currentTab} className="flex-col pt-6">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="">
|
||||
<Link to="">Overview</Link>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="deployments">
|
||||
<Link to="deployments">Deployments</Link>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="database">
|
||||
<Link to="database">Database</Link>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="integrations">
|
||||
<Link to="integrations">Integrations</Link>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings">
|
||||
<Link to="settings">Settings</Link>
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
{/* Not wrapping in Tab.Content because we are using Outlet */}
|
||||
<div className="py-7">
|
||||
<Outlet context={{ project, onUpdate }} />
|
||||
</TabsBody>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<h4>Project not found</h4>
|
||||
<div className="grid place-items-center h-[calc(100vh-174px)]">
|
||||
<Heading as="h4" className="text-2xl font-medium">
|
||||
Project not found.
|
||||
</Heading>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import templates from 'assets/templates';
|
||||
import { RepositoryList } from 'components/projects/create/RepositoryList';
|
||||
import RepositoryList from 'components/projects/create/RepositoryList';
|
||||
import ConnectAccount from 'components/projects/create/ConnectAccount';
|
||||
import { useOctokit } from 'context/OctokitContext';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Domain, DomainStatus } from 'gql-client';
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { RequestError } from 'octokit';
|
||||
|
||||
import { Typography, Chip, Avatar, Tooltip } from '@material-tailwind/react';
|
||||
|
||||
import ActivityCard from '../../../../components/projects/project/ActivityCard';
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import { useOctokit } from '../../../../context/OctokitContext';
|
||||
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import {
|
||||
BranchStrokeIcon,
|
||||
CheckRoundFilledIcon,
|
||||
ClockIcon,
|
||||
CursorBoxIcon,
|
||||
GithubStrokeIcon,
|
||||
GlobeIcon,
|
||||
LinkIcon,
|
||||
StorageIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import { Activity } from 'components/projects/project/overview/Activity';
|
||||
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
|
||||
import { CalendarDaysIcon } from 'components/shared/CustomIcon/CalendarDaysIcon';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
|
||||
const COMMITS_PER_PAGE = 4;
|
||||
|
||||
@@ -103,92 +115,109 @@ const OverviewTabPanel = () => {
|
||||
}, [project]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-5 gap-[72px] mt-7">
|
||||
<div className="grid grid-cols-5 gap-[72px]">
|
||||
<div className="col-span-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Avatar
|
||||
src={project.icon || '/gray.png'}
|
||||
variant="rounded"
|
||||
placeholder={''}
|
||||
size={48}
|
||||
initials={getInitials(project.name)}
|
||||
imageSrc={project.icon}
|
||||
type="blue"
|
||||
/>
|
||||
<div className="grow">
|
||||
<Typography placeholder={''}>{project.name}</Typography>
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
<div className="flex-1 space-y-1">
|
||||
<Heading className="text-lg leading-6 font-medium">
|
||||
{project.name}
|
||||
</Heading>
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
{project.subDomain}
|
||||
</Typography>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 text-sm items-center">
|
||||
<div>^ Domain</div>
|
||||
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
|
||||
{liveDomain ? (
|
||||
<Chip
|
||||
className="normal-case ml-6 inline font-normal"
|
||||
size="sm"
|
||||
value="Connected"
|
||||
icon="^"
|
||||
color="green"
|
||||
/>
|
||||
<Tag type="positive" size="xs" leftIcon={<CheckRoundFilledIcon />}>
|
||||
Connected
|
||||
</Tag>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<Chip
|
||||
className="normal-case inline font-normal mx-2"
|
||||
size="sm"
|
||||
value="Not connected"
|
||||
icon="^"
|
||||
color="orange"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag type="attention" size="xs" leftIcon={<ClockIcon />}>
|
||||
Not connected
|
||||
</Tag>
|
||||
<Button
|
||||
className="normal-case rounded-full"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigate('settings/domains');
|
||||
}}
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
>
|
||||
Setup
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</OverviewInfo>
|
||||
{project.deployments.length !== 0 ? (
|
||||
<>
|
||||
<div className="flex justify-between p-2 text-sm">
|
||||
<p>^ Source</p>
|
||||
<p>^ {project.deployments[0]?.branch}</p>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 text-sm">
|
||||
<p>^ Deployment</p>
|
||||
<p className="text-blue-600">{liveDomain?.name}</p>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 text-sm">
|
||||
<p>^ Created</p>
|
||||
<p>
|
||||
{relativeTimeMs(project.deployments[0].createdAt)} by ^{' '}
|
||||
<Tooltip content={project.deployments[0].createdBy.name}>
|
||||
{formatAddress(project.deployments[0].createdBy.name ?? '')}
|
||||
</Tooltip>
|
||||
</p>
|
||||
</div>
|
||||
{/* SOURCE */}
|
||||
<OverviewInfo label="Source" icon={<GithubStrokeIcon />}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<BranchStrokeIcon className="text-elements-low-em w-4 h-5" />
|
||||
<span className="text-elements-high-em text-sm tracking-tighter">
|
||||
{project.deployments[0]?.branch}
|
||||
</span>
|
||||
</div>
|
||||
</OverviewInfo>
|
||||
|
||||
{/* DATABASE */}
|
||||
<OverviewInfo label="Database" icon={<StorageIcon />}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Link to="#">
|
||||
<span className="group text-controls-primary hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
|
||||
{/* // TODO: add db name
|
||||
dbname
|
||||
*/}
|
||||
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</OverviewInfo>
|
||||
|
||||
{/* DEPLOYMENT */}
|
||||
<OverviewInfo label="Deployment URL" icon={<CursorBoxIcon />}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Link to="#">
|
||||
<span className="text-controls-primary group hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
|
||||
{liveDomain?.name}{' '}
|
||||
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</OverviewInfo>
|
||||
|
||||
{/* DEPLOYMENT DATE */}
|
||||
<OverviewInfo label="Deployment date" icon={<CalendarDaysIcon />}>
|
||||
<div className="flex gap-2 items-center text-elements-high-em text-sm tracking-tighter">
|
||||
<span>{relativeTimeMs(project.deployments[0].createdAt)}</span>
|
||||
by
|
||||
<Avatar
|
||||
// TODO: add imageSrc
|
||||
// imageSrc={project.deployments[0]?.createdBy.avatar}
|
||||
initials={getInitials(
|
||||
project.deployments[0]?.createdBy?.name ?? '',
|
||||
)}
|
||||
className="rounded-full"
|
||||
size={24}
|
||||
/>
|
||||
<span>{project.deployments[0]?.createdBy?.name}</span>
|
||||
</div>
|
||||
</OverviewInfo>
|
||||
</>
|
||||
) : (
|
||||
<div>No current deployment found</div>
|
||||
<p className="text-elements-low-em text-sm py-3">
|
||||
No current deployment found.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-2 mr-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||
<Button variant="tertiary" size="sm">
|
||||
See all
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
{activities.map((activity, index) => {
|
||||
return (
|
||||
<ActivityCard activity={activity} key={`activity-${index}`} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Activity activities={activities} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, {
|
||||
ReactElement,
|
||||
isValidElement,
|
||||
Children,
|
||||
cloneElement as reactCloneElement,
|
||||
HTMLProps,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { ClassProp } from 'tailwind-variants';
|
||||
import { cn } from './classnames';
|
||||
|
||||
interface cloneElement extends HTMLProps<ReactNode> {
|
||||
element: ReactNode;
|
||||
themeStyle?: (props: ClassProp) => string;
|
||||
}
|
||||
|
||||
export const cloneElement = ({
|
||||
element,
|
||||
themeStyle,
|
||||
className,
|
||||
...props
|
||||
}: cloneElement) => {
|
||||
if (isValidElement(element)) {
|
||||
return (
|
||||
<>
|
||||
{Children.map(element, (child) => {
|
||||
const originalClassName = (child.props as HTMLProps<ReactNode>)
|
||||
?.className;
|
||||
|
||||
return reactCloneElement(child as ReactElement, {
|
||||
className: themeStyle
|
||||
? themeStyle({
|
||||
className: cn(originalClassName, className), // overriding icon classNames
|
||||
})
|
||||
: originalClassName,
|
||||
...props,
|
||||
});
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
@@ -12,6 +12,9 @@ export default withMT({
|
||||
zIndex: {
|
||||
tooltip: '52',
|
||||
},
|
||||
letterSpacing: {
|
||||
tight: '-0.084px',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
display: ['Inter Display', 'sans-serif'],
|
||||
|
||||
Reference in New Issue
Block a user