Compare commits

...

10 Commits

Author SHA1 Message Date
Szilárd Dóró
c1bea1294d Merge pull request #1512 from nhost/changeset-release/main
chore: update versions
2023-01-12 15:46:08 +01:00
github-actions[bot]
8af2f6e9dd chore: update versions 2023-01-12 11:43:39 +00:00
Szilárd Dóró
e3d0b96917 Merge pull request #1503 from nhost/feat/retrigger-deployments
feat(dashboard): Retrigger Deployments
2023-01-12 12:41:55 +01:00
Szilárd Dóró
36c3519cf8 chore(dashboard): retrigger deployments 2023-01-12 10:18:28 +01:00
Szilárd Dóró
9b52e9bf13 Merge branch 'main' into feat/retrigger-deployments 2023-01-12 09:49:46 +01:00
Szilárd Dóró
07c8d90053 fix(dashboard): lint errors 2023-01-11 11:13:37 +01:00
Szilárd Dóró
a2621e40a4 feat(dashboard): unified list items for deployments
fixed the way the latest scheduled or pending deployment is tracked
2023-01-11 10:37:10 +01:00
Szilárd Dóró
61120a137a feat(dashboard): add redeployment support to overview 2023-01-11 09:43:06 +01:00
Szilárd Dóró
faea8feb2e Merge branch 'main' into feat/retrigger-deployments 2023-01-11 08:56:05 +01:00
Szilárd Dóró
552e31a4f0 feat(dashboard): initial redeploy button code 2023-01-10 17:12:14 +01:00
17 changed files with 506 additions and 304 deletions

View File

@@ -1,5 +1,11 @@
# @nhost/dashboard
## 0.9.6
### Patch Changes
- 36c3519c: feat(dashboard): retrigger deployments
## 0.9.5
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.9.5",
"version": "0.9.6",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View File

