1- import React , { useContext , useState } from "react" ;
2- import { deleteRequest , downloadRequest , putRequest } from "@/common/utils/RequestUtil" ;
1+ import React , { useContext , useEffect , useState } from "react" ;
2+ import { deleteRequest , downloadRequest , patchRequest , putRequest } from "@/common/utils/RequestUtil" ;
33import { SpeedtestContext } from "@/common/contexts/Speedtests" ;
4+ import { ConfigContext } from "@/common/contexts/Config" ;
45import { ToastNotificationContext } from "@/common/contexts/ToastNotification" ;
56import { t } from "i18next" ;
67import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
7- import { faFileExport , faFileImport , faTrashCan , faChartLine } from "@fortawesome/free-solid-svg-icons" ;
8+ import { faFileExport , faFileImport , faTrashCan , faChartLine , faClockRotateLeft , faCheck , faArrowLeft } from "@fortawesome/free-solid-svg-icons" ;
9+
10+ const RETENTION_PRESETS = [
11+ { id : "week" , days : 7 } ,
12+ { id : "month" , days : 30 } ,
13+ { id : "quarter" , days : 90 } ,
14+ { id : "half_year" , days : 180 } ,
15+ { id : "year" , days : 365 } ,
16+ { id : "forever" , days : 0 }
17+ ] ;
18+
19+ const matchPreset = ( days ) => RETENTION_PRESETS . find ( p => p . days === days ) ?. id || "custom" ;
820
921export default ( { tests, close} ) => {
1022 const [ deleteWarning , setDeleteWarning ] = useState ( false ) ;
1123 const { updateTests} = useContext ( SpeedtestContext ) ;
24+ const [ config , reloadConfig ] = useContext ( ConfigContext ) ;
1225 const updateToast = useContext ( ToastNotificationContext ) ;
1326
27+ const initialDays = parseInt ( config ?. retentionDays ?? "365" ) ;
28+ const [ retentionSelected , setRetentionSelected ] = useState ( matchPreset ( initialDays ) ) ;
29+ const [ retentionCustom , setRetentionCustom ] = useState ( String ( initialDays ) ) ;
30+ const [ savingRetention , setSavingRetention ] = useState ( false ) ;
31+
32+ useEffect ( ( ) => {
33+ const days = parseInt ( config ?. retentionDays ?? "365" ) ;
34+ setRetentionSelected ( matchPreset ( days ) ) ;
35+ setRetentionCustom ( String ( days ) ) ;
36+ } , [ config ?. retentionDays ] ) ;
37+
38+ const handleSelectChange = ( value ) => {
39+ setRetentionSelected ( value ) ;
40+ if ( value === "custom" ) {
41+ setRetentionCustom ( String ( initialDays ) ) ;
42+ }
43+ } ;
44+
45+ const exitCustomMode = ( ) => {
46+ const savedPreset = matchPreset ( initialDays ) ;
47+ const target = savedPreset === "custom" ? "year" : savedPreset ;
48+ setRetentionSelected ( target ) ;
49+ setRetentionCustom ( String ( initialDays ) ) ;
50+ } ;
51+
52+ const currentRetentionDays = retentionSelected === "custom"
53+ ? parseInt ( retentionCustom )
54+ : RETENTION_PRESETS . find ( p => p . id === retentionSelected ) ?. days ;
55+
56+ const isRetentionValid = retentionSelected !== "custom"
57+ || ( ! isNaN ( currentRetentionDays ) && currentRetentionDays >= 0 && currentRetentionDays <= 10000 ) ;
58+
59+ const isRetentionDirty = String ( currentRetentionDays ) !== String ( initialDays ) ;
60+
61+ const saveRetention = async ( ) => {
62+ if ( ! isRetentionValid ) return ;
63+ setSavingRetention ( true ) ;
64+ const res = await patchRequest ( "/config/retentionDays" , { value : String ( currentRetentionDays ) } ) ;
65+ setSavingRetention ( false ) ;
66+ if ( res . ok ) {
67+ updateToast ( t ( "storage.retention_saved" ) , "green" , faCheck ) ;
68+ reloadConfig ( ) ;
69+ } else {
70+ updateToast ( t ( "dropdown.changes_unsaved" ) , "red" ) ;
71+ }
72+ } ;
73+
1474 const deleteHistory = ( ) => {
1575 if ( ! deleteWarning ) {
1676 setDeleteWarning ( true ) ;
@@ -71,6 +131,61 @@ export default ({tests, close}) => {
71131 < p className = "storage-row-value" > { tests } { t ( "storage.tests" ) } </ p >
72132 </ div >
73133
134+ < div className = "storage-row" >
135+ < div className = "storage-row-label" >
136+ < FontAwesomeIcon icon = { faClockRotateLeft } />
137+ < h3 title = { t ( "storage.retention_desc" ) } > { t ( "storage.retention" ) } </ h3 >
138+ </ div >
139+ < div className = "storage-row-actions storage-retention-controls" >
140+ { retentionSelected === "custom" ? (
141+ < >
142+ < button
143+ type = "button"
144+ className = "storage-icon-btn"
145+ onClick = { exitCustomMode }
146+ title = { t ( "storage.retention_back" ) }
147+ aria-label = { t ( "storage.retention_back" ) }
148+ >
149+ < FontAwesomeIcon icon = { faArrowLeft } />
150+ </ button >
151+ < div className = "storage-input-wrap" >
152+ < input
153+ type = "number"
154+ min = "0"
155+ max = "10000"
156+ className = { `storage-input${ ! isRetentionValid ? " input-error" : "" } ` }
157+ value = { retentionCustom }
158+ onChange = { ( e ) => setRetentionCustom ( e . target . value ) }
159+ placeholder = { t ( "storage.retention_days_placeholder" ) }
160+ autoFocus
161+ />
162+ < span className = "storage-input-suffix" > { t ( "storage.retention_days_suffix" ) } </ span >
163+ </ div >
164+ </ >
165+ ) : (
166+ < div className = "storage-retention-select-wrap" >
167+ < select
168+ className = "storage-select"
169+ value = { retentionSelected }
170+ onChange = { ( e ) => handleSelectChange ( e . target . value ) }
171+ >
172+ { RETENTION_PRESETS . map ( p => (
173+ < option key = { p . id } value = { p . id } > { t ( `storage.retention_options.${ p . id } ` ) } </ option >
174+ ) ) }
175+ < option value = "custom" > { t ( "storage.retention_options.custom" ) } </ option >
176+ </ select >
177+ </ div >
178+ ) }
179+ < button
180+ className = "dialog-btn"
181+ onClick = { saveRetention }
182+ disabled = { savingRetention || ! isRetentionValid || ! isRetentionDirty }
183+ >
184+ { savingRetention ? t ( "dialog.saving" ) : t ( "dialog.save" ) }
185+ </ button >
186+ </ div >
187+ </ div >
188+
74189 < div className = "storage-row" >
75190 < div className = "storage-row-label" >
76191 < FontAwesomeIcon icon = { faFileExport } />
@@ -106,4 +221,4 @@ export default ({tests, close}) => {
106221 </ div >
107222 </ >
108223 )
109- }
224+ }
0 commit comments