1- import React , { useState , useMemo } from 'react'
1+ import React , { useState , useMemo , useEffect , useRef , useCallback } from 'react'
22import { DataTable , Table } from '@primer/react/experimental'
3- import { TextInput , ActionMenu , ActionList , Pagination } from '@primer/react'
3+ import { TextInput , ActionMenu , ActionList , Pagination , Button } from '@primer/react'
4+ import debounce from 'lodash/debounce'
45import { useTranslation } from '@/languages/components/useTranslation'
6+ import { sendEvent } from '@/events/components/events'
7+ import { EventType } from '@/events/types'
8+ import { sanitizeSearchQuery } from '@/search/lib/sanitize-search-query'
59import type { SecretScanningData } from '@/types'
610
711const PAGE_SIZE = 25
812
13+ // Identifies this table in the docs.v0.TableInteractionEvent analytics.
14+ const TABLE_INTERACTION_NAME = 'secret-scanning-patterns'
15+
16+ // Maps DataTable column ids to the canonical analytics field name so that a
17+ // filter and a sort on the same column report the same
18+ // table_interaction_field_name. Filter keys already use these canonical names.
19+ const COLUMN_FIELD_NAMES : Record < string , string > = {
20+ provider : 'provider' ,
21+ supportedSecret : 'secret' ,
22+ isPublic : 'partnerAlert' ,
23+ isPrivateWithGhas : 'userAlert' ,
24+ hasPushProtection : 'pushProtection' ,
25+ hasValidityCheck : 'validityCheck' ,
26+ hasExtendedMetadata : 'metadata' ,
27+ base64Supported : 'base64' ,
28+ }
29+
930type SecretScanningRow = SecretScanningData & { id : string }
1031
1132type FilterState = {
@@ -17,20 +38,81 @@ type FilterState = {
1738 base64 : 'all' | 'yes' | 'no'
1839}
1940
41+ type FilterKey = Exclude < keyof FilterState , 'search' >
42+
43+ const DEFAULT_FILTERS : FilterState = {
44+ search : '' ,
45+ pushProtection : 'all' ,
46+ validityCheck : 'all' ,
47+ partnerAlert : 'all' ,
48+ metadata : 'all' ,
49+ base64 : 'all' ,
50+ }
51+
52+ type TableInteractionType = 'search' | 'filter' | 'sort' | 'paginate' | 'reset'
53+
2054export function SecretScanningTable ( { data } : { data : SecretScanningData [ ] } ) {
2155 const { t } = useTranslation ( 'secret_scanning' )
22- const [ filters , setFilters ] = useState < FilterState > ( {
23- search : '' ,
24- pushProtection : 'all' ,
25- validityCheck : 'all' ,
26- partnerAlert : 'all' ,
27- metadata : 'all' ,
28- base64 : 'all' ,
29- } )
56+ const [ filters , setFilters ] = useState < FilterState > ( DEFAULT_FILTERS )
3057 const [ currentPage , setCurrentPage ] = useState ( 1 )
3158 const [ sortColumn , setSortColumn ] = useState < string | undefined > ( undefined )
3259 const [ sortDirection , setSortDirection ] = useState < 'ASC' | 'DESC' > ( 'ASC' )
3360
61+ // Emit a TableInteractionEvent for analytics (github/docs-engineering#6593).
62+ const trackInteraction = useCallback (
63+ ( interactionType : TableInteractionType , fieldName ?: string , fieldValue ?: string ) => {
64+ sendEvent ( {
65+ type : EventType . tableInteraction ,
66+ table_interaction_name : TABLE_INTERACTION_NAME ,
67+ table_interaction_type : interactionType ,
68+ table_interaction_field_name : fieldName ,
69+ table_interaction_field_value : fieldValue ,
70+ } )
71+ } ,
72+ [ ] ,
73+ )
74+
75+ // Debounce search tracking so we record the settled query, not every keystroke.
76+ const debouncedTrackSearchRef = useRef < ReturnType < typeof debounce > | null > ( null )
77+ useEffect ( ( ) => {
78+ debouncedTrackSearchRef . current = debounce ( ( query : string ) => {
79+ // Sanitize before logging: users may paste a real secret into this
80+ // table's search to check support, and the query is sent to analytics.
81+ trackInteraction ( 'search' , 'search' , sanitizeSearchQuery ( query ) )
82+ } , 500 )
83+ return ( ) => {
84+ debouncedTrackSearchRef . current ?. flush ( )
85+ debouncedTrackSearchRef . current ?. cancel ( )
86+ }
87+ } , [ trackInteraction ] )
88+
89+ const handleFilterChange = useCallback (
90+ ( field : FilterKey , value : 'all' | 'yes' | 'no' ) => {
91+ setFilters ( ( f ) => ( { ...f , [ field ] : value } ) )
92+ setCurrentPage ( 1 )
93+ trackInteraction ( 'filter' , field , value )
94+ } ,
95+ [ trackInteraction ] ,
96+ )
97+
98+ const handleReset = useCallback ( ( ) => {
99+ setFilters ( DEFAULT_FILTERS )
100+ setCurrentPage ( 1 )
101+ setSortColumn ( undefined )
102+ setSortDirection ( 'ASC' )
103+ debouncedTrackSearchRef . current ?. cancel ( )
104+ trackInteraction ( 'reset' )
105+ } , [ trackInteraction ] )
106+
107+ const hasActiveFilters =
108+ filters . search !== '' ||
109+ filters . pushProtection !== 'all' ||
110+ filters . validityCheck !== 'all' ||
111+ filters . partnerAlert !== 'all' ||
112+ filters . metadata !== 'all' ||
113+ filters . base64 !== 'all' ||
114+ sortColumn !== undefined
115+
34116 // Add stable IDs once based on original data order
35117 const dataWithIds : SecretScanningRow [ ] = useMemo ( ( ) => {
36118 return data . map ( ( entry , i ) => ( { ...entry , id : `${ entry . secretType } -${ i } ` } ) )
@@ -92,51 +174,49 @@ export function SecretScanningTable({ data }: { data: SecretScanningData[] }) {
92174 < FilterDropdown
93175 label = { t ( 'filter_push_protection' ) }
94176 value = { filters . pushProtection }
95- onChange = { ( v ) => {
96- setFilters ( ( f ) => ( { ...f , pushProtection : v } ) )
97- setCurrentPage ( 1 )
98- } }
177+ onChange = { ( v ) => handleFilterChange ( 'pushProtection' , v ) }
99178 />
100179 < FilterDropdown
101180 label = { t ( 'filter_validity_check' ) }
102181 value = { filters . validityCheck }
103- onChange = { ( v ) => {
104- setFilters ( ( f ) => ( { ...f , validityCheck : v } ) )
105- setCurrentPage ( 1 )
106- } }
182+ onChange = { ( v ) => handleFilterChange ( 'validityCheck' , v ) }
107183 />
108184 < FilterDropdown
109185 label = { t ( 'filter_partner_alert' ) }
110186 value = { filters . partnerAlert }
111- onChange = { ( v ) => {
112- setFilters ( ( f ) => ( { ...f , partnerAlert : v } ) )
113- setCurrentPage ( 1 )
114- } }
187+ onChange = { ( v ) => handleFilterChange ( 'partnerAlert' , v ) }
115188 />
116189 < FilterDropdown
117190 label = { t ( 'filter_metadata' ) }
118191 value = { filters . metadata }
119- onChange = { ( v ) => {
120- setFilters ( ( f ) => ( { ...f , metadata : v } ) )
121- setCurrentPage ( 1 )
122- } }
192+ onChange = { ( v ) => handleFilterChange ( 'metadata' , v ) }
123193 />
124194 < FilterDropdown
125195 label = { t ( 'filter_base64' ) }
126196 value = { filters . base64 }
127- onChange = { ( v ) => {
128- setFilters ( ( f ) => ( { ...f , base64 : v } ) )
129- setCurrentPage ( 1 )
130- } }
197+ onChange = { ( v ) => handleFilterChange ( 'base64' , v ) }
131198 />
199+ { hasActiveFilters && (
200+ < Button
201+ variant = "invisible"
202+ size = "small"
203+ onClick = { handleReset }
204+ aria-label = { t ( 'clear_filters_aria_label' ) }
205+ >
206+ { t ( 'clear_filters' ) }
207+ </ Button >
208+ ) }
132209 </ div >
133210 < TextInput
134211 aria-label = { t ( 'search_aria_label' ) }
135212 placeholder = { t ( 'search_placeholder' ) }
136213 value = { filters . search }
137214 onChange = { ( e ) => {
138- setFilters ( ( f ) => ( { ...f , search : e . target . value } ) )
215+ const value = e . target . value
216+ setFilters ( ( f ) => ( { ...f , search : value } ) )
139217 setCurrentPage ( 1 )
218+ if ( value . trim ( ) ) debouncedTrackSearchRef . current ?.( value )
219+ else debouncedTrackSearchRef . current ?. cancel ( )
140220 } }
141221 />
142222 </ div >
@@ -168,6 +248,11 @@ export function SecretScanningTable({ data }: { data: SecretScanningData[] }) {
168248 setSortColumn ( String ( columnId ) )
169249 setSortDirection ( direction )
170250 setCurrentPage ( 1 )
251+ trackInteraction (
252+ 'sort' ,
253+ COLUMN_FIELD_NAMES [ String ( columnId ) ] ?? String ( columnId ) ,
254+ direction ,
255+ )
171256 } }
172257 columns = { [
173258 {
@@ -280,7 +365,10 @@ export function SecretScanningTable({ data }: { data: SecretScanningData[] }) {
280365 aria-label = { t ( 'pagination_label' ) }
281366 pageCount = { pageCount }
282367 currentPage = { currentPage }
283- onPageChange = { ( _e : React . MouseEvent , page : number ) => setCurrentPage ( page ) }
368+ onPageChange = { ( _e : React . MouseEvent , page : number ) => {
369+ setCurrentPage ( page )
370+ trackInteraction ( 'paginate' , 'page' , String ( page ) )
371+ } }
284372 />
285373 ) }
286374 </ div >
0 commit comments