@@ -5,17 +5,16 @@ import { createLogger } from '@sim/logger'
55import { toError } from '@sim/utils/errors'
66import { Loader2 } from 'lucide-react'
77import { useParams } from 'next/navigation'
8- import { Button } from '@/components/emcn'
8+ import { Button , Label } from '@/components/emcn'
99import {
1010 Select ,
1111 SelectContent ,
1212 SelectItem ,
1313 SelectTrigger ,
1414 SelectValue ,
1515} from '@/components/ui/select'
16- import type { PlanCategory } from '@/lib/billing/plan-helpers '
16+ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider '
1717import {
18- type DataRetentionResponse ,
1918 useUpdateWorkspaceRetention ,
2019 useWorkspaceRetention ,
2120} from '@/ee/data-retention/hooks/data-retention'
@@ -36,48 +35,6 @@ const DAY_OPTIONS = [
3635 { value : 'never' , label : 'Forever' } ,
3736] as const
3837
39- interface RetentionFieldProps {
40- label : string
41- description : string
42- value : string
43- onChange : ( value : string ) => void
44- disabled : boolean
45- }
46-
47- function RetentionField ( { label, description, value, onChange, disabled } : RetentionFieldProps ) {
48- const standard = DAY_OPTIONS . find ( ( o ) => o . value === value )
49- const options = standard
50- ? DAY_OPTIONS
51- : [ ...DAY_OPTIONS , { value, label : `${ value } days (custom)` } as const ]
52-
53- return (
54- < div className = 'flex items-center justify-between py-2' >
55- < div className = 'flex flex-col gap-0.5' >
56- < span className = 'text-[13px] text-[var(--text-primary)]' > { label } </ span >
57- < p className = 'text-[12px] text-[var(--text-muted)]' > { description } </ p >
58- </ div >
59- { disabled ? (
60- < span className = 'text-[13px] text-[var(--text-muted)]' >
61- { standard ?. label ?? `${ value } days` }
62- </ span >
63- ) : (
64- < Select value = { value } onValueChange = { onChange } >
65- < SelectTrigger className = 'w-[140px] text-[13px]' >
66- < SelectValue />
67- </ SelectTrigger >
68- < SelectContent >
69- { options . map ( ( opt ) => (
70- < SelectItem key = { opt . value } value = { opt . value } className = 'text-[13px]' >
71- { opt . label }
72- </ SelectItem >
73- ) ) }
74- </ SelectContent >
75- </ Select >
76- ) }
77- </ div >
78- )
79- }
80-
8138function hoursToDisplayDays ( hours : number | null ) : string {
8239 if ( hours === null ) return 'never'
8340 return String ( Math . round ( hours / 24 ) )
@@ -88,65 +45,76 @@ function daysToHours(days: string): number | null {
8845 return Number ( days ) * 24
8946}
9047
91- const PLAN_LABELS : Record < PlanCategory , string > = {
92- free : 'Free' ,
93- pro : 'Pro' ,
94- team : 'Team' ,
95- enterprise : 'Enterprise' ,
48+ interface SettingRowProps {
49+ label : string
50+ description ?: string
51+ children : React . ReactNode
9652}
9753
98- function LockedView ( { data } : { data : DataRetentionResponse } ) {
54+ function SettingRow ( { label , description , children } : SettingRowProps ) {
9955 return (
100- < div className = 'flex flex-col gap-5' >
101- < p className = 'text-[13px] text-[var(--text-muted)]' >
102- Data retention policies control how long your workspace data is kept before automatic
103- cleanup. Custom retention periods are available on Enterprise plans.
104- </ p >
105- < div className = 'flex flex-col' >
106- < RetentionField
107- label = 'Log Retention'
108- description = 'How long execution logs are kept before archival.'
109- value = { hoursToDisplayDays ( data . effective . logRetentionHours ) }
110- onChange = { ( ) => { } }
111- disabled
112- />
113- < RetentionField
114- label = 'Soft Deletion Cleanup'
115- description = 'How long deleted resources are recoverable before permanent removal.'
116- value = { hoursToDisplayDays ( data . effective . softDeleteRetentionHours ) }
117- onChange = { ( ) => { } }
118- disabled
119- />
120- < RetentionField
121- label = 'Task Cleanup'
122- description = 'How long before old tasks are permanently deleted.'
123- value = { hoursToDisplayDays ( data . effective . taskCleanupHours ) }
124- onChange = { ( ) => { } }
125- disabled
126- />
127- </ div >
128- < p className = 'text-[12px] text-[var(--text-muted)]' >
129- { PLAN_LABELS [ data . plan ] } plan defaults. Upgrade to Enterprise to customize retention
130- periods.
131- </ p >
56+ < div className = 'flex flex-col gap-1.5' >
57+ < Label className = 'text-[13px] text-[var(--text-primary)]' > { label } </ Label >
58+ { description && < p className = 'text-[12px] text-[var(--text-muted)]' > { description } </ p > }
59+ { children }
13260 </ div >
13361 )
13462}
13563
136- function EditableView ( { data, workspaceId } : { data : DataRetentionResponse ; workspaceId : string } ) {
137- const updateMutation = useUpdateWorkspaceRetention ( )
64+ function SectionTitle ( { children } : { children : React . ReactNode } ) {
65+ return < h3 className = 'mb-4 font-medium text-[15px] text-[var(--text-primary)]' > { children } </ h3 >
66+ }
13867
139- const [ logDays , setLogDays ] = useState ( hoursToDisplayDays ( data . effective . logRetentionHours ) )
140- const [ softDeleteDays , setSoftDeleteDays ] = useState (
141- hoursToDisplayDays ( data . effective . softDeleteRetentionHours )
142- )
143- const [ taskCleanupDays , setTaskCleanupDays ] = useState (
144- hoursToDisplayDays ( data . effective . taskCleanupHours )
68+ interface RetentionSelectProps {
69+ value : string
70+ onChange : ( value : string ) => void
71+ }
72+
73+ function RetentionSelect ( { value, onChange } : RetentionSelectProps ) {
74+ const standard = DAY_OPTIONS . find ( ( o ) => o . value === value )
75+ const options = standard
76+ ? DAY_OPTIONS
77+ : [ ...DAY_OPTIONS , { value, label : `${ value } days (custom)` } as const ]
78+
79+ return (
80+ < Select value = { value } onValueChange = { onChange } >
81+ < SelectTrigger className = 'h-[36px] max-w-[200px] text-[13px]' >
82+ < SelectValue />
83+ </ SelectTrigger >
84+ < SelectContent >
85+ { options . map ( ( opt ) => (
86+ < SelectItem key = { opt . value } value = { opt . value } className = 'text-[13px]' >
87+ { opt . label }
88+ </ SelectItem >
89+ ) ) }
90+ </ SelectContent >
91+ </ Select >
14592 )
93+ }
94+
95+ export function DataRetentionSettings ( ) {
96+ const params = useParams < { workspaceId : string } > ( )
97+ const workspaceId = params . workspaceId
98+
99+ const { data, isLoading } = useWorkspaceRetention ( workspaceId )
100+ const { canAdmin } = useUserPermissionsContext ( )
101+ const updateMutation = useUpdateWorkspaceRetention ( )
102+
103+ const [ logDays , setLogDays ] = useState ( '' )
104+ const [ softDeleteDays , setSoftDeleteDays ] = useState ( '' )
105+ const [ taskCleanupDays , setTaskCleanupDays ] = useState ( '' )
106+ const [ formInitialized , setFormInitialized ] = useState ( false )
146107
147108 const [ saveError , setSaveError ] = useState < string | null > ( null )
148109 const [ saveSuccess , setSaveSuccess ] = useState ( false )
149110
111+ if ( data && ! formInitialized ) {
112+ setLogDays ( hoursToDisplayDays ( data . effective . logRetentionHours ) )
113+ setSoftDeleteDays ( hoursToDisplayDays ( data . effective . softDeleteRetentionHours ) )
114+ setTaskCleanupDays ( hoursToDisplayDays ( data . effective . taskCleanupHours ) )
115+ setFormInitialized ( true )
116+ }
117+
150118 const handleSave = useCallback ( async ( ) => {
151119 setSaveError ( null )
152120 setSaveSuccess ( false )
@@ -168,35 +136,68 @@ function EditableView({ data, workspaceId }: { data: DataRetentionResponse; work
168136 }
169137 } , [ workspaceId , logDays , softDeleteDays , taskCleanupDays ] )
170138
171- return (
172- < div className = 'flex flex-col gap-5' >
173- < p className = 'text-[13px] text-[var(--text-muted)]' >
174- Configure how long your workspace data is retained before automatic cleanup. Values apply to
175- all workflows in this workspace.
176- </ p >
177- < div className = 'flex flex-col' >
178- < RetentionField
179- label = 'Log Retention'
180- description = 'How long execution logs are kept before archival.'
181- value = { logDays }
182- onChange = { setLogDays }
183- disabled = { false }
184- />
185- < RetentionField
186- label = 'Soft Deletion Cleanup'
187- description = 'How long deleted resources are recoverable before permanent removal.'
188- value = { softDeleteDays }
189- onChange = { setSoftDeleteDays }
190- disabled = { false }
191- />
192- < RetentionField
193- label = 'Task Cleanup'
194- description = 'How long before old tasks are permanently deleted.'
195- value = { taskCleanupDays }
196- onChange = { setTaskCleanupDays }
197- disabled = { false }
198- />
139+ if ( isLoading ) {
140+ return (
141+ < div className = 'flex flex-col gap-8' >
142+ { [ ...Array ( 3 ) ] . map ( ( _ , i ) => (
143+ < div key = { i } className = 'flex flex-col gap-3' >
144+ < div className = 'h-4 w-32 animate-pulse rounded bg-[var(--surface-3)]' />
145+ < div className = 'h-9 w-full animate-pulse rounded-lg bg-[var(--surface-3)]' />
146+ </ div >
147+ ) ) }
148+ </ div >
149+ )
150+ }
151+
152+ if ( ! data ) {
153+ return (
154+ < div className = 'flex h-full items-center justify-center text-[var(--text-muted)] text-sm' >
155+ Failed to load data retention settings.
156+ </ div >
157+ )
158+ }
159+
160+ if ( ! data . isEnterprise ) {
161+ return (
162+ < div className = 'flex h-full items-center justify-center text-[var(--text-muted)] text-sm' >
163+ Data retention is available on Enterprise plans only.
199164 </ div >
165+ )
166+ }
167+
168+ if ( ! canAdmin ) {
169+ return (
170+ < div className = 'flex h-full items-center justify-center text-[var(--text-muted)] text-sm' >
171+ Only workspace admins can configure data retention settings.
172+ </ div >
173+ )
174+ }
175+
176+ return (
177+ < div className = 'flex flex-col gap-8' >
178+ < section >
179+ < SectionTitle > Retention Periods</ SectionTitle >
180+ < div className = 'flex flex-col gap-5' >
181+ < SettingRow
182+ label = 'Log retention'
183+ description = 'How long execution logs are kept before they are permanently deleted.'
184+ >
185+ < RetentionSelect value = { logDays } onChange = { setLogDays } />
186+ </ SettingRow >
187+ < SettingRow
188+ label = 'Soft deletion cleanup'
189+ description = 'How long deleted resources remain recoverable before they are permanently removed.'
190+ >
191+ < RetentionSelect value = { softDeleteDays } onChange = { setSoftDeleteDays } />
192+ </ SettingRow >
193+ < SettingRow
194+ label = 'Task cleanup'
195+ description = 'How long copilot chats, runs, and inbox tasks are kept before they are permanently deleted.'
196+ >
197+ < RetentionSelect value = { taskCleanupDays } onChange = { setTaskCleanupDays } />
198+ </ SettingRow >
199+ </ div >
200+ </ section >
200201
201202 < div className = 'flex items-center gap-3' >
202203 < Button onClick = { handleSave } disabled = { updateMutation . isPending } className = 'text-[13px]' >
@@ -217,34 +218,3 @@ function EditableView({ data, workspaceId }: { data: DataRetentionResponse; work
217218 </ div >
218219 )
219220}
220-
221- export function DataRetentionSettings ( ) {
222- const params = useParams ( )
223- const workspaceId = params . workspaceId as string
224-
225- const { data, isLoading, error } = useWorkspaceRetention ( workspaceId )
226-
227- if ( isLoading ) {
228- return (
229- < div className = 'flex flex-col gap-4' >
230- { [ ...Array ( 3 ) ] . map ( ( _ , i ) => (
231- < div key = { i } className = 'h-[40px] w-full animate-pulse rounded bg-[var(--surface-3)]' />
232- ) ) }
233- </ div >
234- )
235- }
236-
237- if ( error || ! data ) {
238- return (
239- < div className = 'text-[13px] text-[var(--text-muted)]' >
240- Failed to load data retention settings.
241- </ div >
242- )
243- }
244-
245- if ( ! data . isEnterprise ) {
246- return < LockedView data = { data } />
247- }
248-
249- return < EditableView data = { data } workspaceId = { workspaceId } />
250- }
0 commit comments