@@ -3,51 +3,103 @@ import ContentBox from '@/components/elements/ContentBox';
33import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm' ;
44import getApiKeys , { ApiKey } from '@/api/account/getApiKeys' ;
55import SpinnerOverlay from '@/components/elements/SpinnerOverlay' ;
6- import { Simulate } from 'react-dom/test-utils' ;
76import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' ;
87import { faKey } from '@fortawesome/free-solid-svg-icons/faKey' ;
98import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt' ;
9+ import ConfirmationModal from '@/components/elements/ConfirmationModal' ;
10+ import deleteApiKey from '@/api/account/deleteApiKey' ;
11+ import { Actions , useStoreActions } from 'easy-peasy' ;
12+ import { ApplicationStore } from '@/state' ;
13+ import FlashMessageRender from '@/components/FlashMessageRender' ;
14+ import { httpErrorToHuman } from '@/api/http' ;
15+ import format from 'date-fns/format' ;
1016
1117export default ( ) => {
18+ const [ deleteIdentifier , setDeleteIdentifier ] = useState ( '' ) ;
1219 const [ keys , setKeys ] = useState < ApiKey [ ] > ( [ ] ) ;
1320 const [ loading , setLoading ] = useState ( true ) ;
21+ const { addError, clearFlashes } = useStoreActions ( ( actions : Actions < ApplicationStore > ) => actions . flashes ) ;
1422
1523 useEffect ( ( ) => {
24+ clearFlashes ( 'account' ) ;
1625 getApiKeys ( )
1726 . then ( keys => setKeys ( keys ) )
1827 . then ( ( ) => setLoading ( false ) )
1928 . catch ( error => {
2029 console . error ( error ) ;
30+ addError ( { key : 'account' , message : httpErrorToHuman ( error ) } ) ;
2131 } ) ;
2232 } , [ ] ) ;
2333
34+ const doDeletion = ( identifier : string ) => {
35+ setLoading ( true ) ;
36+ clearFlashes ( 'account' ) ;
37+ deleteApiKey ( identifier )
38+ . then ( ( ) => setKeys ( s => ( [
39+ ...( s || [ ] ) . filter ( key => key . identifier !== identifier ) ,
40+ ] ) ) )
41+ . catch ( error => {
42+ console . error ( error ) ;
43+ addError ( { key : 'account' , message : httpErrorToHuman ( error ) } ) ;
44+ } )
45+ . then ( ( ) => setLoading ( false ) ) ;
46+ } ;
47+
2448 return (
2549 < div className = { 'my-10 flex' } >
26- < ContentBox title = { 'Create API Key' } className = { 'flex-1' } showFlashes = { 'account' } >
27- < CreateApiKeyForm />
50+ < FlashMessageRender byKey = { 'account' } className = { 'mb-4' } />
51+ < ContentBox title = { 'Create API Key' } className = { 'flex-1' } >
52+ < CreateApiKeyForm onKeyCreated = { key => setKeys ( s => ( [ ...s ! , key ] ) ) } />
2853 </ ContentBox >
2954 < ContentBox title = { 'API Keys' } className = { 'ml-10 flex-1' } >
3055 < SpinnerOverlay visible = { loading } />
56+ { deleteIdentifier &&
57+ < ConfirmationModal
58+ title = { 'Confirm key deletion' }
59+ buttonText = { 'Yes, delete key' }
60+ visible = { true }
61+ onConfirmed = { ( ) => {
62+ doDeletion ( deleteIdentifier ) ;
63+ setDeleteIdentifier ( '' ) ;
64+ } }
65+ onCanceled = { ( ) => setDeleteIdentifier ( '' ) }
66+ >
67+ Are you sure you wish to delete this API key? All requests using it will immediately be
68+ invalidated and will fail.
69+ </ ConfirmationModal >
70+ }
3171 {
32- keys . map ( key => (
33- < div key = { key . identifier } className = { 'grey-row-box bg-neutral-600 mb-2 flex items-center' } >
34- < FontAwesomeIcon icon = { faKey } className = { 'text-neutral-300' } />
35- < p className = { 'text-sm ml-4 flex-1' } >
36- { key . description }
37- </ p >
38- < p className = { 'text-sm ml-4' } >
39- < code className = { 'font-mono py-1 px-2 bg-neutral-900 rounded' } >
40- { key . identifier }
41- </ code >
42- </ p >
43- < button className = { 'ml-4 p-2 text-sm' } >
44- < FontAwesomeIcon
45- icon = { faTrashAlt }
46- className = { 'text-neutral-400 hover:text-red-400 transition-color duration-150' }
47- />
48- </ button >
49- </ div >
50- ) )
72+ keys . length === 0 ?
73+ < p className = { 'text-center text-sm' } >
74+ { loading ? 'Loading...' : 'No API keys exist for this account.' }
75+ </ p >
76+ :
77+ keys . map ( key => (
78+ < div key = { key . identifier } className = { 'grey-row-box bg-neutral-600 mb-2 flex items-center' } >
79+ < FontAwesomeIcon icon = { faKey } className = { 'text-neutral-300' } />
80+ < div className = { 'ml-4 flex-1' } >
81+ < p className = { 'text-sm' } > { key . description } </ p >
82+ < p className = { 'text-2xs text-neutral-300 uppercase' } >
83+ Last
84+ used: { key . lastUsedAt ? format ( key . lastUsedAt , 'MMM Do, YYYY HH:mm' ) : 'Never' }
85+ </ p >
86+ </ div >
87+ < p className = { 'text-sm ml-4' } >
88+ < code className = { 'font-mono py-1 px-2 bg-neutral-900 rounded' } >
89+ { key . identifier }
90+ </ code >
91+ </ p >
92+ < button
93+ className = { 'ml-4 p-2 text-sm' }
94+ onClick = { ( ) => setDeleteIdentifier ( key . identifier ) }
95+ >
96+ < FontAwesomeIcon
97+ icon = { faTrashAlt }
98+ className = { 'text-neutral-400 hover:text-red-400 transition-color duration-150' }
99+ />
100+ </ button >
101+ </ div >
102+ ) )
51103 }
52104 </ ContentBox >
53105 </ div >
0 commit comments