DashboardPreferences.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { LOCAL_STORAGE_KEYS, useParams } from 'common'
  3. import { HelpCircle } from 'lucide-react'
  4. import { useForm } from 'react-hook-form'
  5. import { toast } from 'sonner'
  6. import {
  7. Button,
  8. Card,
  9. CardContent,
  10. CardFooter,
  11. Dialog,
  12. DialogClose,
  13. DialogContent,
  14. DialogFooter,
  15. DialogHeader,
  16. DialogSection,
  17. DialogSectionSeparator,
  18. DialogTitle,
  19. DialogTrigger,
  20. Form,
  21. FormControl,
  22. FormField,
  23. } from 'ui'
  24. import { Admonition } from 'ui-patterns'
  25. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  26. import {
  27. PageSection,
  28. PageSectionContent,
  29. PageSectionMeta,
  30. PageSectionSummary,
  31. PageSectionTitle,
  32. } from 'ui-patterns/PageSection'
  33. import ShimmeringLoader, { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  34. import * as z from 'zod'
  35. import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
  36. import { InlineLink } from '@/components/ui/InlineLink'
  37. import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
  38. import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
  39. const formSchema = z.object({
  40. defaultDatabase: z.string().optional(),
  41. })
  42. export type DashboardPreference = z.infer<typeof formSchema>
  43. const DEFAULT_PREFERENCE: DashboardPreference = {
  44. defaultDatabase: undefined,
  45. }
  46. /**
  47. * [Joshen] JFYI am not convinced about the UX of this, will iterate over time
  48. * and only release to public when we're satisfied with how it behaves
  49. * - Where should "Dashboard preferences" live
  50. * - Preferences currently only apply to the user via local storage until we have middleware support that will persist the setting on the project for all users
  51. * - Should selecting which database to run queries for dashboard be an option for users to configure (or for us to just default to)
  52. * - I'd love for this to work seamlessly, but main concern atm is latency which is region dependent
  53. * - Also, current database logic only applies to Table Editor atm, will need to extend it further to other pages
  54. */
  55. export const DashboardPreferences = () => {
  56. const { ref: projectRef } = useParams()
  57. const [dashboardPreferences, setDashboardPreferences, { isLoading }] =
  58. useLocalStorageQuery<DashboardPreference>(
  59. LOCAL_STORAGE_KEYS.DASHBOARD_PREFERENCES(projectRef ?? '_'),
  60. DEFAULT_PREFERENCE
  61. )
  62. const { isPending } = useReadReplicasQuery({ projectRef })
  63. const form = useForm<z.infer<typeof formSchema>>({
  64. resolver: zodResolver(formSchema as any),
  65. defaultValues: dashboardPreferences,
  66. values: dashboardPreferences,
  67. mode: 'onSubmit',
  68. reValidateMode: 'onBlur',
  69. })
  70. const onSubmit = async (values: DashboardPreference) => {
  71. if (!projectRef) return console.error('Ref is required')
  72. setDashboardPreferences(values)
  73. form.reset(values)
  74. toast.success('Successfully saved dashboard preferences!')
  75. }
  76. return (
  77. <PageSection>
  78. <PageSectionMeta>
  79. <PageSectionSummary>
  80. <PageSectionTitle id="queries">Queries</PageSectionTitle>
  81. </PageSectionSummary>
  82. </PageSectionMeta>
  83. <PageSectionContent className="flex flex-col gap-y-4">
  84. {/* [Joshen] Ideally we're able to persist this for all users in the project, but will need support in our middleware */}
  85. <Admonition
  86. type="note"
  87. description="These preferences control only your experience in the dashboard. Other members of this project will not be affected."
  88. />
  89. {isLoading ? (
  90. <Card>
  91. <CardContent>
  92. <GenericSkeletonLoader />
  93. </CardContent>
  94. </Card>
  95. ) : (
  96. <Form {...form}>
  97. <form onSubmit={form.handleSubmit(onSubmit)}>
  98. <Card>
  99. <CardContent>
  100. <FormField
  101. control={form.control}
  102. name="defaultDatabase"
  103. render={({ field }) => (
  104. <FormItemLayout
  105. layout="flex-row-reverse"
  106. label="Preferred database for dashboard queries"
  107. description={
  108. <p>
  109. All read queries from the dashboard will run against the selected
  110. database by default
  111. <DashboardQueriesDialog />
  112. </p>
  113. }
  114. className="[&>div]:md:w-1/2"
  115. >
  116. {isPending ? (
  117. <ShimmeringLoader />
  118. ) : (
  119. <FormControl>
  120. {/* [Joshen] Need to disable unhealthy replicas */}
  121. <DatabaseSelector
  122. isForm
  123. buttonProps={{ size: 'small', className: 'w-full' }}
  124. selectedDatabaseId={field.value ?? projectRef}
  125. onSelectId={(id) =>
  126. field.onChange(id === projectRef ? undefined : id)
  127. }
  128. />
  129. </FormControl>
  130. )}
  131. </FormItemLayout>
  132. )}
  133. />
  134. </CardContent>
  135. <CardFooter className="justify-end space-x-2">
  136. {form.formState.isDirty && (
  137. <Button
  138. type="default"
  139. htmlType="button"
  140. onClick={() => form.reset(dashboardPreferences)}
  141. >
  142. Cancel
  143. </Button>
  144. )}
  145. <Button type="primary" htmlType="submit" disabled={!form.formState.isDirty}>
  146. Save changes
  147. </Button>
  148. </CardFooter>
  149. </Card>
  150. </form>
  151. </Form>
  152. )}
  153. </PageSectionContent>
  154. </PageSection>
  155. )
  156. }
  157. const DashboardQueriesDialog = () => {
  158. const { ref } = useParams()
  159. return (
  160. <Dialog>
  161. <DialogTrigger className="ml-1 translate-y-0.5">
  162. <HelpCircle size={14} className="hover:text-foreground transition" />
  163. </DialogTrigger>
  164. <DialogContent aria-describedby={undefined}>
  165. <DialogHeader>
  166. <DialogTitle>How does the dashboard interact with your project's database?</DialogTitle>
  167. </DialogHeader>
  168. <DialogSectionSeparator />
  169. <DialogSection className="flex flex-col gap-y-2">
  170. <p className="text-sm">
  171. The dashboard queries your project's database to display data across various interfaces,
  172. such as the <InlineLink href={`/project/${ref}/editor`}>Table Editor</InlineLink>, the{' '}
  173. <InlineLink href={`/project/${ref}/auth/users`}>Auth Users</InlineLink> page, and more.
  174. </p>
  175. <p className="text-sm">
  176. You can route these queries to a read replica instead, which will help reduce load on
  177. your primary database.
  178. </p>
  179. </DialogSection>
  180. <DialogFooter>
  181. <DialogClose asChild className="opacity-100">
  182. <Button type="default">Understood</Button>
  183. </DialogClose>
  184. </DialogFooter>
  185. </DialogContent>
  186. </Dialog>
  187. )
  188. }