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;