diff --git a/components/sql-editor.tsx b/components/sql-editor.tsx index 7f9cbaf..e656c23 100644 --- a/components/sql-editor.tsx +++ b/components/sql-editor.tsx @@ -5,7 +5,7 @@ import { Editor } from "@monaco-editor/react"; import { Button } from "@/components/ui/button"; import { ResultsTable } from "@/components/results-table"; import { Label } from "@/components/ui/label"; -import { useRunQuery } from "@/hooks/use-supabase-manager"; +import { useRunQuery } from "@/hooks/use-run-query"; import { ArrowUp, Loader2, diff --git a/components/supabase-manager/auth.tsx b/components/supabase-manager/auth.tsx index ebfdba0..d36c12b 100644 --- a/components/supabase-manager/auth.tsx +++ b/components/supabase-manager/auth.tsx @@ -1,10 +1,7 @@ "use client"; import { DynamicForm } from "@/components/dynamic-form"; -import { - useGetAuthConfig, - useUpdateAuthConfig, -} from "@/hooks/use-supabase-manager"; +import { useGetAuthConfig, useUpdateAuthConfig } from "@/hooks/use-auth"; import { authEmailProviderSchema, authFieldLabels, @@ -12,7 +9,7 @@ import { type AuthGeneralSettingsSchema, authGoogleProviderSchema, authPhoneProviderSchema, -} from "@/lib/schemas"; +} from "@/lib/schemas/auth"; import { AlertTriangle, ChevronRight, Mail, Phone, User } from "lucide-react"; import { useCallback, useMemo } from "react"; import { z } from "zod"; diff --git a/components/supabase-manager/database.tsx b/components/supabase-manager/database.tsx index c85f856..f7215e9 100644 --- a/components/supabase-manager/database.tsx +++ b/components/supabase-manager/database.tsx @@ -2,7 +2,8 @@ import { useState, useMemo, useCallback } from "react"; import { z, type ZodTypeAny } from "zod"; -import { useListTables, useRunQuery } from "@/hooks/use-supabase-manager"; +import { useListTables } from "@/hooks/use-tables"; +import { useRunQuery } from "@/hooks/use-run-query"; import { SqlEditor } from "@/components/sql-editor"; import { DynamicForm } from "@/components/dynamic-form"; import { toast } from "sonner"; diff --git a/components/supabase-manager/index.tsx b/components/supabase-manager/index.tsx index e6ccfed..33af3f9 100644 --- a/components/supabase-manager/index.tsx +++ b/components/supabase-manager/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, ReactNode } from "react"; +import { useState, ReactNode, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -68,43 +68,46 @@ function DialogView({ const currentView = stack[stack.length - 1]; const activeManager = stack.length > 0 ? stack[0].title : null; - const navigationItems = [ - { - title: "Database", - icon: Database, - component: , - }, - { - title: "Storage", - icon: HardDrive, - component: , - }, - { - title: "Auth", - icon: Shield, - component: , - }, - { - title: "Users", - icon: Users, - component: , - }, - { - title: "Secrets", - icon: KeyRound, - component: , - }, - { - title: "Logs", - icon: ScrollText, - component: , - }, - { - title: "Suggestions", - icon: Lightbulb, - component: , - }, - ]; + const navigationItems = useMemo( + () => [ + { + title: "Database", + icon: Database, + component: , + }, + { + title: "Storage", + icon: HardDrive, + component: , + }, + { + title: "Auth", + icon: Shield, + component: , + }, + { + title: "Users", + icon: Users, + component: , + }, + { + title: "Secrets", + icon: KeyRound, + component: , + }, + { + title: "Logs", + icon: ScrollText, + component: , + }, + { + title: "Suggestions", + icon: Lightbulb, + component: , + }, + ], + [projectRef] + ); if (isMobile) { return ( diff --git a/components/supabase-manager/logs.tsx b/components/supabase-manager/logs.tsx index 71f30fd..4c011bf 100644 --- a/components/supabase-manager/logs.tsx +++ b/components/supabase-manager/logs.tsx @@ -29,7 +29,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { useGetLogs } from "@/hooks/use-supabase-manager"; +import { useGetLogs } from "@/hooks/use-logs"; import { LogsTableName, genDefaultQuery } from "@/lib/logs"; import { cn } from "@/lib/utils"; import { Check, ChevronsUpDown, Logs, Terminal } from "lucide-react"; diff --git a/components/supabase-manager/secrets.tsx b/components/supabase-manager/secrets.tsx index e62de97..a9f1737 100644 --- a/components/supabase-manager/secrets.tsx +++ b/components/supabase-manager/secrets.tsx @@ -15,8 +15,8 @@ import { useCreateSecrets, useDeleteSecrets, useGetSecrets, -} from "@/hooks/use-supabase-manager"; -import { secretsSchema } from "@/lib/schemas"; +} from "@/hooks/use-secrets"; +import { secretsSchema } from "@/lib/schemas/secrets"; import { zodResolver } from "@hookform/resolvers/zod"; import { AlertTriangle, Minus, PlusIcon, Key } from "lucide-react"; import { useFieldArray, useForm } from "react-hook-form"; diff --git a/components/supabase-manager/storage.tsx b/components/supabase-manager/storage.tsx index 4463905..acf440d 100644 --- a/components/supabase-manager/storage.tsx +++ b/components/supabase-manager/storage.tsx @@ -1,8 +1,6 @@ "use client"; -import { useCallback } from "react"; -import { useSheetNavigation } from "../../contexts/SheetNavigationContext"; -import { useGetBuckets, useListObjects } from "@/hooks/use-supabase-manager"; +import { useGetBuckets } from "@/hooks/use-storage"; import { Skeleton } from "@/components/ui/skeleton"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; diff --git a/components/supabase-manager/suggestions.tsx b/components/supabase-manager/suggestions.tsx index 58ddb2f..eb6bb1d 100644 --- a/components/supabase-manager/suggestions.tsx +++ b/components/supabase-manager/suggestions.tsx @@ -1,16 +1,11 @@ "use client"; -import { useGetSuggestions } from "@/hooks/use-supabase-manager"; +import { useGetSuggestions } from "@/hooks/use-suggestions"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Terminal } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { useMemo } from "react"; -import { Button } from "@/components/ui/button"; -import { - Tooltip, - TooltipTrigger, - TooltipContent, -} from "@/components/ui/tooltip"; + import ReactMarkdown from "react-markdown"; import { Skeleton } from "@/components/ui/skeleton"; diff --git a/components/supabase-manager/users-growth.tsx b/components/supabase-manager/users-growth.tsx index d6b5468..87c42be 100644 --- a/components/supabase-manager/users-growth.tsx +++ b/components/supabase-manager/users-growth.tsx @@ -8,7 +8,7 @@ import { ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; -import { useGetUserCountsByDay } from "@/hooks/use-supabase-manager"; +import { useGetUserCountsByDay } from "@/hooks/use-user-counts"; import { Skeleton } from "@/components/ui/skeleton"; import { AlertTriangle } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; diff --git a/hooks/use-auth.ts b/hooks/use-auth.ts new file mode 100644 index 0000000..a5bb1af --- /dev/null +++ b/hooks/use-auth.ts @@ -0,0 +1,71 @@ +"use client"; + +import { client } from "@/lib/management-api"; +import type { components } from "@/lib/management-api-schema"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { type AxiosError } from "axios"; +import { toast } from "sonner"; + +const getAuthConfig = async (projectRef: string) => { + const { data, error } = await client.GET("/v1/projects/{ref}/config/auth", { + params: { + path: { ref: projectRef }, + }, + }); + if (error) { + throw error; + } + + return data; +}; + +export const useGetAuthConfig = (projectRef: string) => { + return useQuery({ + queryKey: ["auth-config", projectRef], + queryFn: () => getAuthConfig(projectRef), + enabled: !!projectRef, + retry: false, + }); +}; + +// UPDATE Auth Config +const updateAuthConfig = async ({ + projectRef, + payload, +}: { + projectRef: string; + payload: components["schemas"]["UpdateAuthConfigBody"]; +}) => { + const { data, error } = await client.PATCH("/v1/projects/{ref}/config/auth", { + params: { + path: { + ref: projectRef, + }, + }, + body: payload, + }); + if (error) { + throw error; + } + + return data; +}; + +export const useUpdateAuthConfig = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: updateAuthConfig, + onSuccess: (data, variables) => { + toast.success(`Auth config updated.`); + queryClient.invalidateQueries({ + queryKey: ["auth-config", variables.projectRef], + }); + }, + onError: (error: AxiosError<{ message: string }>) => { + toast.error( + error.response?.data?.message || + "There was a problem with your request." + ); + }, + }); +}; diff --git a/hooks/use-logs.ts b/hooks/use-logs.ts new file mode 100644 index 0000000..4dd995a --- /dev/null +++ b/hooks/use-logs.ts @@ -0,0 +1,67 @@ +"use client"; + +import { client } from "@/lib/management-api"; +import { useQuery } from "@tanstack/react-query"; + +// GET Logs +const getLogs = async ({ + projectRef, + iso_timestamp_start, + iso_timestamp_end, + sql, +}: { + projectRef: string; + iso_timestamp_start?: string; + iso_timestamp_end?: string; + sql?: string; +}) => { + const { data, error } = await client.GET( + "/v1/projects/{ref}/analytics/endpoints/logs.all", + { + params: { + path: { + ref: projectRef, + }, + query: { + iso_timestamp_start, + iso_timestamp_end, + sql, + }, + }, + } + ); + if (error) { + throw error; + } + + return data; +}; + +export const useGetLogs = ( + projectRef: string, + params: { + iso_timestamp_start?: string; + iso_timestamp_end?: string; + sql?: string; + } = {} +) => { + const queryKey = ["logs", projectRef, params.sql]; + + return useQuery({ + queryKey: queryKey, + queryFn: () => { + const now = new Date(); + const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); + + const queryParams = { + sql: params.sql, + iso_timestamp_start: + params.iso_timestamp_start ?? oneHourAgo.toISOString(), + iso_timestamp_end: params.iso_timestamp_end ?? now.toISOString(), + }; + return getLogs({ projectRef, ...queryParams }); + }, + enabled: !!projectRef, + retry: false, + }); +}; diff --git a/hooks/use-run-query.ts b/hooks/use-run-query.ts new file mode 100644 index 0000000..ff7ca37 --- /dev/null +++ b/hooks/use-run-query.ts @@ -0,0 +1,51 @@ +"use client"; + +import { client } from "@/lib/management-api"; +import type { components } from "@/lib/management-api-schema"; +import { listTablesSql } from "@/lib/pg-meta"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { type AxiosError } from "axios"; +import { toast } from "sonner"; + +// RUN SQL Query +export const runQuery = async ({ + projectRef, + query, + readOnly, +}: { + projectRef: string; + query: string; + readOnly?: boolean; +}) => { + const { data, error } = await client.POST( + "/v1/projects/{ref}/database/query", + { + params: { + path: { + ref: projectRef, + }, + }, + body: { + query, + read_only: readOnly, + }, + } + ); + + if (error) { + throw error; + } + + return data as any; +}; + +export const useRunQuery = () => { + return useMutation({ + mutationFn: runQuery, + onError: (error: AxiosError<{ message: string }>) => { + toast.error( + error.response?.data?.message || "There was a problem with your query." + ); + }, + }); +}; diff --git a/hooks/use-secrets.ts b/hooks/use-secrets.ts new file mode 100644 index 0000000..c46e9f2 --- /dev/null +++ b/hooks/use-secrets.ts @@ -0,0 +1,116 @@ +"use client"; + +import { client } from "@/lib/management-api"; +import type { components } from "@/lib/management-api-schema"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { type AxiosError } from "axios"; +import { toast } from "sonner"; + +// GET Secrets +const getSecrets = async (projectRef: string) => { + const { data, error } = await client.GET("/v1/projects/{ref}/secrets", { + params: { + path: { + ref: projectRef, + }, + }, + }); + if (error) { + throw error; + } + + return data; +}; + +export const useGetSecrets = (projectRef: string) => { + return useQuery({ + queryKey: ["secrets", projectRef], + queryFn: () => getSecrets(projectRef), + enabled: !!projectRef, + retry: false, + }); +}; + +// CREATE Secrets +const createSecrets = async ({ + projectRef, + secrets, +}: { + projectRef: string; + secrets: components["schemas"]["CreateSecretBody"]; +}) => { + const { data, error } = await client.POST("/v1/projects/{ref}/secrets", { + params: { + path: { + ref: projectRef, + }, + }, + body: secrets, + }); + if (error) { + throw error; + } + + return data; +}; + +export const useCreateSecrets = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createSecrets, + onSuccess: (data, variables) => { + toast.success(`Secrets created successfully.`); + queryClient.refetchQueries({ + queryKey: ["secrets", variables.projectRef], + }); + }, + onError: (error: AxiosError<{ message: string }>) => { + toast.error( + error.response?.data?.message || + "There was a problem with your request." + ); + }, + }); +}; + +// DELETE Secrets +const deleteSecrets = async ({ + projectRef, + secretNames, +}: { + projectRef: string; + secretNames: string[]; +}) => { + const { data, error } = await client.DELETE("/v1/projects/{ref}/secrets", { + params: { + path: { + ref: projectRef, + }, + }, + body: secretNames, + }); + if (error) { + throw error; + } + + return data; +}; + +export const useDeleteSecrets = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: deleteSecrets, + onSuccess: (data, variables) => { + toast.success(`Secrets deleted successfully.`); + queryClient.invalidateQueries({ + queryKey: ["secrets", variables.projectRef], + }); + }, + onError: (error: AxiosError<{ message: string }>) => { + toast.error( + error.response?.data?.message || + "There was a problem with your request." + ); + }, + }); +}; diff --git a/hooks/use-storage.ts b/hooks/use-storage.ts new file mode 100644 index 0000000..095ae29 --- /dev/null +++ b/hooks/use-storage.ts @@ -0,0 +1,72 @@ +"use client"; + +import { client } from "@/lib/management-api"; +import { useQuery } from "@tanstack/react-query"; + +// GET Buckets +const getBuckets = async (projectRef: string) => { + const { data, error } = await client.GET( + "/v1/projects/{ref}/storage/buckets", + { + params: { + path: { + ref: projectRef, + }, + }, + } + ); + if (error) { + throw error; + } + + return data; +}; + +export const useGetBuckets = (projectRef: string) => { + return useQuery({ + queryKey: ["buckets", projectRef], + queryFn: () => getBuckets(projectRef), + enabled: !!projectRef, + retry: false, + }); +}; + +// LIST Objects +const listObjects = async ({ + projectRef, + bucketId, +}: { + projectRef: string; + bucketId: string; +}) => { + const { data, error } = await client.POST( + // TODO + // @ts-expect-error this endpoint is not yet implemented + "/v1/projects/{ref}/storage/buckets/{bucketId}/objects/list", + { + params: { + path: { + ref: projectRef, + bucketId, + }, + }, + body: { + path: "", + options: { limit: 100, offset: 0 }, + }, + } + ); + if (error) { + throw error; + } + + return data as any; +}; + +export const useListObjects = (projectRef: string, bucketId: string) => { + return useQuery({ + queryKey: ["objects", projectRef, bucketId], + queryFn: () => listObjects({ projectRef, bucketId }), + enabled: !!projectRef && !!bucketId, + }); +}; diff --git a/hooks/use-suggestions.ts b/hooks/use-suggestions.ts new file mode 100644 index 0000000..5c4ace1 --- /dev/null +++ b/hooks/use-suggestions.ts @@ -0,0 +1,53 @@ +"use client"; + +import { client } from "@/lib/management-api"; +import { useQuery } from "@tanstack/react-query"; + +// GET Suggestions +const getSuggestions = async (projectRef: string) => { + const [ + { data: performanceData, error: performanceError }, + { data: securityData, error: securityError }, + ] = await Promise.all([ + client.GET("/v1/projects/{ref}/advisors/performance", { + params: { + path: { + ref: projectRef, + }, + }, + }), + client.GET("/v1/projects/{ref}/advisors/security", { + params: { + path: { + ref: projectRef, + }, + }, + }), + ]); + if (performanceError) { + throw performanceError; + } + if (securityError) { + throw securityError; + } + + // Add type to each suggestion + const performanceLints = (performanceData?.lints || []).map((lint) => ({ + ...lint, + type: "performance" as const, + })); + const securityLints = (securityData?.lints || []).map((lint) => ({ + ...lint, + type: "security" as const, + })); + return [...performanceLints, ...securityLints]; +}; + +export const useGetSuggestions = (projectRef: string) => { + return useQuery({ + queryKey: ["suggestions", projectRef], + queryFn: () => getSuggestions(projectRef), + enabled: !!projectRef, + retry: false, + }); +}; diff --git a/hooks/use-supabase-manager.ts b/hooks/use-supabase-manager.ts deleted file mode 100644 index 684b508..0000000 --- a/hooks/use-supabase-manager.ts +++ /dev/null @@ -1,477 +0,0 @@ -"use client"; - -import { client } from "@/lib/management-api"; -import type { components } from "@/lib/management-api-schema"; -import { listTablesSql } from "@/lib/pg-meta"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { type AxiosError } from "axios"; -import { toast } from "sonner"; - -const getAuthConfig = async (projectRef: string) => { - const { data, error } = await client.GET("/v1/projects/{ref}/config/auth", { - params: { - path: { ref: projectRef }, - }, - }); - if (error) { - throw error; - } - - return data; -}; - -export const useGetAuthConfig = (projectRef: string) => { - return useQuery({ - queryKey: ["auth-config", projectRef], - queryFn: () => getAuthConfig(projectRef), - enabled: !!projectRef, - retry: false, - }); -}; - -// UPDATE Auth Config -const updateAuthConfig = async ({ - projectRef, - payload, -}: { - projectRef: string; - payload: components["schemas"]["UpdateAuthConfigBody"]; -}) => { - const { data, error } = await client.PATCH("/v1/projects/{ref}/config/auth", { - params: { - path: { - ref: projectRef, - }, - }, - body: payload, - }); - if (error) { - throw error; - } - - return data; -}; - -export const useUpdateAuthConfig = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: updateAuthConfig, - onSuccess: (data, variables) => { - toast.success(`Auth config updated.`); - queryClient.invalidateQueries({ - queryKey: ["auth-config", variables.projectRef], - }); - }, - onError: (error: AxiosError<{ message: string }>) => { - toast.error( - error.response?.data?.message || - "There was a problem with your request." - ); - }, - }); -}; - -// RUN SQL Query -const runQuery = async ({ - projectRef, - query, - readOnly, -}: { - projectRef: string; - query: string; - readOnly?: boolean; -}) => { - const { data, error } = await client.POST( - "/v1/projects/{ref}/database/query", - { - params: { - path: { - ref: projectRef, - }, - }, - body: { - query, - read_only: readOnly, - }, - } - ); - - if (error) { - throw error; - } - - return data as any; -}; - -export const useRunQuery = () => { - return useMutation({ - mutationFn: runQuery, - onError: (error: AxiosError<{ message: string }>) => { - toast.error( - error.response?.data?.message || "There was a problem with your query." - ); - }, - }); -}; - -// LIST Tables -const listTables = ({ - projectRef, - schemas, -}: { - projectRef: string; - schemas?: string[]; -}) => { - const sql = listTablesSql(schemas); - return runQuery({ - projectRef, - query: sql, - readOnly: true, - }); -}; - -export const useListTables = (projectRef: string, schemas?: string[]) => { - return useQuery({ - queryKey: ["tables", projectRef, schemas], - queryFn: () => listTables({ projectRef, schemas }), - enabled: !!projectRef, - }); -}; - -// GET Buckets -const getBuckets = async (projectRef: string) => { - const { data, error } = await client.GET( - "/v1/projects/{ref}/storage/buckets", - { - params: { - path: { - ref: projectRef, - }, - }, - } - ); - if (error) { - throw error; - } - - return data; -}; - -export const useGetBuckets = (projectRef: string) => { - return useQuery({ - queryKey: ["buckets", projectRef], - queryFn: () => getBuckets(projectRef), - enabled: !!projectRef, - retry: false, - }); -}; - -// LIST Objects -const listObjects = async ({ - projectRef, - bucketId, -}: { - projectRef: string; - bucketId: string; -}) => { - const { data, error } = await client.POST( - // TODO - // @ts-expect-error this endpoint is not yet implemented - "/v1/projects/{ref}/storage/buckets/{bucketId}/objects/list", - { - params: { - path: { - ref: projectRef, - bucketId, - }, - }, - body: { - path: "", - options: { limit: 100, offset: 0 }, - }, - } - ); - if (error) { - throw error; - } - - return data as any; -}; - -export const useListObjects = (projectRef: string, bucketId: string) => { - return useQuery({ - queryKey: ["objects", projectRef, bucketId], - queryFn: () => listObjects({ projectRef, bucketId }), - enabled: !!projectRef && !!bucketId, - }); -}; - -// GET Logs -const getLogs = async ({ - projectRef, - iso_timestamp_start, - iso_timestamp_end, - sql, -}: { - projectRef: string; - iso_timestamp_start?: string; - iso_timestamp_end?: string; - sql?: string; -}) => { - const { data, error } = await client.GET( - "/v1/projects/{ref}/analytics/endpoints/logs.all", - { - params: { - path: { - ref: projectRef, - }, - query: { - iso_timestamp_start, - iso_timestamp_end, - sql, - }, - }, - } - ); - if (error) { - throw error; - } - - return data; -}; - -export const useGetLogs = ( - projectRef: string, - params: { - iso_timestamp_start?: string; - iso_timestamp_end?: string; - sql?: string; - } = {} -) => { - const queryKey = ["logs", projectRef, params.sql]; - - return useQuery({ - queryKey: queryKey, - queryFn: () => { - const now = new Date(); - const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); - - const queryParams = { - sql: params.sql, - iso_timestamp_start: - params.iso_timestamp_start ?? oneHourAgo.toISOString(), - iso_timestamp_end: params.iso_timestamp_end ?? now.toISOString(), - }; - return getLogs({ projectRef, ...queryParams }); - }, - enabled: !!projectRef, - retry: false, - }); -}; - -// GET User Counts by day -const getUserCountsByDay = ({ - projectRef, - days, -}: { - projectRef: string; - days: number; -}) => { - const sql = /* SQL */ ` - WITH days_series AS ( - SELECT generate_series( - date_trunc('day', now() - interval '${Number(days) - 1} days'), - date_trunc('day', now()), - '1 day'::interval - )::date AS date - ) - SELECT - d.date, - COALESCE(u.users, 0)::int as users - FROM - days_series d - LEFT JOIN ( - SELECT - date_trunc('day', created_at AT TIME ZONE 'UTC')::date as date, - count(id) as users - FROM - auth.users - GROUP BY 1 - ) u ON d.date = u.date - ORDER BY - d.date ASC; - `; - - return runQuery({ - projectRef, - query: sql, - readOnly: true, - }); -}; - -export const useGetUserCountsByDay = (projectRef: string, days: number) => { - return useQuery({ - queryKey: ["user-counts", projectRef, days], - queryFn: () => getUserCountsByDay({ projectRef, days }), - enabled: !!projectRef, - retry: false, - }); -}; - -// GET Suggestions -const getSuggestions = async (projectRef: string) => { - const [ - { data: performanceData, error: performanceError }, - { data: securityData, error: securityError }, - ] = await Promise.all([ - client.GET("/v1/projects/{ref}/advisors/performance", { - params: { - path: { - ref: projectRef, - }, - }, - }), - client.GET("/v1/projects/{ref}/advisors/security", { - params: { - path: { - ref: projectRef, - }, - }, - }), - ]); - if (performanceError) { - throw performanceError; - } - if (securityError) { - throw securityError; - } - - // Add type to each suggestion - const performanceLints = (performanceData?.lints || []).map((lint) => ({ - ...lint, - type: "performance" as const, - })); - const securityLints = (securityData?.lints || []).map((lint) => ({ - ...lint, - type: "security" as const, - })); - return [...performanceLints, ...securityLints]; -}; - -export const useGetSuggestions = (projectRef: string) => { - return useQuery({ - queryKey: ["suggestions", projectRef], - queryFn: () => getSuggestions(projectRef), - enabled: !!projectRef, - retry: false, - }); -}; - -// GET Secrets -const getSecrets = async (projectRef: string) => { - const { data, error } = await client.GET("/v1/projects/{ref}/secrets", { - params: { - path: { - ref: projectRef, - }, - }, - }); - if (error) { - throw error; - } - - return data; -}; - -export const useGetSecrets = (projectRef: string) => { - return useQuery({ - queryKey: ["secrets", projectRef], - queryFn: () => getSecrets(projectRef), - enabled: !!projectRef, - retry: false, - }); -}; - -// CREATE Secrets -const createSecrets = async ({ - projectRef, - secrets, -}: { - projectRef: string; - secrets: components["schemas"]["CreateSecretBody"]; -}) => { - const { data, error } = await client.POST("/v1/projects/{ref}/secrets", { - params: { - path: { - ref: projectRef, - }, - }, - body: secrets, - }); - if (error) { - throw error; - } - - return data; -}; - -export const useCreateSecrets = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: createSecrets, - onSuccess: (data, variables) => { - toast.success(`Secrets created successfully.`); - queryClient.refetchQueries({ - queryKey: ["secrets", variables.projectRef], - }); - }, - onError: (error: AxiosError<{ message: string }>) => { - toast.error( - error.response?.data?.message || - "There was a problem with your request." - ); - }, - }); -}; - -// DELETE Secrets -const deleteSecrets = async ({ - projectRef, - secretNames, -}: { - projectRef: string; - secretNames: string[]; -}) => { - const { data, error } = await client.DELETE("/v1/projects/{ref}/secrets", { - params: { - path: { - ref: projectRef, - }, - }, - body: secretNames, - }); - if (error) { - throw error; - } - - return data; -}; - -export const useDeleteSecrets = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: deleteSecrets, - onSuccess: (data, variables) => { - toast.success(`Secrets deleted successfully.`); - queryClient.invalidateQueries({ - queryKey: ["secrets", variables.projectRef], - }); - }, - onError: (error: AxiosError<{ message: string }>) => { - toast.error( - error.response?.data?.message || - "There was a problem with your request." - ); - }, - }); -}; diff --git a/hooks/use-tables.ts b/hooks/use-tables.ts new file mode 100644 index 0000000..abd233f --- /dev/null +++ b/hooks/use-tables.ts @@ -0,0 +1,29 @@ +"use client"; + +import { listTablesSql } from "@/lib/pg-meta"; +import { runQuery } from "./use-run-query"; +import { useQuery } from "@tanstack/react-query"; + +// LIST Tables +const listTables = ({ + projectRef, + schemas, +}: { + projectRef: string; + schemas?: string[]; +}) => { + const sql = listTablesSql(schemas); + return runQuery({ + projectRef, + query: sql, + readOnly: true, + }); +}; + +export const useListTables = (projectRef: string, schemas?: string[]) => { + return useQuery({ + queryKey: ["tables", projectRef, schemas], + queryFn: () => listTables({ projectRef, schemas }), + enabled: !!projectRef, + }); +}; diff --git a/hooks/use-user-counts.ts b/hooks/use-user-counts.ts new file mode 100644 index 0000000..61a308e --- /dev/null +++ b/hooks/use-user-counts.ts @@ -0,0 +1,53 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { runQuery } from "./use-run-query"; + +// GET User Counts by day +const getUserCountsByDay = ({ + projectRef, + days, +}: { + projectRef: string; + days: number; +}) => { + const sql = ` + WITH days_series AS ( + SELECT generate_series( + date_trunc('day', now() - interval '${Number(days) - 1} days'), + date_trunc('day', now()), + '1 day'::interval + )::date AS date + ) + SELECT + d.date, + COALESCE(u.users, 0)::int as users + FROM + days_series d + LEFT JOIN ( + SELECT + date_trunc('day', created_at AT TIME ZONE 'UTC')::date as date, + count(id) as users + FROM + auth.users + GROUP BY 1 + ) u ON d.date = u.date + ORDER BY + d.date ASC; + `; + + return runQuery({ + projectRef, + query: sql, + readOnly: true, + }); +}; + +export const useGetUserCountsByDay = (projectRef: string, days: number) => { + return useQuery({ + queryKey: ["user-counts", projectRef, days], + queryFn: () => getUserCountsByDay({ projectRef, days }), + enabled: !!projectRef, + retry: false, + }); +}; diff --git a/lib/schemas.ts b/lib/schemas/auth.ts similarity index 92% rename from lib/schemas.ts rename to lib/schemas/auth.ts index 291d39c..0a42de9 100644 --- a/lib/schemas.ts +++ b/lib/schemas/auth.ts @@ -300,31 +300,3 @@ export type AuthConfigUpdateSchema = z.infer; // A version used for partial updates (all fields optional) export const authConfigUpdatePayloadSchema = authConfigUpdateSchema.partial(); - -export const secretsSchema = z - .object({ - secrets: z.array( - z.object({ - name: z - .string() - .min(1, "Secret name is required.") - .regex( - /^[a-zA-Z_][a-zA-Z0-9_]*$/, - "Must contain letters, numbers, and underscores, starting with a letter or underscore." - ), - value: z.string().min(1, "Secret value is required."), - }) - ), - }) - .describe("Secrets schema for managing environment variables."); - -export type SecretsSchema = z.infer; - -export const deleteSecretsSchema = z - .object({ - secretNames: z - .array(z.string().min(1, "Secret name cannot be empty.")) - .min(1, "At least one secret name is required."), - }) - .describe("Schema for deleting secrets by name."); -export type DeleteSecretsSchema = z.infer; diff --git a/lib/schemas/secrets.ts b/lib/schemas/secrets.ts new file mode 100644 index 0000000..9dd8a25 --- /dev/null +++ b/lib/schemas/secrets.ts @@ -0,0 +1,29 @@ +import z from "zod"; + +export const secretsSchema = z + .object({ + secrets: z.array( + z.object({ + name: z + .string() + .min(1, "Secret name is required.") + .regex( + /^[a-zA-Z_][a-zA-Z0-9_]*$/, + "Must contain letters, numbers, and underscores, starting with a letter or underscore." + ), + value: z.string().min(1, "Secret value is required."), + }) + ), + }) + .describe("Secrets schema for managing environment variables."); + +export type SecretsSchema = z.infer; + +export const deleteSecretsSchema = z + .object({ + secretNames: z + .array(z.string().min(1, "Secret name cannot be empty.")) + .min(1, "At least one secret name is required."), + }) + .describe("Schema for deleting secrets by name."); +export type DeleteSecretsSchema = z.infer;