@@ -28,6 +28,8 @@ const (
2828 screenRDSList
2929 screenRDSDetail
3030 screenRDSConfirm
31+ screenSecretList
32+ screenSecretDetail
3133 screenContextPicker
3234 screenContextAdd
3335 screenLoading
@@ -96,6 +98,14 @@ type rdsTickMsg struct {
9698 instanceID string
9799}
98100
101+ type secretsLoadedMsg struct {
102+ secrets []awsservice.Secret
103+ }
104+
105+ type secretDetailLoadedMsg struct {
106+ detail * awsservice.SecretDetail
107+ }
108+
99109// Model is the root Bubbletea model.
100110type Model struct {
101111 cfg * config.Config
@@ -146,6 +156,14 @@ type Model struct {
146156 rdsConfirmInput string // typed input for destructive action confirmation
147157 rdsPolling bool
148158
159+ // Secrets Manager browser state
160+ secrets []awsservice.Secret
161+ filteredSecrets []awsservice.Secret
162+ secretIdx int
163+ secretFilter string
164+ secretFilterActive bool
165+ selectedSecret * awsservice.SecretDetail
166+
149167 // Context picker
150168 configPath string
151169 ctxList []config.ContextInfo
@@ -251,6 +269,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
251269 m .screen = screenRDSList
252270 return m , nil
253271
272+ case secretsLoadedMsg :
273+ m .secrets = msg .secrets
274+ m .filteredSecrets = msg .secrets
275+ m .secretIdx = 0
276+ m .screen = screenSecretList
277+ return m , nil
278+
279+ case secretDetailLoadedMsg :
280+ m .selectedSecret = msg .detail
281+ m .screen = screenSecretDetail
282+ return m , nil
283+
254284 case rdsActionDoneMsg :
255285 if msg .err != nil {
256286 m .errMsg = msg .err .Error ()
@@ -366,6 +396,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
366396 return m .updateRDSDetail (msg )
367397 case screenRDSConfirm :
368398 return m .updateRDSConfirm (msg )
399+ case screenSecretList :
400+ return m .updateSecretList (msg )
401+ case screenSecretDetail :
402+ return m .updateSecretDetail (msg )
369403 case screenContextPicker :
370404 return m .updateContextPicker (msg )
371405 case screenContextAdd :
@@ -429,6 +463,9 @@ func (m Model) updateFeatureList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
429463 case domain .FeatureRDSBrowser :
430464 m .screen = screenLoading
431465 return m , m .loadRDSInstances ()
466+ case domain .FeatureSecretsBrowser :
467+ m .screen = screenLoading
468+ return m , m .loadSecrets ()
432469 }
433470 }
434471 }
@@ -1021,6 +1058,10 @@ func (m Model) View() string {
10211058 v = m .viewRDSDetail ()
10221059 case screenRDSConfirm :
10231060 v = m .viewRDSConfirm ()
1061+ case screenSecretList :
1062+ v = m .viewSecretList ()
1063+ case screenSecretDetail :
1064+ v = m .viewSecretDetail ()
10241065 case screenContextPicker :
10251066 v = m .viewContextPicker ()
10261067 case screenContextAdd :
@@ -1698,3 +1739,209 @@ func (m Model) viewRDSConfirm() string {
16981739 }
16991740 return b .String ()
17001741}
1742+
1743+ func (m Model ) loadSecrets () tea.Cmd {
1744+ return func () tea.Msg {
1745+ ctx := context .Background ()
1746+ repo , err := awsservice .NewAwsRepository (ctx , m .cfg )
1747+ if err != nil {
1748+ return errMsg {err : err }
1749+ }
1750+ secrets , err := repo .ListSecrets (ctx )
1751+ if err != nil {
1752+ return errMsg {err : err }
1753+ }
1754+ if len (secrets ) == 0 {
1755+ return errMsg {err : fmt .Errorf ("no secrets found" )}
1756+ }
1757+ return secretsLoadedMsg {secrets : secrets }
1758+ }
1759+ }
1760+
1761+ func (m Model ) loadSecretDetail (name string ) tea.Cmd {
1762+ return func () tea.Msg {
1763+ ctx := context .Background ()
1764+ repo := m .awsRepo
1765+ if repo == nil {
1766+ var err error
1767+ repo , err = awsservice .NewAwsRepository (ctx , m .cfg )
1768+ if err != nil {
1769+ return errMsg {err : err }
1770+ }
1771+ }
1772+ detail , err := repo .GetSecretDetail (ctx , name )
1773+ if err != nil {
1774+ return errMsg {err : err }
1775+ }
1776+ return secretDetailLoadedMsg {detail : detail }
1777+ }
1778+ }
1779+
1780+ func (m Model ) updateSecretList (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
1781+ key := msg .String ()
1782+
1783+ if m .secretFilterActive {
1784+ switch key {
1785+ case "esc" :
1786+ m .secretFilterActive = false
1787+ case "enter" :
1788+ m .secretFilterActive = false
1789+ case "backspace" :
1790+ if len (m .secretFilter ) > 0 {
1791+ m .secretFilter = m .secretFilter [:len (m .secretFilter )- 1 ]
1792+ m .applySecretFilter ()
1793+ }
1794+ default :
1795+ if len (key ) == 1 {
1796+ m .secretFilter += key
1797+ m .applySecretFilter ()
1798+ }
1799+ }
1800+ return m , nil
1801+ }
1802+
1803+ switch key {
1804+ case "q" , "esc" :
1805+ m .screen = screenFeatureList
1806+ m .secretFilter = ""
1807+ m .filteredSecrets = m .secrets
1808+ m .secretIdx = 0
1809+ case "up" , "k" :
1810+ if m .secretIdx > 0 {
1811+ m .secretIdx --
1812+ }
1813+ case "down" , "j" :
1814+ if m .secretIdx < len (m .filteredSecrets )- 1 {
1815+ m .secretIdx ++
1816+ }
1817+ case "/" :
1818+ m .secretFilterActive = true
1819+ case "enter" :
1820+ if len (m .filteredSecrets ) > 0 && m .secretIdx < len (m .filteredSecrets ) {
1821+ selected := m .filteredSecrets [m .secretIdx ]
1822+ m .screen = screenLoading
1823+ return m , m .loadSecretDetail (selected .Name )
1824+ }
1825+ }
1826+ return m , nil
1827+ }
1828+
1829+ func (m Model ) updateSecretDetail (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
1830+ switch msg .String () {
1831+ case "q" , "esc" :
1832+ m .selectedSecret = nil
1833+ m .screen = screenSecretList
1834+ }
1835+ return m , nil
1836+ }
1837+
1838+ func (m * Model ) applySecretFilter () {
1839+ if m .secretFilter == "" {
1840+ m .filteredSecrets = m .secrets
1841+ } else {
1842+ query := strings .ToLower (m .secretFilter )
1843+ var result []awsservice.Secret
1844+ for _ , s := range m .secrets {
1845+ if strings .Contains (s .FilterText (), query ) {
1846+ result = append (result , s )
1847+ }
1848+ }
1849+ m .filteredSecrets = result
1850+ }
1851+ m .secretIdx = 0
1852+ }
1853+
1854+ func (m Model ) viewSecretList () string {
1855+ var b strings.Builder
1856+ b .WriteString (m .renderStatusBar ())
1857+ b .WriteString (titleStyle .Render ("Secrets Manager" ))
1858+ b .WriteString ("\n " )
1859+
1860+ if m .secretFilterActive {
1861+ b .WriteString (filterStyle .Render (fmt .Sprintf ("Filter: %s▏" , m .secretFilter )))
1862+ } else if m .secretFilter != "" {
1863+ b .WriteString (dimStyle .Render (fmt .Sprintf ("Filter: %s" , m .secretFilter )))
1864+ }
1865+ b .WriteString ("\n \n " )
1866+
1867+ if len (m .filteredSecrets ) == 0 {
1868+ b .WriteString (dimStyle .Render (" No matching secrets" ))
1869+ b .WriteString ("\n " )
1870+ } else {
1871+ visibleLines := max (m .height - 8 , 5 )
1872+ start := 0
1873+ if m .secretIdx >= visibleLines {
1874+ start = m .secretIdx - visibleLines + 1
1875+ }
1876+ end := min (start + visibleLines , len (m .filteredSecrets ))
1877+
1878+ for i := start ; i < end ; i ++ {
1879+ s := m .filteredSecrets [i ]
1880+ cursor := " "
1881+ style := normalStyle
1882+ if i == m .secretIdx {
1883+ cursor = "> "
1884+ style = selectedStyle
1885+ }
1886+ b .WriteString (style .Render (fmt .Sprintf ("%s%s" , cursor , s .DisplayTitle ())))
1887+ b .WriteString ("\n " )
1888+ }
1889+
1890+ b .WriteString ("\n " )
1891+ b .WriteString (dimStyle .Render (fmt .Sprintf (" %d/%d secrets" , len (m .filteredSecrets ), len (m .secrets ))))
1892+ }
1893+
1894+ b .WriteString ("\n " )
1895+ b .WriteString (dimStyle .Render ("↑/↓: navigate • /: filter • enter: detail • esc: back • H: home" ))
1896+ return b .String ()
1897+ }
1898+
1899+ func (m Model ) viewSecretDetail () string {
1900+ if m .selectedSecret == nil {
1901+ return ""
1902+ }
1903+ d := m .selectedSecret
1904+ var b strings.Builder
1905+ b .WriteString (m .renderStatusBar ())
1906+ b .WriteString (titleStyle .Render ("Secret Detail" ))
1907+ b .WriteString ("\n \n " )
1908+
1909+ labelStyle := lipgloss .NewStyle ().Width (14 )
1910+ b .WriteString (normalStyle .Render (fmt .Sprintf (" %s%s" , labelStyle .Render ("Name" ), d .Name )))
1911+ b .WriteString ("\n " )
1912+
1913+ kmsKey := d .KMSKeyID
1914+ if kmsKey == "" {
1915+ kmsKey = dimStyle .Render ("(aws/secretsmanager)" )
1916+ }
1917+ b .WriteString (normalStyle .Render (fmt .Sprintf (" %s%s" , labelStyle .Render ("Encryption Key" ), kmsKey )))
1918+ b .WriteString ("\n \n " )
1919+
1920+ if len (d .Values ) > 0 {
1921+ b .WriteString (titleStyle .Render ("Key / Value" ))
1922+ b .WriteString ("\n \n " )
1923+
1924+ keys := make ([]string , 0 , len (d .Values ))
1925+ for k := range d .Values {
1926+ keys = append (keys , k )
1927+ }
1928+ for i := 1 ; i < len (keys ); i ++ {
1929+ for j := i ; j > 0 && keys [j ] < keys [j - 1 ]; j -- {
1930+ keys [j ], keys [j - 1 ] = keys [j - 1 ], keys [j ]
1931+ }
1932+ }
1933+
1934+ for _ , k := range keys {
1935+ b .WriteString (fmt .Sprintf (" %s %s\n " , dimStyle .Render (k ), normalStyle .Render (d .Values [k ])))
1936+ }
1937+ } else if d .Raw != "" {
1938+ b .WriteString (titleStyle .Render ("Value" ))
1939+ b .WriteString ("\n \n " )
1940+ b .WriteString (normalStyle .Render (fmt .Sprintf (" %s" , d .Raw )))
1941+ b .WriteString ("\n " )
1942+ }
1943+
1944+ b .WriteString ("\n " )
1945+ b .WriteString (dimStyle .Render ("esc: back • H: home" ))
1946+ return b .String ()
1947+ }
0 commit comments