@@ -28,18 +28,19 @@ import { Check, ChevronsUpDown } from "lucide-react"
2828import { zodResolver } from "@hookform/resolvers/zod"
2929import { useForm } from "react-hook-form"
3030import { useFormState } from 'react-dom'
31- import { useEffect , useState } from "react" ;
31+ import { useEffect , useMemo , useState } from "react" ;
3232import { FormUtils } from "@/frontend/utils/form.utilts" ;
3333import { SubmitButton } from "@/components/custom/submit-button" ;
3434import { AppVolume } from "@prisma/client"
3535import { AppVolumeEditModel , appVolumeEditZodModel } from "@/shared/model/volume-edit.model"
3636import { ServerActionResult } from "@/shared/model/server-action-error-return.model"
37- import { saveVolume } from "./actions"
37+ import { getShareableVolumes , saveVolume } from "./actions"
3838import { toast } from "sonner"
3939import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from "@/components/ui/tooltip" ;
4040import { QuestionMarkCircledIcon } from "@radix-ui/react-icons"
4141import { AppExtendedModel } from "@/shared/model/app-extended.model"
4242import { NodeInfoModel } from "@/shared/model/node-info.model"
43+ import { Checkbox } from "@/components/ui/checkbox"
4344
4445const accessModes = [
4546 { label : "ReadWriteOnce" , value : "ReadWriteOnce" } ,
@@ -51,14 +52,18 @@ const storageClasses = [
5152 { label : "Local Path" , value : "local-path" , description : "Node-local volumes, no replication. Data is stored on the master node. Only works in a single node setup." }
5253] as const
5354
55+ type AppVolumeWithSharing = AppVolume & { sharedVolumeId ?: string | null ; shareWithOtherApps ?: boolean } ;
56+
5457export default function DialogEditDialog ( { children, volume, app, nodesInfo } : {
5558 children : React . ReactNode ;
56- volume ?: AppVolume ;
59+ volume ?: AppVolumeWithSharing ;
5760 app : AppExtendedModel ;
5861 nodesInfo : NodeInfoModel [ ] ;
5962} ) {
6063
6164 const [ isOpen , setIsOpen ] = useState < boolean > ( false ) ;
65+ const [ useExistingVolume , setUseExistingVolume ] = useState ( false ) ;
66+ const [ shareableVolumes , setShareableVolumes ] = useState < { id : string ; containerMountPath : string ; size : number ; storageClassName : string ; accessMode : string ; app : { name : string } } [ ] > ( [ ] ) ;
6267
6368
6469 const form = useForm < AppVolumeEditModel > ( {
@@ -67,9 +72,16 @@ export default function DialogEditDialog({ children, volume, app, nodesInfo }: {
6772 ...volume ,
6873 accessMode : volume ?. accessMode ?? ( app . replicas > 1 ? "ReadWriteMany" : "ReadWriteOnce" ) ,
6974 storageClassName : ( volume ?. storageClassName ?? "longhorn" ) as 'longhorn' | 'local-path' ,
75+ shareWithOtherApps : volume ?. shareWithOtherApps ?? false ,
76+ sharedVolumeId : volume ?. sharedVolumeId ?? null ,
7077 }
7178 } ) ;
7279
80+ const selectedAccessMode = form . watch ( "accessMode" ) ;
81+ const selectedSharedVolumeId = form . watch ( "sharedVolumeId" ) ;
82+ const selectedSharedVolume = useMemo ( ( ) => shareableVolumes . find ( item => item . id === selectedSharedVolumeId ) , [ shareableVolumes , selectedSharedVolumeId ] ) ;
83+ const hasShareableVolumes = shareableVolumes . length > 0 ;
84+
7385 const [ state , formAction ] = useFormState ( ( state : ServerActionResult < any , any > , payload : AppVolumeEditModel ) =>
7486 saveVolume ( state , {
7587 ...payload ,
@@ -93,9 +105,49 @@ export default function DialogEditDialog({ children, volume, app, nodesInfo }: {
93105 ...volume ,
94106 accessMode : volume ?. accessMode ?? ( app . replicas > 1 ? "ReadWriteMany" : "ReadWriteOnce" ) ,
95107 storageClassName : ( volume ?. storageClassName ?? "longhorn" ) as 'longhorn' | 'local-path' ,
108+ shareWithOtherApps : volume ?. shareWithOtherApps ?? false ,
109+ sharedVolumeId : volume ?. sharedVolumeId ?? null ,
96110 } ) ;
111+ setUseExistingVolume ( false ) ;
97112 } , [ volume ] ) ;
98113
114+ useEffect ( ( ) => {
115+ if ( ! isOpen || volume ) {
116+ return ;
117+ }
118+ const loadShareableVolumes = async ( ) => {
119+ const response = await getShareableVolumes ( app . id ) ;
120+ if ( response . status === 'success' && response . data ) {
121+ setShareableVolumes ( response . data ) ;
122+ } else {
123+ setShareableVolumes ( [ ] ) ;
124+ }
125+ } ;
126+ loadShareableVolumes ( ) ;
127+ } , [ app . id , isOpen , volume ] ) ;
128+
129+ useEffect ( ( ) => {
130+ if ( ! useExistingVolume ) {
131+ form . setValue ( "sharedVolumeId" , null ) ;
132+ return ;
133+ }
134+ if ( selectedSharedVolume ) {
135+ form . setValue ( "size" , selectedSharedVolume . size ) ;
136+ form . setValue ( "accessMode" , selectedSharedVolume . accessMode ) ;
137+ form . setValue ( "storageClassName" , selectedSharedVolume . storageClassName as 'longhorn' | 'local-path' ) ;
138+ form . setValue ( "shareWithOtherApps" , false ) ;
139+ }
140+ } , [ form , selectedSharedVolume , useExistingVolume ] ) ;
141+
142+ useEffect ( ( ) => {
143+ if ( ! useExistingVolume || selectedSharedVolumeId ) {
144+ return ;
145+ }
146+ if ( shareableVolumes . length > 0 ) {
147+ form . setValue ( "sharedVolumeId" , shareableVolumes [ 0 ] . id ) ;
148+ }
149+ } , [ form , selectedSharedVolumeId , shareableVolumes , useExistingVolume ] ) ;
150+
99151 return (
100152 < >
101153 < div onClick = { ( ) => setIsOpen ( true ) } >
@@ -114,6 +166,86 @@ export default function DialogEditDialog({ children, volume, app, nodesInfo }: {
114166 return formAction ( data ) ;
115167 } ) ( ) } >
116168 < div className = "space-y-4" >
169+ { ! volume && (
170+ < div className = "flex items-center gap-2" >
171+ < Checkbox
172+ id = "use-existing-volume"
173+ checked = { useExistingVolume }
174+ onCheckedChange = { ( checked ) => setUseExistingVolume ( ! ! checked ) }
175+ disabled = { ! hasShareableVolumes }
176+ />
177+ < FormLabel htmlFor = "use-existing-volume" > Use existing shared volume</ FormLabel >
178+ </ div >
179+ ) }
180+ { ! volume && ! hasShareableVolumes && (
181+ < p className = "text-xs text-muted-foreground" >
182+ No shared volumes are available from other apps in this project.
183+ </ p >
184+ ) }
185+ { ! volume && useExistingVolume && (
186+ < FormField
187+ control = { form . control }
188+ name = "sharedVolumeId"
189+ render = { ( { field } ) => (
190+ < FormItem className = "flex flex-col" >
191+ < FormLabel > Shared Volume</ FormLabel >
192+ < Popover >
193+ < PopoverTrigger asChild >
194+ < FormControl >
195+ < Button
196+ variant = "outline"
197+ role = "combobox"
198+ className = { cn (
199+ "w-full justify-between" ,
200+ ! field . value && "text-muted-foreground"
201+ ) }
202+ >
203+ { selectedSharedVolume
204+ ? `${ selectedSharedVolume . app . name } · ${ selectedSharedVolume . containerMountPath } `
205+ : "Select a shared volume" }
206+ < ChevronsUpDown className = "opacity-50" />
207+ </ Button >
208+ </ FormControl >
209+ </ PopoverTrigger >
210+ < PopoverContent className = "max-w-[320px] p-0" >
211+ < Command >
212+ < CommandList >
213+ < CommandGroup >
214+ { shareableVolumes . map ( ( shareableVolume ) => (
215+ < CommandItem
216+ value = { `${ shareableVolume . app . name } -${ shareableVolume . containerMountPath } ` }
217+ key = { shareableVolume . id }
218+ onSelect = { ( ) => {
219+ form . setValue ( "sharedVolumeId" , shareableVolume . id ) ;
220+ } }
221+ >
222+ < div className = "flex flex-col gap-1" >
223+ < span > { shareableVolume . app . name } </ span >
224+ < span className = "text-xs text-muted-foreground" > { shareableVolume . containerMountPath } · { shareableVolume . size } MB</ span >
225+ </ div >
226+ < Check
227+ className = { cn (
228+ "ml-auto" ,
229+ shareableVolume . id === field . value
230+ ? "opacity-100"
231+ : "opacity-0"
232+ ) }
233+ />
234+ </ CommandItem >
235+ ) ) }
236+ </ CommandGroup >
237+ </ CommandList >
238+ </ Command >
239+ </ PopoverContent >
240+ </ Popover >
241+ < FormDescription >
242+ Select a ReadWriteMany volume shared by another app in this project.
243+ </ FormDescription >
244+ < FormMessage />
245+ </ FormItem >
246+ ) }
247+ />
248+ ) }
117249 < FormField
118250 control = { form . control }
119251 name = "containerMountPath"
@@ -135,7 +267,7 @@ export default function DialogEditDialog({ children, volume, app, nodesInfo }: {
135267 < FormItem >
136268 < FormLabel > Size in MB</ FormLabel >
137269 < FormControl >
138- < Input type = "number" placeholder = "ex. 20" { ...field } />
270+ < Input type = "number" placeholder = "ex. 20" { ...field } disabled = { useExistingVolume || ! ! volume ?. sharedVolumeId } />
139271 </ FormControl >
140272 < FormMessage />
141273 </ FormItem >
@@ -145,7 +277,7 @@ export default function DialogEditDialog({ children, volume, app, nodesInfo }: {
145277 < FormField
146278 control = { form . control }
147279 name = "accessMode"
148- disabled = { ! ! volume }
280+ disabled = { ! ! volume || useExistingVolume || ! ! volume ?. sharedVolumeId }
149281 render = { ( { field } ) => (
150282 < FormItem className = "flex flex-col" >
151283 < FormLabel className = "flex gap-2" >
@@ -223,6 +355,29 @@ export default function DialogEditDialog({ children, volume, app, nodesInfo }: {
223355 </ FormItem >
224356 ) }
225357 />
358+ { ! useExistingVolume && ! volume ?. sharedVolumeId && selectedAccessMode === 'ReadWriteMany' && (
359+ < FormField
360+ control = { form . control }
361+ name = "shareWithOtherApps"
362+ render = { ( { field } ) => (
363+ < FormItem className = "space-y-2" >
364+ < div className = "flex items-center gap-2" >
365+ < FormControl >
366+ < Checkbox
367+ checked = { field . value ?? false }
368+ onCheckedChange = { ( checked ) => field . onChange ( ! ! checked ) }
369+ />
370+ </ FormControl >
371+ < FormLabel > Share with other apps in project</ FormLabel >
372+ </ div >
373+ < FormDescription >
374+ Allow other apps in this project to mount this volume at their own paths.
375+ </ FormDescription >
376+ < FormMessage />
377+ </ FormItem >
378+ ) }
379+ />
380+ ) }
226381 { nodesInfo . length === 1 &&
227382 < FormField
228383 control = { form . control }
@@ -256,7 +411,7 @@ export default function DialogEditDialog({ children, volume, app, nodesInfo }: {
256411 "w-full justify-between" ,
257412 ! field . value && "text-muted-foreground"
258413 ) }
259- disabled = { ! ! volume }
414+ disabled = { ! ! volume || useExistingVolume || ! ! volume ?. sharedVolumeId }
260415 >
261416 { field . value
262417 ? storageClasses . find (
0 commit comments