From 7e6ff1a13ac6d8138bc86517506a35df8eaa07c6 Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Mon, 23 Sep 2024 14:47:54 +0530 Subject: [PATCH 01/10] Refactor: Add tooltip to display available bench commands in APIList component --- .../features/api_viewer/APIList.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/dashboard/src/components/features/api_viewer/APIList.tsx b/dashboard/src/components/features/api_viewer/APIList.tsx index 48e5a3b..0ca836e 100644 --- a/dashboard/src/components/features/api_viewer/APIList.tsx +++ b/dashboard/src/components/features/api_viewer/APIList.tsx @@ -8,6 +8,7 @@ import { AiOutlineBranches } from "react-icons/ai" import { GoPackage } from "react-icons/go" import { CommandContent } from "../commands/CommandsContent" import { HiOutlineCommandLine } from "react-icons/hi2"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" export interface APIListProps { apiList: APIData[] @@ -50,15 +51,24 @@ export const APIList = ({ apiList, app_name, branch_name, setSelectedEndpoint, s

{branch_name}

- - - - - - + + + + + + + + + + + + Click to view available bench commands in this app + + +
From a799be76be61abe24907a99d72583848ed4ef0ed Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Mon, 23 Sep 2024 14:48:14 +0530 Subject: [PATCH 02/10] Refactor: Add tooltip and API call functionality to APIDetails component --- .../features/APIClient/APIClientContent.tsx | 275 ++++++++++++++++++ .../features/api_viewer/APIDetails.tsx | 29 +- 2 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 dashboard/src/components/features/APIClient/APIClientContent.tsx diff --git a/dashboard/src/components/features/APIClient/APIClientContent.tsx b/dashboard/src/components/features/APIClient/APIClientContent.tsx new file mode 100644 index 0000000..e75aee1 --- /dev/null +++ b/dashboard/src/components/features/APIClient/APIClientContent.tsx @@ -0,0 +1,275 @@ +import CopyButton from "@/components/common/CopyToClipboard/CopyToClipboard" +import { Button } from "@/components/ui/button" +import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Argument } from "@/types/APIData" +import { DialogClose } from "@radix-ui/react-dialog" +import { FrappeConfig, FrappeContext } from "frappe-react-sdk" +import { useCallback, useContext, useEffect, useState } from "react" +import { useForm } from "react-hook-form" +import { BsArrowRight } from "react-icons/bs" +import { FaCaretDown } from "react-icons/fa" +import { IoAdd } from "react-icons/io5"; + + +export interface APIClientContentProps { + endpoint: string + parameters?: Argument[] +} + +export const APIClientContent = ({ endpoint, parameters }: APIClientContentProps) => { + + const [requestType, setRequestType] = useState<"GET" | "POST">('GET') + + // create state of parameters which is Record where key is parameter name and value is parameter value and key is one of the argument of parameters + + const [parameterValues, setParameterValues] = useState>({}) + + const { setValue, reset } = useForm() + + const [paramsType, setParamsType] = useState<'params' | 'form-data'>('params') + + const [response, setResponse] = useState({}) + + const onParamsTypeChange = useCallback((type: 'params' | 'form-data') => { + if (type === 'params') { + setParamsType('params') + if (parameters) { + const initialParameterValues: Record = {} + parameters.forEach((parameter) => { + setValue(parameter.argument, '') + initialParameterValues[parameter.argument] = '' + }) + // setParameterValues(initialParameterValues) + } else { + // setParameterValues({}) + reset({}) + + } + } else { + setParamsType('form-data') + // setParameterValues({}) + reset({}) + } + }, [parameters]) + + useEffect(() => { + if (parameters) { + // const initialParameterValues: Record = {} + // parameters.forEach((parameter) => { + // initialParameterValues[parameter.argument] = '' + // }) + // setParameterValues(initialParameterValues) + parameters.forEach((parameter) => { + setValue(parameter.argument, '') + }) + + } + }, [parameters]) + + const { call } = useContext(FrappeContext) as FrappeConfig + + const returnString = (value: any) => { + try { + // If it's a string but contains JSON-like structure, attempt to parse it + const parsedValue = JSON.parse(value); + + if (Array.isArray(parsedValue)) { + return JSON.stringify(parsedValue); // Convert array to string + } else if (typeof parsedValue === 'object' && parsedValue !== null) { + return JSON.stringify(parsedValue); // Convert object to string + } + } catch (error) { + // If JSON.parse fails, it's not a JSON string, continue to the next checks + } + + // Check if it's already a string + if (typeof value === 'string') { + return value; // Return the string as it is + } + + // Check if it's an array + if (Array.isArray(value)) { + return JSON.stringify(value); // Convert array to string + } + + // Check if it's an object + if (typeof value === 'object' && value !== null) { + return JSON.stringify(value); // Convert object to string + } + + // If it's not string, array, or object, return the string representation of the value + return String(value); + } + + const onAPIRequest = () => { + // filter out those parameters which are empty + + const filteredParameterValues = Object.keys(parameterValues).reduce((acc, key) => { + if (parameterValues[key] !== '') { + // check if value is string if it not if it is list or object then convert it to string using JSON.stringify + acc[key] = returnString(parameterValues[key]) + } + return acc + }, {} as Record) + if (requestType === 'GET') { + // call get api with endpoint and parameterValues + call.get(endpoint, filteredParameterValues).then((response) => { + setResponse(response) + }).catch((error) => { + setResponse(error) + }) + } else { + // call post api with endpoint and parameterValues + call.post(endpoint, filteredParameterValues).then((response) => { + setResponse(response) + }).catch((error) => { + setResponse(error) + }) + } + } + + return ( + + + API Request + + Easily test and interact with your API endpoints. + +
+
+ + + + + + setRequestType('GET')}> + GET + + setRequestType('POST')}> + POST + + + +
+ + +
+
+
+
+
+ + + + + + onParamsTypeChange('params')}> + Params + + onParamsTypeChange('form-data')}> + Form Data + + + +
+
+ + + + + Key + + + Value + + + + + {paramsType === 'params' ? + parameters?.filter((params) => params.argument !== '')?.map((parameter) => ( + + {parameter.argument}{parameter.default ? null : *} + + setParameterValues({ ...parameterValues, [parameter.argument]: e.target.value })} + className="h-8" + /> + + + )) : parameterValues && Object.keys(parameterValues).map((key) => ( + + + setParameterValues({ ...parameterValues, [e.target.value]: parameterValues[key] })} + className="h-8" + /> + + + setParameterValues({ ...parameterValues, [key]: e.target.value })} + className="h-8" + /> + + + )) + } + +
+ {/* {paramsType === 'form-data' &&
+ +
} */} +
+
+
+ +
+ +
+
+                                    {
+                                        JSON.stringify(response, null, 2)
+                                    }
+                                
+
+
+
+
+
+ + + + + +
+ ) +} \ No newline at end of file diff --git a/dashboard/src/components/features/api_viewer/APIDetails.tsx b/dashboard/src/components/features/api_viewer/APIDetails.tsx index 3e26c80..3cf0c12 100644 --- a/dashboard/src/components/features/api_viewer/APIDetails.tsx +++ b/dashboard/src/components/features/api_viewer/APIDetails.tsx @@ -11,6 +11,10 @@ import { useFrappeGetCall } from "frappe-react-sdk" import { useMemo } from "react" import { MdOutlineFileDownload } from "react-icons/md" import Markdown from "react-markdown" +import { AiOutlineThunderbolt } from "react-icons/ai" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { Dialog, DialogTrigger } from "@/components/ui/dialog" +import { APIClientContent } from "../APIClient/APIClientContent" export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, setSelectedEndpoint, viewerType }: { project_branch: string, endpointData: APIData[], selectedEndpoint: string, setSelectedEndpoint: React.Dispatch>, viewerType: string }) => { @@ -98,7 +102,30 @@ export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, set
Endpoint :
{data?.api_path}
- +
+ + {viewerType === 'app' && + + + + + + + + + + + Click to make an API call to this endpoint + + + } +
From cbc9707b4745990b3ffb1c41a654e3ca9500485b Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Mon, 23 Sep 2024 20:19:05 +0530 Subject: [PATCH 03/10] Refactor: support for formData --- .../features/APIClient/APIClientContent.tsx | 274 +++++++++--------- 1 file changed, 141 insertions(+), 133 deletions(-) diff --git a/dashboard/src/components/features/APIClient/APIClientContent.tsx b/dashboard/src/components/features/APIClient/APIClientContent.tsx index e75aee1..0413c3d 100644 --- a/dashboard/src/components/features/APIClient/APIClientContent.tsx +++ b/dashboard/src/components/features/APIClient/APIClientContent.tsx @@ -8,7 +8,7 @@ import { Argument } from "@/types/APIData" import { DialogClose } from "@radix-ui/react-dialog" import { FrappeConfig, FrappeContext } from "frappe-react-sdk" import { useCallback, useContext, useEffect, useState } from "react" -import { useForm } from "react-hook-form" +import { FormProvider, useForm } from "react-hook-form" import { BsArrowRight } from "react-icons/bs" import { FaCaretDown } from "react-icons/fa" import { IoAdd } from "react-icons/io5"; @@ -23,45 +23,35 @@ export const APIClientContent = ({ endpoint, parameters }: APIClientContentProps const [requestType, setRequestType] = useState<"GET" | "POST">('GET') - // create state of parameters which is Record where key is parameter name and value is parameter value and key is one of the argument of parameters + const methods = useForm() - const [parameterValues, setParameterValues] = useState>({}) - - const { setValue, reset } = useForm() + const { setValue, reset, register, handleSubmit, watch } = useForm() const [paramsType, setParamsType] = useState<'params' | 'form-data'>('params') const [response, setResponse] = useState({}) + const data = watch() + const onParamsTypeChange = useCallback((type: 'params' | 'form-data') => { if (type === 'params') { setParamsType('params') if (parameters) { - const initialParameterValues: Record = {} parameters.forEach((parameter) => { setValue(parameter.argument, '') - initialParameterValues[parameter.argument] = '' }) - // setParameterValues(initialParameterValues) } else { - // setParameterValues({}) reset({}) } } else { setParamsType('form-data') - // setParameterValues({}) reset({}) } }, [parameters]) useEffect(() => { if (parameters) { - // const initialParameterValues: Record = {} - // parameters.forEach((parameter) => { - // initialParameterValues[parameter.argument] = '' - // }) - // setParameterValues(initialParameterValues) parameters.forEach((parameter) => { setValue(parameter.argument, '') }) @@ -104,26 +94,27 @@ export const APIClientContent = ({ endpoint, parameters }: APIClientContentProps return String(value); } - const onAPIRequest = () => { - // filter out those parameters which are empty - - const filteredParameterValues = Object.keys(parameterValues).reduce((acc, key) => { - if (parameterValues[key] !== '') { + const onSubmit = (data: any) => { + const filteredParameterValues = Object.keys(data).reduce((acc, key) => { + if (data[key] !== '') { // check if value is string if it not if it is list or object then convert it to string using JSON.stringify - acc[key] = returnString(parameterValues[key]) + acc[key] = returnString(data[key]) } return acc }, {} as Record) + + const paramsData = handleFormData(filteredParameterValues) + if (requestType === 'GET') { // call get api with endpoint and parameterValues - call.get(endpoint, filteredParameterValues).then((response) => { + call.get(endpoint, paramsData).then((response) => { setResponse(response) }).catch((error) => { setResponse(error) }) } else { // call post api with endpoint and parameterValues - call.post(endpoint, filteredParameterValues).then((response) => { + call.post(endpoint, paramsData).then((response) => { setResponse(response) }).catch((error) => { setResponse(error) @@ -131,145 +122,162 @@ export const APIClientContent = ({ endpoint, parameters }: APIClientContentProps } } + const handleFormData = (data: any) => { + if (paramsType === 'form-data') { + const formData = new FormData() + Object.keys(data).forEach((key) => { + if (key.startsWith('key-')) { + formData.append(data[key], data[key.replace('key', 'value')]) + } + }) + return formData + } + else { + return Object.keys(data)?.filter((key) => !(key.includes('key') || key.includes('value'))).reduce((acc, key) => { + acc[key] = data[key] + return acc + }, {} as Record) + } + } + return ( - - - API Request - - Easily test and interact with your API endpoints. - -
-
- - - + + + setRequestType('GET')}> + GET + + setRequestType('POST')}> + POST + + + +
+ + - - - setRequestType('GET')}> - GET - - setRequestType('POST')}> - POST - - - -
- - -
-
-
-
-
- - - - - - onParamsTypeChange('params')}> - Params - - onParamsTypeChange('form-data')}> - Form Data - - -
-
- - - - - Key - - - Value - - - - - {paramsType === 'params' ? - parameters?.filter((params) => params.argument !== '')?.map((parameter) => ( + +
+
+
+ + + + + + onParamsTypeChange('params')}> + Params + + onParamsTypeChange('form-data')}> + Form Data + + + +
+
+
+ + + + Key + + + Value + + + + + {paramsType === 'params' ? parameters?.filter((params) => params.argument !== '')?.map((parameter) => ( {parameter.argument}{parameter.default ? null : *} setParameterValues({ ...parameterValues, [parameter.argument]: e.target.value })} + {...register(parameter.argument)} className="h-8" /> - )) : parameterValues && Object.keys(parameterValues).map((key) => ( + )) : data && Object.keys(data)?.filter((key) => key.startsWith('key-')).map((key) => ( setParameterValues({ ...parameterValues, [e.target.value]: parameterValues[key] })} + {...register(key)} className="h-8" /> setParameterValues({ ...parameterValues, [key]: e.target.value })} + {...register(key.replace('key', 'value'))} className="h-8" /> - )) + ))} + +
+ {paramsType === 'form-data' &&
+ +
} +
+
+
+ +
+ +
+
+                                        {
+                                            JSON.stringify(response, null, 2)
                                         }
