UserPanel.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { X } from 'lucide-react'
  2. import { parseAsString, useQueryState } from 'nuqs'
  3. import { useState } from 'react'
  4. import {
  5. Button,
  6. cn,
  7. Input,
  8. ResizableHandle,
  9. ResizablePanel,
  10. Tabs_Shadcn_,
  11. TabsContent_Shadcn_,
  12. TabsList_Shadcn_,
  13. TabsTrigger_Shadcn_,
  14. } from 'ui'
  15. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  16. import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock'
  17. import { UserLogs } from './UserLogs'
  18. import { UserOverview } from './UserOverview'
  19. import { PANEL_PADDING } from './Users.constants'
  20. import { useUserQuery } from '@/data/auth/user-query'
  21. import { User } from '@/data/auth/users-infinite-query'
  22. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  23. export const UserPanel = () => {
  24. const { data: project } = useSelectedProjectQuery()
  25. const [selectedId, setSelectedId] = useQueryState(
  26. 'show',
  27. parseAsString.withOptions({ history: 'push', clearOnDefault: true })
  28. )
  29. const [view, setView] = useState<'overview' | 'raw' | 'logs'>('overview')
  30. const [searchQuery, setSearchQuery] = useState('')
  31. const { data: selectedUser, isPending } = useUserQuery({
  32. projectRef: project?.ref,
  33. connectionString: project?.connectionString,
  34. userId: selectedId,
  35. })
  36. const filteredProperties = selectedUser
  37. ? Object.entries(selectedUser)
  38. .filter(
  39. ([key, value]) =>
  40. key.toLowerCase().includes(searchQuery.toLowerCase()) ||
  41. (typeof value === 'string' && value.toLowerCase().includes(searchQuery.toLowerCase()))
  42. )
  43. .reduce((obj, [key, value]) => {
  44. if (value !== undefined) {
  45. obj[key as keyof User] = value as any
  46. }
  47. return obj
  48. }, {} as Partial<User>)
  49. : {}
  50. return (
  51. <>
  52. <ResizableHandle withHandle />
  53. <ResizablePanel defaultSize="35" maxSize="45" minSize="35" className="bg-studio border-t">
  54. <Button
  55. type="text"
  56. className="absolute top-3 right-3 px-1"
  57. icon={<X />}
  58. onClick={() => setSelectedId(null)}
  59. />
  60. <Tabs_Shadcn_
  61. value={view}
  62. className="flex flex-col h-full"
  63. onValueChange={(value) => setView(value as 'overview' | 'raw' | 'logs')}
  64. >
  65. {isPending ? (
  66. <div>
  67. <div className="min-h-[46px] border-b" />
  68. <div className="p-5">
  69. <GenericSkeletonLoader />
  70. </div>
  71. </div>
  72. ) : !!selectedUser ? (
  73. <>
  74. <TabsList_Shadcn_ className="px-5 flex gap-x-4 min-h-[46px]">
  75. <TabsTrigger_Shadcn_
  76. value="overview"
  77. className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent shadow-none!"
  78. >
  79. Overview
  80. </TabsTrigger_Shadcn_>
  81. <TabsTrigger_Shadcn_
  82. value="logs"
  83. className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent shadow-none!"
  84. >
  85. Logs
  86. </TabsTrigger_Shadcn_>
  87. <TabsTrigger_Shadcn_
  88. value="raw"
  89. className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent shadow-none!"
  90. >
  91. Raw JSON
  92. </TabsTrigger_Shadcn_>
  93. </TabsList_Shadcn_>
  94. <TabsContent_Shadcn_
  95. value="overview"
  96. className={cn('mt-0 grow min-h-0 overflow-y-auto')}
  97. >
  98. {selectedUser && (
  99. <UserOverview user={selectedUser} onDeleteSuccess={() => setSelectedId(null)} />
  100. )}
  101. </TabsContent_Shadcn_>
  102. <TabsContent_Shadcn_ value="logs" className={cn('mt-0 grow min-h-0 overflow-y-auto')}>
  103. {selectedUser && <UserLogs user={selectedUser} />}
  104. </TabsContent_Shadcn_>
  105. <TabsContent_Shadcn_
  106. value="raw"
  107. className={cn('mt-0 grow min-h-0 overflow-y-auto', PANEL_PADDING)}
  108. >
  109. <div className="flex items-center mb-2">
  110. <Input
  111. autoFocus
  112. type="text"
  113. placeholder="Filter..."
  114. value={searchQuery}
  115. onChange={(e) => setSearchQuery(e.target.value)}
  116. className="mr-2"
  117. />
  118. <Button
  119. type="text"
  120. disabled={!searchQuery}
  121. onClick={() => setSearchQuery('')}
  122. className="text-xs"
  123. >
  124. Clear
  125. </Button>
  126. </div>
  127. <SimpleCodeBlock className="javascript" parentClassName="[&>*>span]:text-xs">
  128. {JSON.stringify(filteredProperties, null, 2)}
  129. </SimpleCodeBlock>
  130. </TabsContent_Shadcn_>
  131. </>
  132. ) : (
  133. <div className="flex items-center justify-center w-full h-full flex-col gap-y-2">
  134. <p className="text-foreground-light text-sm">
  135. Unable to find user with the following ID in project
  136. </p>
  137. <p className="text-foreground-lighter text-xs">ID: {selectedId}</p>
  138. </div>
  139. )}
  140. </Tabs_Shadcn_>
  141. </ResizablePanel>
  142. </>
  143. )
  144. }