From 409b654f9b1757de32639127ecd75c2275bf7cf3 Mon Sep 17 00:00:00 2001
From: Sushan Yadav <53527664+sushanyadav@users.noreply.github.com>
Date: Fri, 1 Mar 2024 10:05:50 +0545
Subject: [PATCH] Project Deployments - Deployed line items (#147)
* feat: add deployment lines
* fix: typo DeploymentMenu
---
.vscode/settings.json | 3 +-
.../deployments/DeploymentDetailsCard.tsx | 349 ++++++------------
.../project/deployments/DeploymentMenu.tsx | 268 ++++++++++++++
.../project/deployments/FilterForm.tsx | 2 +-
.../shared/CustomIcon/CommitIcon.tsx | 26 ++
.../shared/CustomIcon/RefreshIcon.tsx | 41 ++
.../shared/CustomIcon/RocketIcon.tsx | 20 +
.../components/shared/CustomIcon/UndoIcon.tsx | 21 ++
.../src/components/shared/CustomIcon/index.ts | 4 +
.../shared/OverflownText/OverflownText.tsx | 71 ++++
.../components/shared/OverflownText/index.ts | 1 +
.../src/components/shared/Tag/Tag.theme.ts | 2 +-
.../org-slug/projects/id/Deployments.tsx | 8 +-
packages/frontend/tailwind.config.js | 3 +
14 files changed, 566 insertions(+), 253 deletions(-)
create mode 100644 packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx
create mode 100644 packages/frontend/src/components/shared/CustomIcon/CommitIcon.tsx
create mode 100644 packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx
create mode 100644 packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx
create mode 100644 packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx
create mode 100644 packages/frontend/src/components/shared/OverflownText/OverflownText.tsx
create mode 100644 packages/frontend/src/components/shared/OverflownText/index.ts
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f3e08235..c831fde1 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,7 @@
{
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
- ["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
+ "tv\\('([^)]*)\\')",
+ "(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
]
}
diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx
index e191fe8c..c4940bac 100644
--- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx
+++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx
@@ -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 ;
}
- };
-
- 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 ;
}
- };
- 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 ;
}
};
return (
-
-
-
- {deployment.url && (
-
+
+
+ {/* DEPLOYMENT URL */}
+ {deployment.url && (
+
+
{deployment.url}
-
- )}
-
-
+
+
+ )}
+
{deployment.environment === Environment.Production
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
: 'Preview'}
-
+
-
-
^}
+
+ {/* DEPLOYMENT STATUS */}
+
+
+ {deployment.status}
+
+
+
+ {/* DEPLOYMENT COMMIT DETAILS */}
+
+
+
+ {deployment.branch}
+
+
+
+
+ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
+ {deployment.commitMessage}
+
+
+
+
+ {/* DEPLOYMENT INFOs */}
+
+
+
+
+ {relativeTimeMs(deployment.createdAt)}
+
+
+
+
+ {formatAddress(deployment.createdBy.name ?? '')}
+
+
+
-
-
- ^ {deployment.branch}
-
-
- ^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
- {deployment.commitMessage}
-
-
-
-
- ^ {relativeTimeMs(deployment.createdAt)} ^{' '}
-
- {formatAddress(deployment.createdBy.name ?? '')}
-
-
-
-
- ...
-
-
-
-
- ^ Visit
-
-
- setAssignDomainDialog(!assignDomainDialog)}
- placeholder={''}
- >
- ^ Assign domain
-
- setChangeToProduction(!changeToProduction)}
- disabled={!(deployment.environment !== Environment.Production)}
- placeholder={''}
- >
- ^ Change to production
-
-
- setRedeployToProduction(!redeployToProduction)}
- disabled={
- !(
- deployment.environment === Environment.Production &&
- deployment.isCurrent
- )
- }
- placeholder={''}
- >
- ^ Redeploy to production
-
- setRollbackDeployment(!rollbackDeployment)}
- disabled={
- deployment.isCurrent ||
- deployment.environment !== Environment.Production ||
- !Boolean(currentDeployment)
- }
- placeholder={''}
- >
- ^ Rollback to this version
-
-
-
-
- setChangeToProduction((preVal) => !preVal)}
- open={changeToProduction}
- confirmButtonTitle="Change"
- color="blue"
- handleConfirm={async () => {
- await updateDeployment();
- setChangeToProduction((preVal) => !preVal);
- }}
- >
-
-
- Upon confirmation, this deployment will be changed to production.
-
-
-
- The new deployment will be associated with these domains:
-
- {prodBranchDomains.length > 0 &&
- prodBranchDomains.map((value) => {
- return (
-
- ^ {value.name}
-
- );
- })}
-
-
- setRedeployToProduction((preVal) => !preVal)}
- open={redeployToProduction}
- confirmButtonTitle="Redeploy"
- color="blue"
- handleConfirm={async () => {
- await redeployToProd();
- setRedeployToProduction((preVal) => !preVal);
- }}
- >
-
-
- Upon confirmation, new deployment will be created with the same
- source code as current deployment.
-
-
-
- These domains will point to your new deployment:
-
- {deployment.domain?.name && (
-
- {deployment.domain?.name}
-
- )}
-
-
- {Boolean(currentDeployment) && (
- setRollbackDeployment((preVal) => !preVal)}
- open={rollbackDeployment}
- confirmButtonTitle="Rollback"
- color="blue"
- handleConfirm={async () => {
- await rollbackDeploymentHandler();
- setRollbackDeployment((preVal) => !preVal);
- }}
- >
-
-
- Upon confirmation, this deployment will replace your current
- deployment
-
-
-
-
- These domains will point to your new deployment:
-
-
- ^ {currentDeployment.domain?.name}
-
-
-
- )}
- setAssignDomainDialog(!assignDomainDialog)}
- />
);
};
diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx
new file mode 100644
index 00000000..f4eb9808
--- /dev/null
+++ b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx
@@ -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;
+ 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 (
+ <>
+
+
+
+ }
+ aria-label="Toggle Menu"
+ />
+
+
+
+
+ Visit
+
+
+ setAssignDomainDialog(!assignDomainDialog)}
+ placeholder={''}
+ >
+ Assign domain
+
+ setChangeToProduction(!changeToProduction)}
+ disabled={!(deployment.environment !== Environment.Production)}
+ placeholder={''}
+ >
+ Change to production
+
+
+ setRedeployToProduction(!redeployToProduction)}
+ disabled={
+ !(
+ deployment.environment === Environment.Production &&
+ deployment.isCurrent
+ )
+ }
+ placeholder={''}
+ >
+ Redeploy to production
+
+ setRollbackDeployment(!rollbackDeployment)}
+ disabled={
+ deployment.isCurrent ||
+ deployment.environment !== Environment.Production ||
+ !Boolean(currentDeployment)
+ }
+ placeholder={''}
+ >
+ Rollback to this version
+
+
+
+
+ {/* Dialogs */}
+ setChangeToProduction((preVal) => !preVal)}
+ open={changeToProduction}
+ confirmButtonTitle="Change"
+ color="blue"
+ handleConfirm={async () => {
+ await updateDeployment();
+ setChangeToProduction((preVal) => !preVal);
+ }}
+ >
+
+
+ Upon confirmation, this deployment will be changed to production.
+
+
+
+ The new deployment will be associated with these domains:
+
+ {prodBranchDomains.length > 0 &&
+ prodBranchDomains.map((value) => {
+ return (
+
+ ^ {value.name}
+
+ );
+ })}
+
+
+ setRedeployToProduction((preVal) => !preVal)}
+ open={redeployToProduction}
+ confirmButtonTitle="Redeploy"
+ color="blue"
+ handleConfirm={async () => {
+ await redeployToProd();
+ setRedeployToProduction((preVal) => !preVal);
+ }}
+ >
+
+
+ Upon confirmation, new deployment will be created with the same
+ source code as current deployment.
+
+
+
+ These domains will point to your new deployment:
+
+ {deployment.domain?.name && (
+
+ {deployment.domain?.name}
+
+ )}
+
+
+ {Boolean(currentDeployment) && (
+ setRollbackDeployment((preVal) => !preVal)}
+ open={rollbackDeployment}
+ confirmButtonTitle="Rollback"
+ color="blue"
+ handleConfirm={async () => {
+ await rollbackDeploymentHandler();
+ setRollbackDeployment((preVal) => !preVal);
+ }}
+ >
+
+
+ Upon confirmation, this deployment will replace your current
+ deployment
+
+
+
+
+ These domains will point to your new deployment:
+
+
+ ^ {currentDeployment.domain?.name}
+
+
+
+ )}
+ setAssignDomainDialog(!assignDomainDialog)}
+ />
+ >
+ );
+};
diff --git a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx
index c28c6218..dd549c57 100644
--- a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx
+++ b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx
@@ -44,7 +44,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
}, [value]);
return (
-
+
{
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx
new file mode 100644
index 00000000..3873c371
--- /dev/null
+++ b/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { CustomIcon, CustomIconProps } from './CustomIcon';
+
+export const RefreshIcon = (props: CustomIconProps) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx
new file mode 100644
index 00000000..488fba8f
--- /dev/null
+++ b/packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { CustomIcon, CustomIconProps } from './CustomIcon';
+
+export const RocketIcon = (props: CustomIconProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx
new file mode 100644
index 00000000..713a0b6e
--- /dev/null
+++ b/packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { CustomIcon, CustomIconProps } from './CustomIcon';
+
+export const UndoIcon = (props: CustomIconProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/frontend/src/components/shared/CustomIcon/index.ts b/packages/frontend/src/components/shared/CustomIcon/index.ts
index b9965188..61239304 100644
--- a/packages/frontend/src/components/shared/CustomIcon/index.ts
+++ b/packages/frontend/src/components/shared/CustomIcon/index.ts
@@ -41,6 +41,10 @@ export * from './BranchStrokeIcon';
export * from './StorageIcon';
export * from './LinkIcon';
export * from './CursorBoxIcon';
+export * from './CommitIcon';
+export * from './RocketIcon';
+export * from './RefreshIcon';
+export * from './UndoIcon';
// Templates
export * from './templates';
diff --git a/packages/frontend/src/components/shared/OverflownText/OverflownText.tsx b/packages/frontend/src/components/shared/OverflownText/OverflownText.tsx
new file mode 100644
index 00000000..b03ea580
--- /dev/null
+++ b/packages/frontend/src/components/shared/OverflownText/OverflownText.tsx
@@ -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 = ({
+ tooltipProps,
+ children,
+ content,
+ className,
+ as,
+ ...props
+}: PropsWithChildren>) => {
+ 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 (
+
+
+ {children}
+
+
+ );
+};
diff --git a/packages/frontend/src/components/shared/OverflownText/index.ts b/packages/frontend/src/components/shared/OverflownText/index.ts
new file mode 100644
index 00000000..1ffb16f2
--- /dev/null
+++ b/packages/frontend/src/components/shared/OverflownText/index.ts
@@ -0,0 +1 @@
+export * from './OverflownText';
diff --git a/packages/frontend/src/components/shared/Tag/Tag.theme.ts b/packages/frontend/src/components/shared/Tag/Tag.theme.ts
index a027185d..884f79bc 100644
--- a/packages/frontend/src/components/shared/Tag/Tag.theme.ts
+++ b/packages/frontend/src/components/shared/Tag/Tag.theme.ts
@@ -4,7 +4,7 @@ import type { VariantProps } from 'tailwind-variants';
export const tagTheme = tv(
{
slots: {
- wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'],
+ wrapper: ['inline-flex', 'gap-1.5', 'rounded-lg', 'border'],
icon: [],
label: ['font-inter', 'text-xs'],
},
diff --git a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx
index d26a2dd4..d5f0346f 100644
--- a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx
+++ b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx
@@ -88,17 +88,17 @@ const DeploymentsTabPanel = () => {
setFilterValue(DEFAULT_FILTER_VALUE);
}, []);
- const onUpdateDeploymenToProd = async () => {
+ const onUpdateDeploymentToProd = async () => {
await fetchDeployments();
};
return (
-
+
setFilterValue(value)}
/>
-
+
{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}
/>
diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js
index c362e898..4efc11fe 100644
--- a/packages/frontend/tailwind.config.js
+++ b/packages/frontend/tailwind.config.js
@@ -12,6 +12,9 @@ export default withMT({
zIndex: {
tooltip: '52',
},
+ letterSpacing: {
+ tight: '-0.084px',
+ },
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Inter Display', 'sans-serif'],