11import './MyOpenCRE.scss' ;
22
3- import React , { useState } from 'react' ;
3+ import React , { useRef , useState } from 'react' ;
44import { Button , Container , Form , Header , Message } from 'semantic-ui-react' ;
55
66import { useEnvironment } from '../../hooks' ;
@@ -21,17 +21,52 @@ type ImportErrorResponse = {
2121
2222export const MyOpenCRE = ( ) => {
2323 const { apiUrl } = useEnvironment ( ) ;
24-
2524 const isUploadEnabled = apiUrl !== '/rest/v1' ;
2625
2726 const [ selectedFile , setSelectedFile ] = useState < File | null > ( null ) ;
2827 const [ loading , setLoading ] = useState ( false ) ;
2928 const [ error , setError ] = useState < ImportErrorResponse | null > ( null ) ;
3029 const [ success , setSuccess ] = useState < any | null > ( null ) ;
3130
32- // informational (no-op / empty) messages
31+ const [ preview , setPreview ] = useState < {
32+ rows : number ;
33+ creMappings : number ;
34+ uniqueSections : number ;
35+ creColumns : string [ ] ;
36+ } | null > ( null ) ;
37+
3338 const [ info , setInfo ] = useState < string | null > ( null ) ;
39+ const [ confirmedImport , setConfirmedImport ] = useState ( false ) ;
40+
41+ const fileInputRef = useRef < HTMLInputElement | null > ( null ) ;
42+
43+ /* ------------------ FILE SELECTION ------------------ */
44+
45+ const onFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
46+ setError ( null ) ;
47+ setSuccess ( null ) ;
48+ setInfo ( null ) ;
49+ setConfirmedImport ( false ) ;
50+ setPreview ( null ) ;
51+
52+ if ( ! e . target . files || e . target . files . length === 0 ) return ;
53+
54+ const file = e . target . files [ 0 ] ;
55+
56+ if ( ! file . name . toLowerCase ( ) . endsWith ( '.csv' ) ) {
57+ setError ( {
58+ success : false ,
59+ type : 'FILE_ERROR' ,
60+ message : 'Please upload a valid CSV file.' ,
61+ } ) ;
62+ e . target . value = '' ;
63+ setSelectedFile ( null ) ;
64+ return ;
65+ }
3466
67+ setSelectedFile ( file ) ;
68+ generateCsvPreview ( file ) ;
69+ } ;
3570 /* ------------------ CSV DOWNLOAD ------------------ */
3671
3772 const downloadCreCsv = async ( ) => {
@@ -61,41 +96,15 @@ export const MyOpenCRE = () => {
6196 alert ( 'Failed to download CRE CSV' ) ;
6297 }
6398 } ;
64-
65- /* ------------------ FILE SELECTION ------------------ */
66-
67- const onFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
68- setError ( null ) ;
69- setSuccess ( null ) ;
70- setInfo ( null ) ; // lear stale info messages
71-
72- if ( ! e . target . files || e . target . files . length === 0 ) return ;
73-
74- const file = e . target . files [ 0 ] ;
75-
76- if ( ! file . name . toLowerCase ( ) . endsWith ( '.csv' ) ) {
77- setError ( {
78- success : false ,
79- type : 'FILE_ERROR' ,
80- message : 'Please upload a valid CSV file.' ,
81- } ) ;
82- e . target . value = '' ;
83- setSelectedFile ( null ) ;
84- return ;
85- }
86-
87- setSelectedFile ( file ) ;
88- } ;
89-
9099 /* ------------------ CSV UPLOAD ------------------ */
91100
92101 const uploadCsv = async ( ) => {
93- if ( ! selectedFile ) return ;
102+ if ( ! selectedFile || ! confirmedImport ) return ;
94103
95104 setLoading ( true ) ;
96105 setError ( null ) ;
97106 setSuccess ( null ) ;
98- setInfo ( null ) ; // reset info on upload
107+ setInfo ( null ) ;
99108
100109 const formData = new FormData ( ) ;
101110 formData . append ( 'cre_csv' , selectedFile ) ;
@@ -116,10 +125,11 @@ export const MyOpenCRE = () => {
116125
117126 if ( ! response . ok ) {
118127 setError ( payload ) ;
128+ setPreview ( null ) ;
129+ setConfirmedImport ( false ) ;
119130 return ;
120131 }
121132
122- // handle backend import_type semantics
123133 if ( payload . import_type === 'noop' ) {
124134 setInfo (
125135 'Import completed successfully, but no new CREs or standards were added because all mappings already exist.'
@@ -130,13 +140,19 @@ export const MyOpenCRE = () => {
130140 setSuccess ( payload ) ;
131141 }
132142
133- setSelectedFile ( null ) ;
143+ setConfirmedImport ( false ) ;
144+ setPreview ( null ) ;
145+ if ( fileInputRef . current ) {
146+ fileInputRef . current . value = '' ;
147+ }
134148 } catch ( err : any ) {
135149 setError ( {
136150 success : false ,
137151 type : 'CLIENT_ERROR' ,
138152 message : err . message || 'Unexpected error during import' ,
139153 } ) ;
154+ setPreview ( null ) ;
155+ setConfirmedImport ( false ) ;
140156 } finally {
141157 setLoading ( false ) ;
142158 }
@@ -165,22 +181,58 @@ export const MyOpenCRE = () => {
165181 return < Message negative > { error . message || 'Import failed' } </ Message > ;
166182 } ;
167183
184+ /* ------------------ CSV PREVIEW ------------------ */
185+
186+ const generateCsvPreview = async ( file : File ) => {
187+ const text = await file . text ( ) ;
188+ const lines = text . split ( '\n' ) . filter ( Boolean ) ;
189+
190+ if ( lines . length < 2 ) {
191+ setPreview ( null ) ;
192+ return ;
193+ }
194+
195+ const headers = lines [ 0 ] . split ( ',' ) . map ( ( h ) => h . trim ( ) ) ;
196+ const rows = lines . slice ( 1 ) ;
197+
198+ const creColumns = headers . filter ( ( h ) => h . startsWith ( 'CRE' ) ) ;
199+ let creMappings = 0 ;
200+ const sectionSet = new Set < string > ( ) ;
201+
202+ rows . forEach ( ( line ) => {
203+ const values = line . split ( ',' ) ;
204+ const rowObj : Record < string , string > = { } ;
205+
206+ headers . forEach ( ( h , i ) => {
207+ rowObj [ h ] = ( values [ i ] || '' ) . trim ( ) ;
208+ } ) ;
209+
210+ const name = ( rowObj [ 'standard|name' ] || '' ) . trim ( ) ;
211+ const id = ( rowObj [ 'standard|id' ] || '' ) . trim ( ) ;
212+
213+ if ( name || id ) {
214+ sectionSet . add ( `${ name } |${ id } ` ) ;
215+ }
216+
217+ creColumns . forEach ( ( col ) => {
218+ if ( rowObj [ col ] ) creMappings += 1 ;
219+ } ) ;
220+ } ) ;
221+
222+ setPreview ( {
223+ rows : rows . length ,
224+ creMappings,
225+ uniqueSections : sectionSet . size ,
226+ creColumns,
227+ } ) ;
228+ } ;
229+
168230 /* ------------------ UI ------------------ */
169231
170232 return (
171233 < Container className = "myopencre-container" >
172234 < Header as = "h1" > MyOpenCRE</ Header >
173235
174- < p >
175- MyOpenCRE allows you to map your own security standard (e.g. SOC2) to OpenCRE Common Requirements
176- using a CSV spreadsheet.
177- </ p >
178-
179- < p >
180- Start by downloading the CRE catalogue below, then map your standard’s controls or sections to CRE IDs
181- in the spreadsheet.
182- </ p >
183-
184236 < div className = "myopencre-section" >
185237 < Button primary onClick = { downloadCreCsv } >
186238 Download CRE Catalogue (CSV)
@@ -190,21 +242,8 @@ export const MyOpenCRE = () => {
190242 < div className = "myopencre-section myopencre-upload" >
191243 < Header as = "h3" > Upload Mapping CSV</ Header >
192244
193- < p > Upload your completed mapping spreadsheet to import your standard into OpenCRE.</ p >
194-
195- { ! isUploadEnabled && (
196- < Message info className = "myopencre-disabled" >
197- CSV upload is disabled on hosted environments due to resource constraints.
198- < br />
199- Please run OpenCRE locally to enable standard imports.
200- </ Message >
201- ) }
202-
203245 { renderErrorMessage ( ) }
204-
205- { /* informational messages for noop / empty */ }
206246 { info && < Message info > { info } </ Message > }
207-
208247 { success && (
209248 < Message positive >
210249 < strong > Import successful</ strong >
@@ -215,15 +254,62 @@ export const MyOpenCRE = () => {
215254 </ Message >
216255 ) }
217256
257+ { confirmedImport && ! loading && ! success && ! error && (
258+ < Message positive >
259+ CSV validated successfully. Click < strong > Upload CSV</ strong > to start importing.
260+ </ Message >
261+ ) }
262+
263+ { preview && (
264+ < Message info className = "myopencre-preview" >
265+ < strong > Import Preview</ strong >
266+ < ul >
267+ < li > Rows detected: { preview . rows } </ li >
268+ < li > CRE mappings found: { preview . creMappings } </ li >
269+ < li > Unique standard sections: { preview . uniqueSections } </ li >
270+ < li > CRE columns detected: { preview . creColumns . join ( ', ' ) } </ li >
271+ </ ul >
272+
273+ < Button
274+ primary
275+ size = "small"
276+ onClick = { ( ) => {
277+ setPreview ( null ) ;
278+ setConfirmedImport ( true ) ;
279+ } }
280+ >
281+ Confirm Import
282+ </ Button >
283+
284+ < Button
285+ size = "small"
286+ onClick = { ( ) => {
287+ setPreview ( null ) ;
288+ setConfirmedImport ( false ) ;
289+ setSelectedFile ( null ) ;
290+ if ( fileInputRef . current ) fileInputRef . current . value = '' ;
291+ } }
292+ >
293+ Cancel
294+ </ Button >
295+ </ Message >
296+ ) }
297+
218298 < Form >
219299 < Form . Field >
220- < input type = "file" accept = ".csv" disabled = { ! isUploadEnabled || loading } onChange = { onFileChange } />
300+ < input
301+ ref = { fileInputRef }
302+ type = "file"
303+ accept = ".csv"
304+ disabled = { ! isUploadEnabled || loading || ! ! preview }
305+ onChange = { onFileChange }
306+ />
221307 </ Form . Field >
222308
223309 < Button
224310 primary
225311 loading = { loading }
226- disabled = { ! isUploadEnabled || ! selectedFile || loading }
312+ disabled = { ! isUploadEnabled || ! selectedFile || ! confirmedImport || loading }
227313 onClick = { uploadCsv }
228314 >
229315 Upload CSV
0 commit comments