-                                    
-                                
-                                {/* {paramsType === 'form-data' && 
- -
} */} +
+
-
- -
- -
-
-                                    {
-                                        JSON.stringify(response, null, 2)
-                                    }
-                                
-
+
+ Note: Please use double quotes (") for parameters instead of single quotes (').
-
- - - - - - - + + + + + + + + ) } \ No newline at end of file From c90f382a597e1a6489f5b1fb9e31bbe2c752d8ea Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Mon, 23 Sep 2024 20:34:48 +0530 Subject: [PATCH 04/10] Refactor: Reset form data in APIClientContent component --- .../src/components/features/APIClient/APIClientContent.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dashboard/src/components/features/APIClient/APIClientContent.tsx b/dashboard/src/components/features/APIClient/APIClientContent.tsx index 0413c3d..45e55e1 100644 --- a/dashboard/src/components/features/APIClient/APIClientContent.tsx +++ b/dashboard/src/components/features/APIClient/APIClientContent.tsx @@ -59,6 +59,10 @@ export const APIClientContent = ({ endpoint, parameters }: APIClientContentProps } }, [parameters]) + useEffect(() => { + reset({}) + },[]) + const { call } = useContext(FrappeContext) as FrappeConfig const returnString = (value: any) => { From c87dad04324e6ada7031195056c13cd0e50582af Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Tue, 24 Sep 2024 12:33:59 +0530 Subject: [PATCH 05/10] fix:open app selection modal if apps is not present in location.state --- dashboard/src/pages/features/erd/ERDViewer.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/dashboard/src/pages/features/erd/ERDViewer.tsx b/dashboard/src/pages/features/erd/ERDViewer.tsx index 318d4af..000c5b6 100644 --- a/dashboard/src/pages/features/erd/ERDViewer.tsx +++ b/dashboard/src/pages/features/erd/ERDViewer.tsx @@ -23,9 +23,9 @@ export const ERDViewer = () => { const location = useLocation() - const { apps } = location.state as { apps: string[] } + const { apps } = location.state as { apps: string[] } || {} - const [selectedApps, setSelectedApps] = useState(apps) + const [selectedApps, setSelectedApps] = useState(apps ?? []) const [erdDoctypes, setERDDocTypes] = useState<{ doctype: string, project_branch: string }[]>([]) @@ -39,6 +39,12 @@ export const ERDViewer = () => { }, []) + useEffect(() => { + if (!apps) { + setOpen(true) + } + }, [apps]) + const flowRef = useRef(null) return ( @@ -113,6 +119,12 @@ export const ModuleDoctypeListDrawer = ({ open, setOpen, apps, setSelectedApps, setOpenDialog(false) } + useEffect(() => { + if (apps.length === 0) { + setOpenDialog(true) + } + }, []) + return ( <> @@ -145,7 +157,7 @@ export const ModuleDoctypeListDrawer = ({ open, setOpen, apps, setSelectedApps, : null} - {apps.length ? : null} +
From 95a35879a2b8d239873b9b30c6fbf0df50b3fd03 Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Tue, 24 Sep 2024 13:39:49 +0530 Subject: [PATCH 06/10] Refactor: Simplify getting commands from app module --- commit/api/get_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commit/api/get_commands.py b/commit/api/get_commands.py index 218a4ec..b64187a 100644 --- a/commit/api/get_commands.py +++ b/commit/api/get_commands.py @@ -56,8 +56,8 @@ def get_site_app_commands(app: str) -> dict: # Call get_commands if it is a callable command_list = [] - if hasattr(app_command_module, 'get_commands') and callable(getattr(app_command_module, 'get_commands')): - commands_from_function = app_command_module.get_commands() + if hasattr(app_command_module, 'commands'): + commands_from_function = app_command_module.commands if commands_from_function: for command_instance in commands_from_function: help_text = getattr(command_instance, 'help', 'No help text available') From 2f58fc3fb8f274366ec5f31dd5bb56cea2df5d58 Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Tue, 24 Sep 2024 17:06:34 +0530 Subject: [PATCH 07/10] Refactor: Add padding to command name in CommandComponent --- dashboard/src/components/features/commands/CommandsContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/components/features/commands/CommandsContent.tsx b/dashboard/src/components/features/commands/CommandsContent.tsx index 195111c..eb1bebd 100644 --- a/dashboard/src/components/features/commands/CommandsContent.tsx +++ b/dashboard/src/components/features/commands/CommandsContent.tsx @@ -85,7 +85,7 @@ const CommandComponent = ({ name, help }: CommandResponse) => { return (
-
+
{`bench ${name}`}
Date: Wed, 25 Sep 2024 16:44:25 +0530 Subject: [PATCH 08/10] Refactor: Remove unnecessary code in Header component and adjust styling in Overview component --- dashboard/src/components/common/Header.tsx | 13 ------------- dashboard/src/pages/overview/Overview.tsx | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/dashboard/src/components/common/Header.tsx b/dashboard/src/components/common/Header.tsx index e166546..f60b0fc 100644 --- a/dashboard/src/components/common/Header.tsx +++ b/dashboard/src/components/common/Header.tsx @@ -1,8 +1,5 @@ import { Link } from 'react-router-dom' import CommitLogo from '../../assets/commit-logo.png' -import { Button } from "@/components/ui/button" -import { GitHubLogoIcon } from '@radix-ui/react-icons' - export const Header = ({ text }: { text?: string }) => { return ( @@ -13,16 +10,6 @@ export const Header = ({ text }: { text?: string }) => { {text &&

{text}

}
-
- - -
) } \ No newline at end of file diff --git a/dashboard/src/pages/overview/Overview.tsx b/dashboard/src/pages/overview/Overview.tsx index af8c453..64090d7 100644 --- a/dashboard/src/pages/overview/Overview.tsx +++ b/dashboard/src/pages/overview/Overview.tsx @@ -9,9 +9,9 @@ export const Overview = () => { const areAppsAvailable = isSystemAppAvailable() return ( -
+
- {areAppsAvailable ? + {areAppsAvailable ? Projects Site Apps From c11cae779fe05ca5dbdffdc7be10a3b06329cc50 Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Wed, 25 Sep 2024 16:45:02 +0530 Subject: [PATCH 09/10] chore:Styling and layout fix --- .../features/api_viewer/APIDetails.tsx | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/dashboard/src/components/features/api_viewer/APIDetails.tsx b/dashboard/src/components/features/api_viewer/APIDetails.tsx index 3cf0c12..ba2ac51 100644 --- a/dashboard/src/components/features/api_viewer/APIDetails.tsx +++ b/dashboard/src/components/features/api_viewer/APIDetails.tsx @@ -8,12 +8,12 @@ import { web_url } from "@/config/socket" import { APIData, Argument } from "@/types/APIData" import { XMarkIcon } from "@heroicons/react/24/outline" import { useFrappeGetCall } from "frappe-react-sdk" -import { useMemo } from "react" +import { useMemo, useState } from "react" import { MdOutlineFileDownload } from "react-icons/md" import Markdown from "react-markdown" import { AiOutlineThunderbolt } from "react-icons/ai" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { Dialog, DialogTrigger } from "@/components/ui/dialog" +import { Dialog } from "@/components/ui/dialog" import { APIClientContent } from "../APIClient/APIClientContent" export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, setSelectedEndpoint, viewerType }: { project_branch: string, endpointData: APIData[], selectedEndpoint: string, setSelectedEndpoint: React.Dispatch>, viewerType: string }) => { @@ -65,11 +65,13 @@ export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, set } } + const [apiOpen, setApiOpen] = useState(false) + return (
-

API Details

+

{data?.name}

{data?.allow_guest || data?.xss_safe ?
{data?.allow_guest && Allow Guest @@ -94,33 +96,27 @@ export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, set
-
-
Name :
-
{data?.name}
-
-
+
Endpoint :
-
-
{data?.api_path}
+
+
+ {data?.api_path} +
+
+
{viewerType === 'app' && - - - - - - - - - + + + + Click to make an API call to this endpoint @@ -167,6 +163,9 @@ export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, set } + + +
) } @@ -224,7 +223,7 @@ export const CodeSnippet = ({ apiData, project_branch, file_path, viewerType }: {isLoading &&
} - +
@@ -286,7 +285,7 @@ export const Bruno = ({ doc }: { doc: APIData }) => { {isLoading &&
} - +
+
+
- - - - - )