1+ import { ConfirmDeletionModal } from '@/components/ConfirmDeletionModal' ;
12import { ProgressBar } from '@/components/ProgressBar' ;
23import { Badge } from '@/components/ui/badge' ;
34import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card' ;
@@ -13,14 +14,17 @@ import { isBeingUpdated, isRunning } from '@/components/ui/utils/badgeStatus';
1314import { useInstanceClient } from '@/config/useInstanceClient' ;
1415import { getClusterInfo , getClusterInfoQueryOptions } from '@/features/cluster/queries/getClusterInfoQuery' ;
1516import { ClusterCardAction } from '@/features/clusters/components/ClusterCardAction' ;
17+ import { useTerminateClusterMutation } from '@/features/clusters/mutations/terminateCluster' ;
1618import { onInstanceLogoutSubmit } from '@/features/instance/operations/mutations/onInstanceLogoutSubmit' ;
1719import { useInstanceAuth } from '@/hooks/useAuth' ;
1820import { useOrganizationClusterPermissions } from '@/hooks/usePermissions' ;
1921import { Cluster } from '@/lib/api.patch' ;
2022import { excludeFalsy } from '@/lib/arrays/excludeFalsy' ;
2123import { authStore } from '@/lib/authStore' ;
24+ import { capitalizeWords } from '@/lib/string/capitalizeWords' ;
2225import { getOperationsUrlForCluster } from '@/lib/urls/getOperationsUrlForCluster' ;
23- import { useQuery } from '@tanstack/react-query' ;
26+ import { queryKeys } from '@/react-query/constants' ;
27+ import { useQuery , useQueryClient } from '@tanstack/react-query' ;
2428import { Link } from '@tanstack/react-router' ;
2529import { CopyIcon , Ellipsis } from 'lucide-react' ;
2630import { useCallback , useMemo , useState } from 'react' ;
@@ -29,40 +33,45 @@ import { toast } from 'sonner';
2933const activeClusterStatuses = [ 'RUNNING' ] ;
3034const deletedClusterStatuses = [ 'TERMINATING' , 'TERMINATED' , 'REMOVED' ] ;
3135
32- export function ClusterCard ( {
33- cluster,
34- onTerminateClusterModal,
35- } : {
36- cluster : Cluster ;
37- onTerminateClusterModal : ( cluster : Cluster ) => void ;
38- } ) {
39- const { view, update, remove } = useOrganizationClusterPermissions ( cluster . organizationId , cluster . id ) ;
36+ export function ClusterCard ( { cluster } : { cluster : Cluster ; } ) {
37+ const queryClient = useQueryClient ( ) ;
38+ const operationsUrl = useMemo ( ( ) => getOperationsUrlForCluster ( cluster ) , [ cluster ] ) ;
39+ const instanceClient = useInstanceClient ( operationsUrl ) ;
4040 const auth = useInstanceAuth ( cluster . id ) ;
41- const isUpdating = isBeingUpdated ( cluster . status ) ;
42- const { data : clusterById } = useQuery (
43- getClusterInfoQueryOptions ( isUpdating && cluster . id , 2000 ) ,
44- ) ;
41+
42+ const { view, update, remove } = useOrganizationClusterPermissions ( cluster . organizationId , cluster . id ) ;
43+ const { mutate : terminateCluster , isPending : isTerminateClusterPending } = useTerminateClusterMutation ( ) ;
44+
45+ const [ signingOut , setSigningOut ] = useState ( false ) ;
46+ const [ isTerminateClusterModalOpen , setIsTerminateClusterModalOpen ] = useState ( false ) ;
4547
4648 const isActive = useMemo ( ( ) => cluster . status && activeClusterStatuses . includes ( cluster . status ) , [ cluster . status ] ) ;
49+ const isSelfManaged = useMemo ( ( ) => ! ! cluster ?. plans ?. [ 0 ] ?. planId ?. startsWith ( 'self-hosted' ) , [ cluster ] ) ;
4750 const isTerminated = useMemo (
4851 ( ) => cluster . status && deletedClusterStatuses . includes ( cluster . status ) ,
4952 [ cluster . status ] ,
5053 ) ;
54+
55+ const isUpdating = isBeingUpdated ( cluster . status ) ;
56+ const { data : clusterById } = useQuery (
57+ getClusterInfoQueryOptions ( isUpdating && cluster . id , 2000 ) ,
58+ ) ;
5159 const updating = useMemo ( ( ) => {
5260 if ( isUpdating && clusterById ?. instances ) {
5361 const updating = clusterById . instances . reduce ( ( total , instance ) =>
5462 total + ( isBeingUpdated ( instance . status ) ? 1 : 0 ) , 0 ) ;
5563 const running = clusterById . instances . reduce ( ( total , instance ) =>
5664 total + ( isRunning ( instance . status ) ? 1 : 0 ) , 0 ) ;
65+ const current = running ;
66+ const total = updating + running ;
5767 return {
58- current : running ,
59- total : updating + running ,
68+ width : `${ current === 0 ? 0 : ( current / total * 100 ) } %` ,
69+ text : total > 0
70+ ? `${ current } / ${ total } `
71+ : `${ capitalizeWords ( cluster . status ?? 'Updating' ) } ...` ,
6072 } ;
6173 }
62- } , [ clusterById , isUpdating ] ) ;
63- const operationsUrl = useMemo ( ( ) => getOperationsUrlForCluster ( cluster ) , [ cluster ] ) ;
64- const instanceClient = useInstanceClient ( operationsUrl ) ;
65- const [ signingOut , setSigningOut ] = useState ( false ) ;
74+ } , [ cluster . status , clusterById ?. instances , isUpdating ] ) ;
6675
6776 const onSignOutClick = useCallback ( async ( ) => {
6877 setSigningOut ( true ) ;
@@ -79,9 +88,43 @@ export function ClusterCard({
7988 }
8089 authStore . setUserForEntity ( cluster , null ) ;
8190 } , [ cluster , instanceClient ] ) ;
82- const onTerminateClick = useCallback ( ( ) => {
83- onTerminateClusterModal ( cluster ) ;
84- } , [ cluster , onTerminateClusterModal ] ) ;
91+
92+ const onTerminateClick = useCallback ( ( ) => setIsTerminateClusterModalOpen ( true ) , [ ] ) ;
93+
94+ const handleTerminatedCluster = useCallback ( ( ) => {
95+ terminateCluster ( cluster . id , {
96+ onSuccess : ( ) => {
97+ toast . success ( 'Success' , {
98+ description : isSelfManaged
99+ ? `Cluster successfully removed.`
100+ : `Cluster successfully terminated.` ,
101+ duration : 5000 ,
102+ action : {
103+ label : 'Dismiss' ,
104+ onClick : ( ) => toast . dismiss ( ) ,
105+ } ,
106+ } ) ;
107+ void queryClient . invalidateQueries ( {
108+ queryKey : [ queryKeys . organization ] ,
109+ refetchType : 'active' ,
110+ } ) ;
111+ setIsTerminateClusterModalOpen ( false ) ;
112+ } ,
113+ onError : ( ) => {
114+ toast . error ( 'Error' , {
115+ description : isSelfManaged
116+ ? `Failed to remove cluster: ${ cluster . name } `
117+ : `Failed to terminate cluster: ${ cluster . name } .` ,
118+ duration : 5000 ,
119+ action : {
120+ label : 'Dismiss' ,
121+ onClick : ( ) => toast . dismiss ( ) ,
122+ } ,
123+ } ) ;
124+ setIsTerminateClusterModalOpen ( false ) ;
125+ } ,
126+ } ) ;
127+ } , [ terminateCluster , cluster . id , cluster . name , isSelfManaged , queryClient ] ) ;
85128
86129 const onCopyFQDNClick = useCallback ( ( ) => {
87130 navigator . clipboard . writeText ( cluster . fqdn || '' ) ;
@@ -121,7 +164,7 @@ export function ClusterCard({
121164 ) ,
122165 ! isTerminated && remove && (
123166 < DropdownMenuItem className = "focus:bg-red/70 focus:text-white" onClick = { onTerminateClick } >
124- Terminate
167+ { isSelfManaged ? 'Remove' : ' Terminate' }
125168 </ DropdownMenuItem >
126169 ) ,
127170 ] . filter ( excludeFalsy ) ;
@@ -168,16 +211,28 @@ export function ClusterCard({
168211 </ CardTitle >
169212 </ CardHeader >
170213 < CardContent className = "flex justify-between" >
171- { updating && (
214+ { updating && cluster . status && (
172215 < ProgressBar
173216 animated = { true }
174217 className = "bg-yellow-800/60"
175- width = { ( updating . current === 0 ? 0 : ( updating . current / updating . total * 100 ) ) + '%' }
176- placeholder = " updating..."
218+ width = { updating . width }
219+ placeholder = { updating . text }
177220 />
178221 ) }
179222 { isActive && view && < ClusterCardAction cluster = { cluster } /> }
180223 </ CardContent >
224+
225+ < ConfirmDeletionModal
226+ typeOfThingBeingDeleted = "cluster"
227+ transitiveVerb = { isSelfManaged ? 'Remove' : 'Terminate' }
228+ presentParticiple = { isSelfManaged ? 'Removing' : 'Terminating' }
229+ nameOfThingBeingDeleted = { cluster . name }
230+ isModalOpen = { isTerminateClusterModalOpen }
231+ hideDataLossWarning = { isSelfManaged }
232+ setIsModalOpen = { ( isOpen : boolean ) => setIsTerminateClusterModalOpen ( isOpen ) }
233+ deletionConfirmed = { handleTerminatedCluster }
234+ deletionPending = { isTerminateClusterPending }
235+ />
181236 </ Card >
182237 ) ;
183238}
0 commit comments