Compare commits
10 Commits
@nhost/has
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1bea1294d | ||
|
|
8af2f6e9dd | ||
|
|
e3d0b96917 | ||
|
|
36c3519cf8 | ||
|
|
9b52e9bf13 | ||
|
|
07c8d90053 | ||
|
|
a2621e40a4 | ||
|
|
61120a137a | ||
|
|
faea8feb2e | ||
|
|
552e31a4f0 |
@@ -1,5 +1,11 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.9.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 36c3519c: feat(dashboard): retrigger deployments
|
||||
|
||||
## 0.9.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.9.5",
|
||||
"version": "0.9.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './AppDeploymentDuration';
|
||||
export { default } from './AppDeploymentDuration';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './DeploymentListItem';
|
||||
export { default } from './DeploymentListItem';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
17
dashboard/src/components/ui/v2/ListItem/ListItemAvatar.tsx
Normal file
17
dashboard/src/components/ui/v2/ListItem/ListItemAvatar.tsx
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ArrowCounterclockwiseIcon';
|
||||
@@ -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 } }
|
||||
|
||||
@@ -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">
|
||||
|
||||
79
dashboard/src/utils/__generated__/graphql.ts
generated
79
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -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(
|
||||
|
||||
@@ -13,6 +13,9 @@ module.exports = {
|
||||
...defaultTheme.screens,
|
||||
},
|
||||
extend: {
|
||||
animation: {
|
||||
'spin-reverse': 'spin 1.5s linear infinite reverse',
|
||||
},
|
||||
colors: {
|
||||
primary: '#0052cd',
|
||||
'primary-light': '#ebf3ff',
|
||||
|
||||
Reference in New Issue
Block a user