@@ -1,21 +1,14 @@
import type { DeploymentRowFragment } from '@/generated/graphql';
import { useGetDeploymentsSubSubscription } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import DelayedLoading from '@/ui/DelayedLoading';
import Status, { StatusEnum } from '@/ui/Status';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
import {
useGetDeploymentsSubSubscription,
useScheduledOrPendingDeploymentsSubSubscription,
} from '@/generated/graphql';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import List from '@/ui/v2/List';
import { getLastLiveDeployment } from '@/utils/helpers';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
import {
differenceInSeconds,
formatDistanceToNowStrict,
parseISO,
} from 'date-fns';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
type AppDeploymentsProps = {
appId: string;
@@ -66,146 +59,8 @@ function NextPrevPageLink(props: NextPrevPageLinkProps) {
);
}
type AppDeploymentDurationProps = {
startedAt: string;
endedAt: string;
};
export function AppDeploymentDuration({
startedAt,
endedAt,
}: AppDeploymentDurationProps) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
let interval: NodeJS.Timeout;
if (!endedAt) {
interval = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [endedAt]);
const totalDurationInSeconds = differenceInSeconds(
endedAt ? parseISO(endedAt) : currentTime,
parseISO(startedAt),
);
if (totalDurationInSeconds > 1200) {
return <div>20+m</div>;
}
const durationMins = Math.floor(totalDurationInSeconds / 60);
const durationSecs = totalDurationInSeconds % 60;
return (
<div
style={{
fontVariantNumeric: 'tabular-nums',
}}
className="self-center font-display text-sm+ text-greyscaleDark"
>
{durationMins}m {durationSecs}s
</div>
);
}
type AppDeploymentRowProps = {
deployment: DeploymentRowFragment;
isDeploymentLive: boolean;
};
export function AppDeploymentRow({
deployment,
isDeploymentLive,
}: AppDeploymentRowProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { commitMessage } = deployment;
return (
<div className="flex flex-row items-center px-2 py-4">
<div className="mr-2 flex items-center justify-center">
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="h-8 w-8"
/>
</div>
<div className="mx-4 w-full">
<Link
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
passHref
>
<a
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
>
<div className="max-w-md truncate text-sm+ font-normal text-greyscaleDark">
{commitMessage?.trim() || (
<span className="pr-1 font-normal italic">
No commit message
</span>
)}
</div>
<div className="text-sm+ text-greyscaleGrey">
{formatDistanceToNowStrict(
parseISO(deployment.deploymentStartedAt),
{
addSuffix: true,
},
)}
</div>
</a>
</Link>
</div>
<div className="flex flex-row">
{isDeploymentLive && (
<div className="flex self-center align-middle">
<Status status={StatusEnum.Live}>Live</Status>
</div>
)}
<div className="w-28 self-center text-right font-mono text-sm- font-medium">
<a
className="font-mono font-medium text-greyscaleDark"
target="_blank"
rel="noreferrer"
href={`https://github.com/${currentApplication.githubRepository?.fullName}/commit/${deployment.commitSHA}`}
>
{deployment.commitSHA.substring(0, 7)}
</a>
</div>
<div className="mx-4 w-28 text-right">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
<div className="mx-3 self-center">
<StatusCircle
status={deployment.deploymentStatus as DeploymentStatus}
/>
</div>
<div className="self-center">
<Link
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
passHref
>
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
</Link>
</div>
</div>
</div>
);
}
export default function AppDeployments(props: AppDeploymentsProps) {
const { appId } = props;
const [idOfLiveDeployment, setIdOfLiveDeployment] = useState('');
const router = useRouter();
@@ -216,8 +71,6 @@ export default function AppDeployments(props: AppDeploymentsProps) {
const limit = 10;
const offset = (page - 1) * limit;
// @TODO: Should query for all deployments, then subscribe to new ones.
const { data, loading, error } = useGetDeploymentsSubSubscription({
variables: {
id: appId,
@@ -226,26 +79,36 @@ export default function AppDeployments(props: AppDeploymentsProps) {
},
});
useEffect(() => {
if (!data) {
return;
}
const {
data: scheduledOrPendingDeploymentsData,
loading: scheduledOrPendingDeploymentsLoading,
} = useScheduledOrPendingDeploymentsSubSubscription({
variables: {
appId,
},
});
if (page === 1) {
setIdOfLiveDeployment(getLastLiveDeployment(data.deployments));
}
}, [data, idOfLiveDeployment, loading, page]);
if (loading) {
return <DelayedLoading delay={500} className="mt-12" />;
if (loading || scheduledOrPendingDeploymentsLoading) {
return (
<ActivityIndicator
delay={500}
className="mt-12"
label="Loading deployments..."
/>
);
}
if (error) {
throw error;
}
const nrOfDeployments = data.deployments.length;
const { deployments } = data || {};
const { deployments: scheduledOrPendingDeployments } =
scheduledOrPendingDeploymentsData || {};
const nrOfDeployments = deployments?.length || 0;
const nextAllowed = !(nrOfDeployments < limit);
const liveDeploymentId = getLastLiveDeployment(deployments);
return (
<div className="mt-6">
@@ -253,15 +116,24 @@ export default function AppDeployments(props: AppDeploymentsProps) {
<p className="text-sm text-greyscaleGrey">No deployments yet.</p>
) : (
<div>
<div className="mt-3 divide-y-1 border-t border-b">
{data.deployments.map((deployment) => (
<AppDeploymentRow
deployment={deployment}
<List className="mt-3 divide-y-1 border-t border-b">
{deployments.map((deployment, index) => (
<DeploymentListItem
key={deployment.id}
isDeploymentLive={idOfLiveDeployment === deployment.id}
deployment={deployment}
isLive={liveDeploymentId === deployment.id}
showRedeploy={
scheduledOrPendingDeployments.length > 0
? scheduledOrPendingDeployments.some(
(scheduledOrPendingDeployment) =>
scheduledOrPendingDeployment.id === deployment.id,
)
: index === 0
}
disableRedeploy={scheduledOrPendingDeployments.length > 0}
/>
))}
</div>
</List>
<div className="mt-8 flex w-full justify-center">
<div className="flex items-center">
<NextPrevPageLink

View File

@@ -34,13 +34,13 @@ export function Repo({ repo, setSelectedRepoId }: RepoProps) {
const [updateApp, { loading, error }] = useUpdateAppMutation({
refetchQueries: [
refetchGetAppByWorkspaceAndNameQuery({
workspace: currentWorkspace.slug,
slug: currentApplication.slug,
workspace: currentWorkspace?.slug,
slug: currentApplication?.slug,
}),
],
});
const { githubRepository } = currentApplication;
const { githubRepository } = currentApplication || {};
const isThisRepositoryAlreadyConnected =
githubRepository?.fullName && githubRepository.fullName === repo.fullName;

View File

@@ -0,0 +1,54 @@
import { differenceInSeconds, parseISO } from 'date-fns';
import { useEffect, useState } from 'react';
export interface AppDeploymentDurationProps {
/**
* Start date of the deployment.
*/
startedAt: string;
/**
* End date of the deployment.
*/
endedAt?: string;
}
export default function AppDeploymentDuration({
startedAt,
endedAt,
}: AppDeploymentDurationProps) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
let interval: NodeJS.Timeout;
if (!endedAt) {
interval = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [endedAt]);
const totalDurationInSeconds = differenceInSeconds(
endedAt ? parseISO(endedAt) : currentTime,
parseISO(startedAt),
);
if (totalDurationInSeconds > 1200) {
return <div>20+m</div>;
}
const durationMins = Math.floor(totalDurationInSeconds / 60);
const durationSecs = totalDurationInSeconds % 60;
return (
<div
style={{ fontVariantNumeric: 'tabular-nums' }}
className="self-center font-display text-sm+ text-greyscaleDark"
>
{durationMins}m {durationSecs}s
</div>
);
}

View File

@@ -0,0 +1,2 @@
export * from './AppDeploymentDuration';
export { default } from './AppDeploymentDuration';

View File

@@ -0,0 +1,172 @@
import NavLink from '@/components/common/NavLink';
import AppDeploymentDuration from '@/components/deployments/AppDeploymentDuration';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import Status, { StatusEnum } from '@/ui/Status';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import Button from '@/ui/v2/Button';
import ArrowCounterclockwiseIcon from '@/ui/v2/icons/ArrowCounterclockwiseIcon';
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
import { ListItem } from '@/ui/v2/ListItem';
import Tooltip from '@/ui/v2/Tooltip';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
import { useInsertDeploymentMutation } from '@/utils/__generated__/graphql';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface DeploymentListItemProps {
/**
* Deployment data.
*/
deployment: DeploymentRowFragment;
/**
* Determines whether or not the deployment is live.
*/
isLive?: boolean;
/**
* Determines whether or not the redeploy button should be shown for the
* deployment.
*/
showRedeploy?: boolean;
/**
* Determines whether or not the redeploy button is disabled.
*/
disableRedeploy?: boolean;
}
export default function DeploymentListItem({
deployment,
isLive,
showRedeploy,
disableRedeploy,
}: DeploymentListItemProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const showTime =
!['SCHEDULED', 'PENDING'].includes(deployment.deploymentStatus) &&
deployment.deploymentStartedAt;
const relativeDateOfDeployment = showTime
? formatDistanceToNowStrict(parseISO(deployment.deploymentStartedAt), {
addSuffix: true,
})
: '';
const [insertDeployment, { loading }] = useInsertDeploymentMutation();
const { commitMessage } = deployment;
return (
<ListItem.Root>
<ListItem.Button
className="grid grid-flow-col items-center justify-between gap-2 px-2 py-2"
component={NavLink}
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
>
<div className="flex cursor-pointer flex-row items-center justify-center space-x-2 self-center">
<ListItem.Avatar>
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="h-8 w-8 shrink-0"
/>
</ListItem.Avatar>
<ListItem.Text
primary={
commitMessage?.trim() || (
<span className="truncate pr-1 font-normal italic">
No commit message
</span>
)
}
secondary={relativeDateOfDeployment}
/>
</div>
<div className="grid grid-flow-col gap-2 items-center">
{showRedeploy && (
<Tooltip
title="An active deployment cannot be re-triggered"
hasDisabledChildren={disableRedeploy || loading}
disableHoverListener={!disableRedeploy}
>
<Button
disabled={disableRedeploy || loading}
size="small"
color="secondary"
variant="outlined"
onClick={async (event) => {
event.stopPropagation();
event.preventDefault();
const insertDeploymentPromise = insertDeployment({
variables: {
object: {
appId: currentApplication?.id,
commitMessage: deployment.commitMessage,
commitSHA: deployment.commitSHA,
commitUserAvatarUrl: deployment.commitUserAvatarUrl,
commitUserName: deployment.commitUserName,
deploymentStatus: 'SCHEDULED',
},
},
});
await toast.promise(
insertDeploymentPromise,
{
loading: 'Scheduling deployment...',
success: 'Deployment has been scheduled successfully.',
error: 'An error occurred when scheduling deployment.',
},
toastStyleProps,
);
}}
startIcon={
<ArrowCounterclockwiseIcon
className={twMerge(
'w-4 h-4',
disableRedeploy && 'animate-spin-reverse',
)}
/>
}
className="rounded-full py-1 px-2 text-xs"
>
{disableRedeploy ? 'Redeploying...' : 'Redeploy'}
</Button>
</Tooltip>
)}
{isLive && (
<div className="w-12 flex justify-end">
<Status status={StatusEnum.Live}>Live</Status>
</div>
)}
<div className="w-16 text-right font-mono text-sm- font-medium">
{deployment.commitSHA.substring(0, 7)}
</div>
{showTime && (
<div className="w-[80px] text-right font-mono text-sm- font-medium">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
)}
<StatusCircle
status={deployment.deploymentStatus as DeploymentStatus}
/>
<ChevronRightIcon className="h-4 w-4" />
</div>
</ListItem.Button>
</ListItem.Root>
);
}

View File

@@ -0,0 +1,2 @@
export * from './DeploymentListItem';
export { default } from './DeploymentListItem';

View File

@@ -1,25 +1,21 @@
import { AppDeploymentDuration } from '@/components/applications/AppDeployments';
import { EditRepositorySettings } from '@/components/applications/github/EditRepositorySettings';
import useGitHubModal from '@/components/applications/github/useGitHubModal';
import { useDialog } from '@/components/common/DialogProvider';
import NavLink from '@/components/common/NavLink';
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
import GithubIcon from '@/components/icons/GithubIcon';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import Status, { StatusEnum } from '@/ui/Status';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Button from '@/ui/v2/Button';
import RocketIcon from '@/ui/v2/icons/RocketIcon';
import type { ListItemRootProps } from '@/ui/v2/ListItem';
import { ListItem } from '@/ui/v2/ListItem';
import List from '@/ui/v2/List';
import Text from '@/ui/v2/Text';
import { getLastLiveDeployment } from '@/utils/helpers';
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
import { useGetDeploymentsSubSubscription } from '@/utils/__generated__/graphql';
import {
useGetDeploymentsSubSubscription,
useScheduledOrPendingDeploymentsSubSubscription,
} from '@/utils/__generated__/graphql';
import { ChevronRightIcon } from '@heroicons/react/solid';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import { twMerge } from 'tailwind-merge';
function OverviewDeploymentsTopBar() {
@@ -54,92 +50,6 @@ function OverviewDeploymentsTopBar() {
);
}
interface OverviewDeployProps extends ListItemRootProps {
/**
* Deployment metadata to display.
*/
deployment: DeploymentRowFragment;
/**
* Determines to show a status badge showing the live status of a deployment reflecting the latest state of the application.
*/
isDeploymentLive: boolean;
}
function OverviewDeployment({
deployment,
isDeploymentLive,
className,
}: OverviewDeployProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const relativeDateOfDeployment = formatDistanceToNowStrict(
parseISO(deployment.deploymentStartedAt),
{
addSuffix: true,
},
);
const { commitMessage } = deployment;
return (
<ListItem.Root className={twMerge('grid grid-flow-row', className)}>
<ListItem.Button
className="grid grid-flow-col items-center justify-between gap-2 px-2 py-2"
component={NavLink}
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
>
<div className="flex cursor-pointer flex-row items-center justify-center space-x-2 self-center">
<div>
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="h-8 w-8"
/>
</div>
<div className="grid grid-flow-row truncate text-sm+ font-medium">
<Text className="inline cursor-pointer truncate font-medium leading-snug text-greyscaleDark">
{commitMessage?.trim() || (
<span className="truncate pr-1 font-normal italic">
No commit message
</span>
)}
</Text>
<Text className="text-sm font-normal leading-[1.375rem] text-greyscaleGrey">
{relativeDateOfDeployment}
</Text>
</div>
</div>
<div className="grid grid-flow-col items-center self-center">
{isDeploymentLive && (
<div className="flex self-center align-middle">
<Status status={StatusEnum.Live}>Live</Status>
</div>
)}
<div className="w-20 self-center text-right align-middle font-mono text-sm- font-medium">
{deployment.commitSHA.substring(0, 7)}
</div>
<div className="w-20 self-center text-right align-middle font-mono text-sm-">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
<div className="mx-3 self-center">
<StatusCircle
status={deployment.deploymentStatus as DeploymentStatus}
/>
</div>
<div className="self-center">
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
</div>
</div>
</ListItem.Button>
</ListItem.Root>
);
}
interface OverviewDeploymentsProps {
projectId: string;
githubRepository: { fullName: string };
@@ -159,9 +69,18 @@ function OverviewDeployments({
},
});
if (loading) {
const {
data: scheduledOrPendingDeploymentsData,
loading: scheduledOrPendingDeploymentsLoading,
} = useScheduledOrPendingDeploymentsSubSubscription({
variables: {
appId: projectId,
},
});
if (loading || scheduledOrPendingDeploymentsLoading) {
return (
<div style={{ height: '240px' }}>
<div className="h-60">
<ActivityIndicator label="Loading deployments..." />
</div>
);
@@ -218,22 +137,29 @@ function OverviewDeployments({
);
}
const getLastLiveDeploymentId = getLastLiveDeployment(deployments);
const liveDeploymentId = getLastLiveDeployment(deployments);
const { deployments: scheduledOrPendingDeployments } =
scheduledOrPendingDeploymentsData;
return (
<div className="rounded-x-lg flex flex-col divide-y-1 divide-gray-200 rounded-lg border border-veryLightGray">
{deployments.map((deployment) => {
const isDeploymentLive = deployment.id === getLastLiveDeploymentId;
return (
<OverviewDeployment
key={deployment.id}
deployment={deployment}
isDeploymentLive={isDeploymentLive}
/>
);
})}
</div>
<List className="rounded-x-lg flex flex-col divide-y-1 divide-gray-200 rounded-lg border border-veryLightGray">
{deployments.map((deployment, index) => (
<DeploymentListItem
key={deployment.id}
deployment={deployment}
isLive={deployment.id === liveDeploymentId}
showRedeploy={
scheduledOrPendingDeployments.length > 0
? scheduledOrPendingDeployments.some(
(scheduledOrPendingDeployment) =>
scheduledOrPendingDeployment.id === deployment.id,
)
: index === 0
}
disableRedeploy={scheduledOrPendingDeployments.length > 0}
/>
))}
</List>
);
}

View File

@@ -0,0 +1,17 @@
import { styled } from '@mui/material';
import type { ListItemAvatarProps as MaterialListItemAvatarProps } from '@mui/material/ListItemAvatar';
import MaterialListItemAvatar from '@mui/material/ListItemAvatar';
export interface ListItemAvatarProps extends MaterialListItemAvatarProps {}
const StyledListItemAvatar = styled(MaterialListItemAvatar)({
minWidth: 0,
});
function ListItemAvatar({ children, ...props }: ListItemAvatarProps) {
return <StyledListItemAvatar {...props}>{children}</StyledListItemAvatar>;
}
ListItemAvatar.displayName = 'NhostListItemAvatar';
export default ListItemAvatar;

View File

@@ -1,8 +1,11 @@
import ListItemAvatar from './ListItemAvatar';
import ListItemButton from './ListItemButton';
import ListItemIcon from './ListItemIcon';
import ListItemRoot from './ListItemRoot';
import ListItemText from './ListItemText';
export * from './ListItemAvatar';
export { default as ListItemAvatar } from './ListItemAvatar';
export * from './ListItemButton';
export { default as ListItemButton } from './ListItemButton';
export * from './ListItemIcon';
@@ -13,6 +16,7 @@ export * from './ListItemText';
export { default as ListItemText } from './ListItemText';
export const ListItem = {
Avatar: ListItemAvatar,
Root: ListItemRoot,
Button: ListItemButton,
Icon: ListItemIcon,

View File

@@ -0,0 +1,34 @@
import type { IconProps } from '@/ui/v2/icons';
import SvgIcon from '@mui/material/SvgIcon';
function ArrowCounterclockwiseIcon(props: IconProps) {
return (
<SvgIcon
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
aria-label="A counterclockwise arrow"
{...props}
>
<path
d="M4.99 6.232h-3v-3"
stroke="currentColor"
fill="none"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M4.11 11.89a5.5 5.5 0 1 0 0-7.78L1.99 6.233"
stroke="currentColor"
fill="none"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</SvgIcon>
);
}
ArrowCounterclockwiseIcon.displayName = 'NhostArrowCounterclockwiseIcon';
export default ArrowCounterclockwiseIcon;

View File

@@ -0,0 +1 @@
export { default } from './ArrowCounterclockwiseIcon';

View File

@@ -20,6 +20,23 @@ query getDeployments($id: uuid!, $limit: Int!, $offset: Int!) {
}
}
subscription ScheduledOrPendingDeploymentsSub($appId: uuid!) {
deployments(
where: {
deploymentStatus: { _in: ["PENDING", "SCHEDULED"] }
appId: { _eq: $appId }
}
) {
...DeploymentRow
}
}
mutation InsertDeployment($object: deployments_insert_input!) {
insertDeployment(object: $object) {
...DeploymentRow
}
}
subscription getDeploymentsSub($id: uuid!, $limit: Int!, $offset: Int!) {
deployments(
where: { appId: { _eq: $id } }

View File

@@ -1,4 +1,4 @@
import { AppDeploymentDuration } from '@/components/applications/AppDeployments';
import AppDeploymentDuration from '@/components/deployments/AppDeploymentDuration';
import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout';
import { useDeploymentSubSubscription } from '@/generated/graphql';
@@ -8,6 +8,7 @@ import DelayedLoading from '@/ui/DelayedLoading';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import { Text } from '@/ui/Text';
import Link from '@/ui/v2/Link';
import { format, formatDistanceToNowStrict, parseISO } from 'date-fns';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
@@ -50,6 +51,16 @@ export default function DeploymentDetailsPage() {
);
}
const showTime =
!['SCHEDULED', 'PENDING'].includes(deployment.deploymentStatus) &&
deployment.deploymentStartedAt;
const relativeDateOfDeployment = showTime
? formatDistanceToNowStrict(parseISO(deployment.deploymentStartedAt), {
addSuffix: true,
})
: '';
return (
<Container>
<div className="flex justify-between">
@@ -104,35 +115,37 @@ export default function DeploymentDetailsPage() {
{deployment.commitMessage}
</div>
<div className="text-sm+ text-greyscaleGrey">
{formatDistanceToNowStrict(
parseISO(deployment.deploymentStartedAt),
{
addSuffix: true,
},
)}
{relativeDateOfDeployment}
</div>
</div>
</div>
<div className=" flex items-center">
<a
className="self-center font-mono text-sm- font-medium text-greyscaleDark"
<Link
className="self-center font-mono text-sm- font-medium"
target="_blank"
rel="noreferrer"
href={`https://github.com/${currentApplication.githubRepository?.fullName}/commit/${deployment.commitSHA}`}
underline="hover"
>
{deployment.commitSHA.substring(0, 7)}
</a>
</Link>
<div className="w-20 text-right">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
{showTime && (
<div className="w-20 text-right">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
)}
</div>
</div>
<div>
<div className="rounded-lg bg-verydark p-4 text-sm- text-white">
{deployment.deploymentLogs.length === 0 && (
<span className="font-mono">No message.</span>
)}
{deployment.deploymentLogs.map((log) => (
<div key={log.id} className="flex font-mono">
<div className=" mr-2 flex-shrink-0">

View File

@@ -17215,6 +17215,20 @@ export type GetDeploymentsQueryVariables = Exact<{
export type GetDeploymentsQuery = { __typename?: 'query_root', deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null }> };
export type ScheduledOrPendingDeploymentsSubSubscriptionVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type ScheduledOrPendingDeploymentsSubSubscription = { __typename?: 'subscription_root', deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null }> };
export type InsertDeploymentMutationVariables = Exact<{
object: Deployments_Insert_Input;
}>;
export type InsertDeploymentMutation = { __typename?: 'mutation_root', insertDeployment?: { __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null } | null };
export type GetDeploymentsSubSubscriptionVariables = Exact<{
id: Scalars['uuid'];
limit: Scalars['Int'];
@@ -19147,6 +19161,71 @@ export type GetDeploymentsQueryResult = Apollo.QueryResult<GetDeploymentsQuery,
export function refetchGetDeploymentsQuery(variables: GetDeploymentsQueryVariables) {
return { query: GetDeploymentsDocument, variables: variables }
}
export const ScheduledOrPendingDeploymentsSubDocument = gql`
subscription ScheduledOrPendingDeploymentsSub($appId: uuid!) {
deployments(
where: {deploymentStatus: {_in: ["PENDING", "SCHEDULED"]}, appId: {_eq: $appId}}
) {
...DeploymentRow
}
}
${DeploymentRowFragmentDoc}`;
/**
* __useScheduledOrPendingDeploymentsSubSubscription__
*
* To run a query within a React component, call `useScheduledOrPendingDeploymentsSubSubscription` and pass it any options that fit your needs.
* When your component renders, `useScheduledOrPendingDeploymentsSubSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useScheduledOrPendingDeploymentsSubSubscription({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useScheduledOrPendingDeploymentsSubSubscription(baseOptions: Apollo.SubscriptionHookOptions<ScheduledOrPendingDeploymentsSubSubscription, ScheduledOrPendingDeploymentsSubSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<ScheduledOrPendingDeploymentsSubSubscription, ScheduledOrPendingDeploymentsSubSubscriptionVariables>(ScheduledOrPendingDeploymentsSubDocument, options);
}
export type ScheduledOrPendingDeploymentsSubSubscriptionHookResult = ReturnType<typeof useScheduledOrPendingDeploymentsSubSubscription>;
export type ScheduledOrPendingDeploymentsSubSubscriptionResult = Apollo.SubscriptionResult<ScheduledOrPendingDeploymentsSubSubscription>;
export const InsertDeploymentDocument = gql`
mutation InsertDeployment($object: deployments_insert_input!) {
insertDeployment(object: $object) {
...DeploymentRow
}
}
${DeploymentRowFragmentDoc}`;
export type InsertDeploymentMutationFn = Apollo.MutationFunction<InsertDeploymentMutation, InsertDeploymentMutationVariables>;
/**
* __useInsertDeploymentMutation__
*
* To run a mutation, you first call `useInsertDeploymentMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useInsertDeploymentMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [insertDeploymentMutation, { data, loading, error }] = useInsertDeploymentMutation({
* variables: {
* object: // value for 'object'
* },
* });
*/
export function useInsertDeploymentMutation(baseOptions?: Apollo.MutationHookOptions<InsertDeploymentMutation, InsertDeploymentMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<InsertDeploymentMutation, InsertDeploymentMutationVariables>(InsertDeploymentDocument, options);
}
export type InsertDeploymentMutationHookResult = ReturnType<typeof useInsertDeploymentMutation>;
export type InsertDeploymentMutationResult = Apollo.MutationResult<InsertDeploymentMutation>;
export type InsertDeploymentMutationOptions = Apollo.BaseMutationOptions<InsertDeploymentMutation, InsertDeploymentMutationVariables>;
export const GetDeploymentsSubDocument = gql`
subscription getDeploymentsSub($id: uuid!, $limit: Int!, $offset: Int!) {
deployments(

View File

@@ -13,6 +13,9 @@ module.exports = {
...defaultTheme.screens,
},
extend: {
animation: {
'spin-reverse': 'spin 1.5s linear infinite reverse',
},
colors: {
primary: '#0052cd',
'primary-light': '#ebf3ff',