refactor(frontend): update settings pages fetching using react query (#10248)

### Changes 🏗️
- We have implemented some backend changes, so I have added a new,
updated OpenAPI specification.
- We have updated the settings and API keys page to enable us to use
React Query for fetching data.

### Checklist 📋

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
    - [x] Settings and api keys page is working correctly
This commit is contained in:
Abhimanyu Yadav
2025-06-26 20:43:20 +05:30
committed by GitHub
parent 2af9d75dec
commit f66b8f9c74
13 changed files with 743 additions and 452 deletions

View File

@@ -75,7 +75,7 @@ export const customMutator = async <T = any>(
return {
status: response.status,
response_data,
data: response_data,
headers: response.headers,
} as T;
};

View File

@@ -2931,18 +2931,6 @@
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Preset Id" }
},
{
"name": "graph_id",
"in": "query",
"required": true,
"schema": { "type": "string", "title": "Graph Id" }
},
{
"name": "graph_version",
"in": "query",
"required": true,
"schema": { "type": "integer", "title": "Graph Version" }
}
],
"requestBody": {
@@ -3128,11 +3116,11 @@
}
}
},
"put": {
"patch": {
"tags": ["v2", "library", "private"],
"summary": "Update Library Agent",
"description": "Update the library agent with the given fields.\n\nArgs:\n library_agent_id: ID of the library agent to update.\n payload: Fields to update (auto_update_version, is_favorite, etc.).\n user_id: ID of the authenticated user.\n\nReturns:\n 204 (No Content) on success.\n\nRaises:\n HTTPException(500): If a server/database error occurs.",
"operationId": "putV2Update library agent",
"description": "Update the library agent with the given fields.\n\nArgs:\n library_agent_id: ID of the library agent to update.\n payload: Fields to update (auto_update_version, is_favorite, etc.).\n user_id: ID of the authenticated user.\n\nRaises:\n HTTPException(500): If a server/database error occurs.",
"operationId": "patchV2Update library agent",
"parameters": [
{
"name": "library_agent_id",
@@ -3152,7 +3140,45 @@
}
},
"responses": {
"204": { "description": "Agent updated successfully" },
"200": {
"description": "Agent updated successfully",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/LibraryAgent" }
}
}
},
"500": { "description": "Server error" },
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
},
"delete": {
"tags": ["v2", "library", "private"],
"summary": "Delete Library Agent",
"description": "Soft-delete the specified library agent.\n\nArgs:\n library_agent_id: ID of the library agent to delete.\n user_id: ID of the authenticated user.\n\nReturns:\n 204 No Content if successful.\n\nRaises:\n HTTPException(404): If the agent does not exist.\n HTTPException(500): If a server/database error occurs.",
"operationId": "deleteV2Delete library agent",
"parameters": [
{
"name": "library_agent_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Library Agent Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"204": { "description": "Agent deleted successfully" },
"404": { "description": "Agent not found" },
"500": { "description": "Server error" },
"422": {
"description": "Validation Error",
@@ -3238,6 +3264,55 @@
}
}
},
"/api/library/agents/{library_agent_id}/setup-trigger": {
"post": {
"tags": ["v2", "library", "private"],
"summary": "Setup Trigger",
"description": "Sets up a webhook-triggered `LibraryAgentPreset` for a `LibraryAgent`.\nReturns the correspondingly created `LibraryAgentPreset` with `webhook_id` set.",
"operationId": "postV2SetupTrigger",
"parameters": [
{
"name": "library_agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"description": "ID of the library agent",
"title": "Library Agent Id"
},
"description": "ID of the library agent"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TriggeredPresetSetupParams"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/LibraryAgentPreset" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/otto/ask": {
"post": {
"tags": ["v2", "otto"],
@@ -3713,10 +3788,10 @@
},
"Body_postV2Execute_a_preset": {
"properties": {
"node_input": {
"inputs": {
"additionalProperties": true,
"type": "object",
"title": "Node Input"
"title": "Inputs"
}
},
"type": "object",
@@ -4303,6 +4378,23 @@
"type": "object",
"title": "Input Schema"
},
"credentials_input_schema": {
"additionalProperties": true,
"type": "object",
"title": "Credentials Input Schema",
"description": "Input schema for credentials required by the agent"
},
"has_external_trigger": {
"type": "boolean",
"title": "Has External Trigger",
"description": "Whether the agent has an external trigger (e.g. webhook) node"
},
"trigger_setup_info": {
"anyOf": [
{ "$ref": "#/components/schemas/LibraryAgentTriggerInfo" },
{ "type": "null" }
]
},
"new_output": { "type": "boolean", "title": "New Output" },
"can_access_graph": {
"type": "boolean",
@@ -4326,6 +4418,8 @@
"name",
"description",
"input_schema",
"credentials_input_schema",
"has_external_trigger",
"new_output",
"can_access_graph",
"is_latest_version"
@@ -4342,6 +4436,13 @@
"type": "object",
"title": "Inputs"
},
"credentials": {
"additionalProperties": {
"$ref": "#/components/schemas/CredentialsMetaInput"
},
"type": "object",
"title": "Credentials"
},
"name": { "type": "string", "title": "Name" },
"description": { "type": "string", "title": "Description" },
"is_active": {
@@ -4349,7 +4450,12 @@
"title": "Is Active",
"default": true
},
"webhook_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Webhook Id"
},
"id": { "type": "string", "title": "Id" },
"user_id": { "type": "string", "title": "User Id" },
"updated_at": {
"type": "string",
"format": "date-time",
@@ -4361,9 +4467,11 @@
"graph_id",
"graph_version",
"inputs",
"credentials",
"name",
"description",
"id",
"user_id",
"updated_at"
],
"title": "LibraryAgentPreset",
@@ -4378,12 +4486,23 @@
"type": "object",
"title": "Inputs"
},
"credentials": {
"additionalProperties": {
"$ref": "#/components/schemas/CredentialsMetaInput"
},
"type": "object",
"title": "Credentials"
},
"name": { "type": "string", "title": "Name" },
"description": { "type": "string", "title": "Description" },
"is_active": {
"type": "boolean",
"title": "Is Active",
"default": true
},
"webhook_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Webhook Id"
}
},
"type": "object",
@@ -4391,6 +4510,7 @@
"graph_id",
"graph_version",
"inputs",
"credentials",
"name",
"description"
],
@@ -4439,6 +4559,18 @@
],
"title": "Inputs"
},
"credentials": {
"anyOf": [
{
"additionalProperties": {
"$ref": "#/components/schemas/CredentialsMetaInput"
},
"type": "object"
},
{ "type": "null" }
],
"title": "Credentials"
},
"name": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Name"
@@ -4481,6 +4613,24 @@
"enum": ["COMPLETED", "HEALTHY", "WAITING", "ERROR"],
"title": "LibraryAgentStatus"
},
"LibraryAgentTriggerInfo": {
"properties": {
"provider": { "$ref": "#/components/schemas/ProviderName" },
"config_schema": {
"additionalProperties": true,
"type": "object",
"title": "Config Schema",
"description": "Input schema for the trigger block"
},
"credentials_input_name": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Credentials Input Name"
}
},
"type": "object",
"required": ["provider", "config_schema", "credentials_input_name"],
"title": "LibraryAgentTriggerInfo"
},
"LibraryAgentUpdateRequest": {
"properties": {
"auto_update_version": {
@@ -4497,11 +4647,6 @@
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
"title": "Is Archived",
"description": "Archive the agent"
},
"is_deleted": {
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
"title": "Is Deleted",
"description": "Delete the agent"
}
},
"type": "object",
@@ -5773,6 +5918,31 @@
"required": ["transactions", "next_transaction_time"],
"title": "TransactionHistory"
},
"TriggeredPresetSetupParams": {
"properties": {
"name": { "type": "string", "title": "Name" },
"description": {
"type": "string",
"title": "Description",
"default": ""
},
"trigger_config": {
"additionalProperties": true,
"type": "object",
"title": "Trigger Config"
},
"agent_credentials": {
"additionalProperties": {
"$ref": "#/components/schemas/CredentialsMetaInput"
},
"type": "object",
"title": "Agent Credentials"
}
},
"type": "object",
"required": ["name", "trigger_config"],
"title": "TriggeredPresetSetupParams"
},
"TurnstileVerifyRequest": {
"properties": {
"token": {
@@ -6055,16 +6225,6 @@
"type": "string",
"title": "Provider Webhook Id"
},
"attached_nodes": {
"anyOf": [
{
"items": { "$ref": "#/components/schemas/NodeModel" },
"type": "array"
},
{ "type": "null" }
],
"title": "Attached Nodes"
},
"url": { "type": "string", "title": "Url", "readOnly": true }
},
"type": "object",

View File

@@ -0,0 +1,102 @@
"use client";
import { Loader2, MoreVertical } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useAPISection } from "./useAPISection";
export function APIKeysSection() {
const { apiKeys, isLoading, isDeleting, handleRevokeKey } = useAPISection();
return (
<>
{isLoading ? (
<div className="flex justify-center p-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
apiKeys &&
apiKeys.length > 0 && (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>API Key</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead>Last Used</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiKeys.map((key) => (
<TableRow key={key.id}>
<TableCell>{key.name}</TableCell>
<TableCell>
<div className="rounded-md border p-1 px-2 text-xs">
{`${key.prefix}******************${key.postfix}`}
</div>
</TableCell>
<TableCell>
<Badge
variant={
key.status === "ACTIVE" ? "default" : "destructive"
}
className={
key.status === "ACTIVE"
? "border-green-600 bg-green-100 text-green-800"
: "border-red-600 bg-red-100 text-red-800"
}
>
{key.status}
</Badge>
</TableCell>
<TableCell>
{new Date(key.created_at).toLocaleDateString()}
</TableCell>
<TableCell>
{key.last_used_at
? new Date(key.last_used_at).toLocaleDateString()
: "Never"}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-destructive"
onClick={() => handleRevokeKey(key.id)}
disabled={isDeleting}
>
Revoke
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
)}
</>
);
}

View File

@@ -0,0 +1,61 @@
"use client";
import {
getGetV1ListUserApiKeysQueryKey,
useDeleteV1RevokeApiKey,
useGetV1ListUserApiKeys,
} from "@/api/__generated__/endpoints/api-keys/api-keys";
import { APIKeyWithoutHash } from "@/api/__generated__/models/aPIKeyWithoutHash";
import { useToast } from "@/components/ui/use-toast";
import { getQueryClient } from "@/lib/react-query/queryClient";
export const useAPISection = () => {
const queryClient = getQueryClient();
const { toast } = useToast();
const { data: apiKeys, isLoading } = useGetV1ListUserApiKeys({
query: {
select: (res) => {
return (res.data as APIKeyWithoutHash[]).filter(
(key) => key.status === "ACTIVE",
);
},
},
});
const { mutateAsync: revokeAPIKey, isPending: isDeleting } =
useDeleteV1RevokeApiKey({
mutation: {
onSettled: () => {
return queryClient.invalidateQueries({
queryKey: getGetV1ListUserApiKeysQueryKey(),
});
},
},
});
const handleRevokeKey = async (keyId: string) => {
try {
await revokeAPIKey({
keyId: keyId,
});
toast({
title: "Success",
description: "AutoGPT Platform API key revoked successfully",
});
} catch {
toast({
title: "Error",
description: "Failed to revoke AutoGPT Platform API key",
variant: "destructive",
});
}
};
return {
apiKeys,
isLoading,
isDeleting,
handleRevokeKey,
};
};

View File

@@ -0,0 +1,133 @@
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { LuCopy } from "react-icons/lu";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { useAPIkeysModals } from "./useAPIkeysModals";
import { APIKeyPermission } from "@/api/__generated__/models/aPIKeyPermission";
export const APIKeysModals = () => {
const {
isCreating,
handleCreateKey,
handleCopyKey,
setIsCreateOpen,
setIsKeyDialogOpen,
isCreateOpen,
isKeyDialogOpen,
keyState,
setKeyState,
} = useAPIkeysModals();
return (
<div className="mb-4 flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button>Create Key</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New API Key</DialogTitle>
<DialogDescription>
Create a new AutoGPT Platform API key
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={keyState.newKeyName}
onChange={(e) =>
setKeyState((prev) => ({
...prev,
newKeyName: e.target.value,
}))
}
placeholder="My AutoGPT Platform API Key"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description (Optional)</Label>
<Input
id="description"
value={keyState.newKeyDescription}
onChange={(e) =>
setKeyState((prev) => ({
...prev,
newKeyDescription: e.target.value,
}))
}
placeholder="Used for..."
/>
</div>
<div className="grid gap-2">
<Label>Permissions</Label>
{Object.values(APIKeyPermission).map((permission) => (
<div className="flex items-center space-x-2" key={permission}>
<Checkbox
id={permission}
checked={keyState.selectedPermissions.includes(permission)}
onCheckedChange={(checked: boolean) => {
setKeyState((prev) => ({
...prev,
selectedPermissions: checked
? [...prev.selectedPermissions, permission]
: prev.selectedPermissions.filter(
(p) => p !== permission,
),
}));
}}
/>
<Label htmlFor={permission}>{permission}</Label>
</div>
))}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreateOpen(false)}>
Cancel
</Button>
<Button onClick={handleCreateKey} disabled={isCreating}>
Create
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isKeyDialogOpen} onOpenChange={setIsKeyDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>AutoGPT Platform API Key Created</DialogTitle>
<DialogDescription>
Please copy your AutoGPT API key now. You won&apos;t be able to
see it again!
</DialogDescription>
</DialogHeader>
<div className="flex items-center space-x-2">
<code className="flex-1 rounded-md bg-secondary p-2 text-sm">
{keyState.newApiKey}
</code>
<Button size="icon" variant="outline" onClick={handleCopyKey}>
<LuCopy className="h-4 w-4" />
</Button>
</div>
<DialogFooter>
<Button onClick={() => setIsKeyDialogOpen(false)}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};

View File

@@ -0,0 +1,78 @@
"use client";
import {
getGetV1ListUserApiKeysQueryKey,
usePostV1CreateNewApiKey,
} from "@/api/__generated__/endpoints/api-keys/api-keys";
import { APIKeyPermission } from "@/api/__generated__/models/aPIKeyPermission";
import { CreateAPIKeyResponse } from "@/api/__generated__/models/createAPIKeyResponse";
import { useToast } from "@/components/ui/use-toast";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { useState } from "react";
export const useAPIkeysModals = () => {
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isKeyDialogOpen, setIsKeyDialogOpen] = useState(false);
const [keyState, setKeyState] = useState({
newKeyName: "",
newKeyDescription: "",
newApiKey: "",
selectedPermissions: [] as APIKeyPermission[],
});
const queryClient = getQueryClient();
const { toast } = useToast();
const { mutateAsync: createAPIKey, isPending: isCreating } =
usePostV1CreateNewApiKey({
mutation: {
onSettled: () => {
return queryClient.invalidateQueries({
queryKey: getGetV1ListUserApiKeysQueryKey(),
});
},
},
});
const handleCreateKey = async () => {
try {
const response = await createAPIKey({
data: {
name: keyState.newKeyName,
permissions: keyState.selectedPermissions,
description: keyState.newKeyDescription,
},
});
setKeyState((prev) => ({
...prev,
newApiKey: (response.data as CreateAPIKeyResponse).plain_text_key,
}));
setIsCreateOpen(false);
setIsKeyDialogOpen(true);
} catch {
toast({
title: "Error",
description: "Failed to create AutoGPT Platform API key",
variant: "destructive",
});
}
};
const handleCopyKey = () => {
navigator.clipboard.writeText(keyState.newApiKey);
toast({
title: "Copied",
description: "AutoGPT Platform API key copied to clipboard",
});
};
return {
isCreating,
handleCreateKey,
handleCopyKey,
setIsCreateOpen,
setIsKeyDialogOpen,
isCreateOpen,
isKeyDialogOpen,
keyState,
setKeyState,
};
};

View File

@@ -1,12 +1,31 @@
import { Metadata } from "next/types";
import { APIKeysSection } from "@/components/agptui/composite/APIKeySection";
import { APIKeysSection } from "@/app/(platform)/profile/(user)/api_keys/components/APIKeySection/APIKeySection";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { APIKeysModals } from "./components/APIKeysModals/APIKeysModals";
export const metadata: Metadata = { title: "API Keys - AutoGPT Platform" };
const ApiKeysPage = () => {
return (
<div className="w-full pr-4 pt-24 md:pt-0">
<APIKeysSection />
<Card>
<CardHeader>
<CardTitle>AutoGPT Platform API Keys</CardTitle>
<CardDescription>
Manage your AutoGPT Platform API keys for programmatic access
</CardDescription>
</CardHeader>
<CardContent>
<APIKeysModals />
<APIKeysSection />
</CardContent>
</Card>
</div>
);
};

View File

@@ -2,8 +2,11 @@
import { revalidatePath } from "next/cache";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import BackendApi from "@/lib/autogpt-server-api";
import { NotificationPreferenceDTO } from "@/lib/autogpt-server-api/types";
import {
postV1UpdateNotificationPreferences,
postV1UpdateUserEmail,
} from "@/api/__generated__/endpoints/auth/auth";
export async function updateSettings(formData: FormData) {
const supabase = await getServerSupabase();
@@ -29,8 +32,7 @@ export async function updateSettings(formData: FormData) {
const { error: emailError } = await supabase.auth.updateUser({
email,
});
const api = new BackendApi();
await api.updateUserEmail(email);
await postV1UpdateUserEmail(email);
if (emailError) {
throw new Error(`${emailError.message}`);
@@ -38,7 +40,6 @@ export async function updateSettings(formData: FormData) {
}
try {
const api = new BackendApi();
const preferences: NotificationPreferenceDTO = {
email: user?.email || "",
preferences: {
@@ -55,7 +56,7 @@ export async function updateSettings(formData: FormData) {
},
daily_limit: 0,
};
await api.updateUserPreferences(preferences);
await postV1UpdateNotificationPreferences(preferences);
} catch (error) {
console.error(error);
throw new Error(`Failed to update preferences: ${error}`);
@@ -64,9 +65,3 @@ export async function updateSettings(formData: FormData) {
revalidatePath("/profile/settings");
return { success: true };
}
export async function getUserPreferences(): Promise<NotificationPreferenceDTO> {
const api = new BackendApi();
const preferences = await api.getUserPreferences();
return preferences;
}

View File

@@ -1,10 +1,5 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { User } from "@supabase/supabase-js";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -18,100 +13,22 @@ import {
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
import { updateSettings } from "@/app/(platform)/profile/(user)/settings/actions";
import { toast } from "@/components/ui/use-toast";
import { NotificationPreferenceDTO } from "@/lib/autogpt-server-api";
import { NotificationPreference } from "@/api/__generated__/models/notificationPreference";
import { User } from "@supabase/supabase-js";
import { useSettingsForm } from "./useSettingsForm";
const formSchema = z
.object({
email: z.string().email(),
password: z
.string()
.optional()
.refine((val) => {
// If password is provided, it must be at least 8 characters
if (val) return val.length >= 12;
return true;
}, "String must contain at least 12 character(s)"),
confirmPassword: z.string().optional(),
notifyOnAgentRun: z.boolean(),
notifyOnZeroBalance: z.boolean(),
notifyOnLowBalance: z.boolean(),
notifyOnBlockExecutionFailed: z.boolean(),
notifyOnContinuousAgentError: z.boolean(),
notifyOnDailySummary: z.boolean(),
notifyOnWeeklySummary: z.boolean(),
notifyOnMonthlySummary: z.boolean(),
})
.refine(
(data) => {
if (data.password || data.confirmPassword) {
return data.password === data.confirmPassword;
}
return true;
},
{
message: "Passwords do not match",
path: ["confirmPassword"],
},
);
interface SettingsFormProps {
export const SettingsForm = ({
preferences,
user,
}: {
preferences: NotificationPreference;
user: User;
preferences: NotificationPreferenceDTO;
}
export default function SettingsForm({ user, preferences }: SettingsFormProps) {
const defaultValues = {
email: user.email || "",
password: "",
confirmPassword: "",
notifyOnAgentRun: preferences.preferences.AGENT_RUN,
notifyOnZeroBalance: preferences.preferences.ZERO_BALANCE,
notifyOnLowBalance: preferences.preferences.LOW_BALANCE,
notifyOnBlockExecutionFailed:
preferences.preferences.BLOCK_EXECUTION_FAILED,
notifyOnContinuousAgentError:
preferences.preferences.CONTINUOUS_AGENT_ERROR,
notifyOnDailySummary: preferences.preferences.DAILY_SUMMARY,
notifyOnWeeklySummary: preferences.preferences.WEEKLY_SUMMARY,
notifyOnMonthlySummary: preferences.preferences.MONTHLY_SUMMARY,
};
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues,
}) => {
const { form, onSubmit, onCancel } = useSettingsForm({
preferences,
user,
});
async function onSubmit(values: z.infer<typeof formSchema>) {
try {
const formData = new FormData();
Object.entries(values).forEach(([key, value]) => {
if (key !== "confirmPassword") {
formData.append(key, value.toString());
}
});
await updateSettings(formData);
toast({
title: "Successfully updated settings",
});
} catch (error) {
toast({
title: "Error",
description:
error instanceof Error ? error.message : "Something went wrong",
variant: "destructive",
});
throw error;
}
}
function onCancel() {
form.reset(defaultValues);
}
return (
<Form {...form}>
<form
@@ -396,4 +313,4 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
</form>
</Form>
);
}
};

View File

@@ -0,0 +1,51 @@
import { z } from "zod";
export const formSchema = z
.object({
email: z.string().email(),
password: z
.string()
.optional()
.refine((val) => {
if (val) return val.length >= 12;
return true;
}, "String must contain at least 12 character(s)"),
confirmPassword: z.string().optional(),
notifyOnAgentRun: z.boolean(),
notifyOnZeroBalance: z.boolean(),
notifyOnLowBalance: z.boolean(),
notifyOnBlockExecutionFailed: z.boolean(),
notifyOnContinuousAgentError: z.boolean(),
notifyOnDailySummary: z.boolean(),
notifyOnWeeklySummary: z.boolean(),
notifyOnMonthlySummary: z.boolean(),
})
.refine((data) => {
if (data.password || data.confirmPassword) {
return data.password === data.confirmPassword;
}
return true;
});
export const createDefaultValues = (
user: { email?: string },
preferences: { preferences?: Record<string, boolean> },
) => {
const defaultValues = {
email: user.email || "",
password: "",
confirmPassword: "",
notifyOnAgentRun: preferences.preferences?.AGENT_RUN,
notifyOnZeroBalance: preferences.preferences?.ZERO_BALANCE,
notifyOnLowBalance: preferences.preferences?.LOW_BALANCE,
notifyOnBlockExecutionFailed:
preferences.preferences?.BLOCK_EXECUTION_FAILED,
notifyOnContinuousAgentError:
preferences.preferences?.CONTINUOUS_AGENT_ERROR,
notifyOnDailySummary: preferences.preferences?.DAILY_SUMMARY,
notifyOnWeeklySummary: preferences.preferences?.WEEKLY_SUMMARY,
notifyOnMonthlySummary: preferences.preferences?.MONTHLY_SUMMARY,
};
return defaultValues;
};

View File

@@ -0,0 +1,57 @@
"use client";
import { useForm } from "react-hook-form";
import { createDefaultValues, formSchema } from "./helper";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { updateSettings } from "../../actions";
import { useToast } from "@/components/ui/use-toast";
import { NotificationPreference } from "@/api/__generated__/models/notificationPreference";
import { User } from "@supabase/supabase-js";
export const useSettingsForm = ({
preferences,
user,
}: {
preferences: NotificationPreference;
user: User;
}) => {
const { toast } = useToast();
const defaultValues = createDefaultValues(user, preferences);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues,
});
async function onSubmit(values: z.infer<typeof formSchema>) {
try {
const formData = new FormData();
Object.entries(values).forEach(([key, value]) => {
if (key !== "confirmPassword") {
formData.append(key, value.toString());
}
});
await updateSettings(formData);
toast({
title: "Successfully updated settings",
});
} catch (error) {
toast({
title: "Error",
description:
error instanceof Error ? error.message : "Something went wrong",
variant: "destructive",
});
throw error;
}
}
function onCancel() {
form.reset(defaultValues);
}
return { form, onSubmit, onCancel };
};

View File

@@ -1,23 +1,37 @@
"use client";
import { useGetV1GetNotificationPreferences } from "@/api/__generated__/endpoints/auth/auth";
import { SettingsForm } from "@/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import * as React from "react";
import { Metadata } from "next";
import SettingsForm from "@/components/profile/settings/SettingsForm";
import { getServerUser } from "@/lib/supabase/server/getServerUser";
import SettingsLoading from "./loading";
import { redirect } from "next/navigation";
import { getUserPreferences } from "./actions";
export const metadata: Metadata = {
title: "Settings - AutoGPT Platform",
description: "Manage your account settings and preferences.",
};
export default function SettingsPage() {
const {
data: preferences,
isError,
isLoading,
} = useGetV1GetNotificationPreferences({
query: {
select: (res) => {
return res.data;
},
},
});
export default async function SettingsPage() {
const { user, error } = await getServerUser();
const { user, isUserLoading } = useSupabase();
if (error || !user) {
if (isLoading || isUserLoading) {
return <SettingsLoading />;
}
if (!user) {
redirect("/login");
}
const preferences = await getUserPreferences();
if (isError || !preferences || !preferences.preferences) {
return "Errror..."; // TODO: Will use a Error reusable components from Block Menu redesign
}
return (
<div className="container max-w-2xl space-y-6 py-10">
@@ -27,7 +41,7 @@ export default async function SettingsPage() {
Manage your account settings and preferences.
</p>
</div>
<SettingsForm user={user} preferences={preferences} />
<SettingsForm preferences={preferences} user={user} />
</div>
);
}

View File

@@ -1,296 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { APIKey, APIKeyPermission } from "@/lib/autogpt-server-api/types";
import { LuCopy } from "react-icons/lu";
import { Loader2, MoreVertical } from "lucide-react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useToast } from "@/components/ui/use-toast";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function APIKeysSection() {
const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isKeyDialogOpen, setIsKeyDialogOpen] = useState(false);
const [newKeyName, setNewKeyName] = useState("");
const [newKeyDescription, setNewKeyDescription] = useState("");
const [newApiKey, setNewApiKey] = useState("");
const [selectedPermissions, setSelectedPermissions] = useState<
APIKeyPermission[]
>([]);
const { toast } = useToast();
const api = useBackendAPI();
useEffect(() => {
loadAPIKeys();
}, []);
const loadAPIKeys = async () => {
setIsLoading(true);
try {
const keys = await api.listAPIKeys();
setApiKeys(keys.filter((key) => key.status === "ACTIVE"));
} finally {
setIsLoading(false);
}
};
const handleCreateKey = async () => {
try {
const response = await api.createAPIKey(
newKeyName,
selectedPermissions,
newKeyDescription,
);
setNewApiKey(response.plain_text_key);
setIsCreateOpen(false);
setIsKeyDialogOpen(true);
loadAPIKeys();
} catch {
toast({
title: "Error",
description: "Failed to create AutoGPT Platform API key",
variant: "destructive",
});
}
};
const handleCopyKey = () => {
navigator.clipboard.writeText(newApiKey);
toast({
title: "Copied",
description: "AutoGPT Platform API key copied to clipboard",
});
};
const handleRevokeKey = async (keyId: string) => {
try {
await api.revokeAPIKey(keyId);
toast({
title: "Success",
description: "AutoGPT Platform API key revoked successfully",
});
loadAPIKeys();
} catch {
toast({
title: "Error",
description: "Failed to revoke AutoGPT Platform API key",
variant: "destructive",
});
}
};
return (
<Card>
<CardHeader>
<CardTitle>AutoGPT Platform API Keys</CardTitle>
<CardDescription>
Manage your AutoGPT Platform API keys for programmatic access
</CardDescription>
</CardHeader>
<CardContent>
<div className="mb-4 flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button>Create Key</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New API Key</DialogTitle>
<DialogDescription>
Create a new AutoGPT Platform API key
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="My AutoGPT Platform API Key"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description (Optional)</Label>
<Input
id="description"
value={newKeyDescription}
onChange={(e) => setNewKeyDescription(e.target.value)}
placeholder="Used for..."
/>
</div>
<div className="grid gap-2">
<Label>Permissions</Label>
{Object.values(APIKeyPermission).map((permission) => (
<div
className="flex items-center space-x-2"
key={permission}
>
<Checkbox
id={permission}
checked={selectedPermissions.includes(permission)}
onCheckedChange={(checked: boolean) => {
setSelectedPermissions(
checked
? [...selectedPermissions, permission]
: selectedPermissions.filter(
(p) => p !== permission,
),
);
}}
/>
<Label htmlFor={permission}>{permission}</Label>
</div>
))}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsCreateOpen(false)}
>
Cancel
</Button>
<Button onClick={handleCreateKey}>Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isKeyDialogOpen} onOpenChange={setIsKeyDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>AutoGPT Platform API Key Created</DialogTitle>
<DialogDescription>
Please copy your AutoGPT API key now. You won&apos;t be able
to see it again!
</DialogDescription>
</DialogHeader>
<div className="flex items-center space-x-2">
<code className="flex-1 rounded-md bg-secondary p-2 text-sm">
{newApiKey}
</code>
<Button size="icon" variant="outline" onClick={handleCopyKey}>
<LuCopy className="h-4 w-4" />
</Button>
</div>
<DialogFooter>
<Button onClick={() => setIsKeyDialogOpen(false)}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{isLoading ? (
<div className="flex justify-center p-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
apiKeys.length > 0 && (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>API Key</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead>Last Used</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiKeys.map((key) => (
<TableRow key={key.id}>
<TableCell>{key.name}</TableCell>
<TableCell>
<div className="rounded-md border p-1 px-2 text-xs">
{`${key.prefix}******************${key.postfix}`}
</div>
</TableCell>
<TableCell>
<Badge
variant={
key.status === "ACTIVE" ? "default" : "destructive"
}
className={
key.status === "ACTIVE"
? "border-green-600 bg-green-100 text-green-800"
: "border-red-600 bg-red-100 text-red-800"
}
>
{key.status}
</Badge>
</TableCell>
<TableCell>
{new Date(key.created_at).toLocaleDateString()}
</TableCell>
<TableCell>
{key.last_used_at
? new Date(key.last_used_at).toLocaleDateString()
: "Never"}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-destructive"
onClick={() => handleRevokeKey(key.id)}
>
Revoke
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
)}
</CardContent>
</Card>
);
}