Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions commit/api/api_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ def get_apis_for_project(project_branch: str):

apis = json.loads(branch_doc.whitelisted_apis).get("apis", []) if branch_doc.whitelisted_apis else []
documentation = json.loads(branch_doc.documentation).get("apis", []) if branch_doc.documentation else []
print('documentation', len(documentation))
for api in apis:
# find the documentation for the api whose function_name equals to name and path same as path
for doc in documentation:
Expand All @@ -33,7 +32,8 @@ def get_apis_for_project(project_branch: str):
"org_logo": org_logo,
"branch_name": branch_doc.branch_name,
"project_branch": branch_doc.name,
"last_updated": branch_doc.last_fetched
"last_updated": branch_doc.last_fetched,
'path_to_folder':branch_doc.path_to_folder
}


Expand Down
70 changes: 70 additions & 0 deletions commit/api/get_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import importlib
import sys
import traceback
import os
import frappe
from frappe.utils.bench_helper import get_app_commands

@frappe.whitelist(allow_guest=True)
def get_project_app_commands(app: str, app_path: str = None) -> dict:
'''
Gets the commands for the app
'''
if not app_path or app_path == '':
# Check the permissions of the user
if not frappe.has_permission('System Manager'):
return frappe.throw('You do not have permission to access this resource', frappe.PermissionError)
return get_site_app_commands(app)
else:
ret = []
try:
if app_path:
# Add the app's directory to the Python path
sys.path.append(app_path)

app_command_module = importlib.import_module(f"{app}.commands")
except ModuleNotFoundError as e:
if e.name == f"{app}.commands":
return ret
traceback.print_exc()
return ret
except Exception:
traceback.print_exc()
return ret
finally:
if app_path:
# Remove the app's directory from the Python path to avoid side effects
sys.path.remove(app_path)

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 commands_from_function:
for command_instance in commands_from_function:
help_text = getattr(command_instance, 'help', 'No help text available')
name = getattr(command_instance, 'name', [])
obj = {
'name': name,
'help': help_text
}
command_list.append(obj)
return command_list

@frappe.whitelist()
def get_site_app_commands(app: str) -> dict:
app_command_module = importlib.import_module(f"{app}.commands")
# 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 commands_from_function:
for command_instance in commands_from_function:
help_text = getattr(command_instance, 'help', 'No help text available')
name = getattr(command_instance, 'name', [])
obj = {
'name': name,
'help': help_text
}
command_list.append(obj)
return command_list
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const ErrorBanner = ({ error, overrideHeading, ...props }: ErrorBannerPro

// TODO: Sometimes, error message has links which route to the ERPNext interface. We need to parse the link to route to the correct page in our interface
// Links are of format <a href="{host_name}/app/{doctype}/{name}">LEAD-00001</a>

if (!error) return null
return (
<div className="bg-red-50 border-l-4 border-red-400 p-4">
<div className="flex">
Expand Down
9 changes: 6 additions & 3 deletions dashboard/src/components/features/api_viewer/APIDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,16 @@ export const ParametersTable = ({ parameters }: { parameters?: Argument[] }) =>
</TableRow>
</TableHeader>
<TableBody>
{parameters?.map((parameter) => (
{parameters?.map((parameter) => {
const isMandatory = parameter.argument !== '' && parameter.argument && !parameter.default
return (
<TableRow key={parameter.argument} className="font-light text-sm">
<TableCell>{parameter.argument}</TableCell>
<TableCell>{parameter.argument}{isMandatory ? <span className="text-red-500 ml-1">*</span> : ''}</TableCell>
<TableCell>{parameter.type ? parameter.type : '-'}</TableCell>
<TableCell>{parameter.default ? parameter.default : '-'}</TableCell>
</TableRow>
))}
)
})}
</TableBody>
</Table>
)
Expand Down
36 changes: 26 additions & 10 deletions dashboard/src/components/features/api_viewer/APIList.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { Button } from "@/components/ui/button"
import { Dialog, DialogTrigger } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { APIData } from "@/types/APIData"
import { useEffect, useMemo, useState } from "react"
import { AiOutlineBranches } from "react-icons/ai"
import { GoPackage } from "react-icons/go"
import { CommandContent } from "../commands/CommandsContent"
import { HiOutlineCommandLine } from "react-icons/hi2";

export interface APIListProps {
apiList: APIData[]
app_name: string
branch_name: string
setSelectedEndpoint: (endpoint: string) => void
selectedEndpoint?: string
path_to_folder: string
}

export const APIList = ({ apiList, app_name, branch_name, setSelectedEndpoint, selectedEndpoint }: APIListProps) => {
export const APIList = ({ apiList, app_name, branch_name, setSelectedEndpoint, selectedEndpoint, path_to_folder }: APIListProps) => {
const [searchQuery, setSearchQuery] = useState<string>('')
const [requestTypeFilter, setRequestTypeFilter] = useState<string>('All')

Expand All @@ -33,16 +38,27 @@ export const APIList = ({ apiList, app_name, branch_name, setSelectedEndpoint, s

return (
<div className="flex flex-col space-y-4 p-3 border-r border-gray-200">
<div className="flex space-x-2 items-center">
<div className="flex flex-wrap items-center space-x-1">
<GoPackage />
<p className="truncate text-md text-gray-700">{app_name}</p>
</div>
<div className="w-px h-4 bg-gray-200" />
<div className="flex flex-wrap items-center space-x-1">
<AiOutlineBranches />
<p>{branch_name}</p>
<div className="flex flex-row space-x-4 justify-between">
<div className="flex space-x-2 items-center">
<div className="flex flex-wrap items-center space-x-1">
<GoPackage />
<p className="truncate text-md text-gray-700">{app_name}</p>
</div>
<div className="w-px h-4 bg-gray-200" />
<div className="flex flex-wrap items-center space-x-1">
<AiOutlineBranches />
<p>{branch_name}</p>
</div>
</div>
<Dialog>
<DialogTrigger asChild>
<Button aria-label="View Bench Commands" size={'sm'} variant={'outline'}>
<HiOutlineCommandLine className="h-4 w-4 mr-2" />
Commands
</Button>
</DialogTrigger>
<CommandContent app={app_name} app_path={path_to_folder} />
</Dialog>
</div>
<div className="flex flex-row space-x-4">
<div className="w-4/5 flex flex-row space-x-4">
Expand Down
100 changes: 100 additions & 0 deletions dashboard/src/components/features/commands/CommandsContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import CopyButton from "@/components/common/CopyToClipboard/CopyToClipboard"
import { ErrorBanner } from "@/components/common/ErrorBanner/ErrorBanner"
import { FullPageLoader } from "@/components/common/FullPageLoader/FullPageLoader"
import { Button } from "@/components/ui/button"
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { DialogClose } from "@radix-ui/react-dialog"
import { useFrappeGetCall } from "frappe-react-sdk"
import { useMemo, useState } from "react"

export interface CommandContentProps {
app: string,
app_path: string
}

export interface CommandResponse {
name: string,
help: string,
}

export const CommandContent = ({ app, app_path }: CommandContentProps) => {

const { data, error, isLoading } = useFrappeGetCall<{ message: CommandResponse[] }>('commit.api.get_commands.get_project_app_commands', {
app: app,
app_path: app_path
}, undefined, {
revalidateOnFocus: false,
revalidateIfStale: false,
})

const [searchQuery, setSearchQuery] = useState<string>('')

const filteredData = useMemo(() => {
return data?.message?.filter((command) => command.name.toLowerCase().includes(searchQuery.toLowerCase()) || command.help.toLowerCase().includes(searchQuery.toLowerCase()))
}, [data, searchQuery])

return (
<DialogContent className="p-6 w-[90vw] sm:w-full overflow-hidden">
<DialogHeader className="text-left">
<DialogTitle>Commands</DialogTitle>
<DialogDescription>
View bench commands for {app} app
</DialogDescription>
</DialogHeader>
<ErrorBanner error={error} />
{isLoading && <FullPageLoader />}
<div className="max-h-[60vh] overflow-y-scroll">
<div className="flex flex-col gap-1">
<div className="flex flex-row pl-1 pr-4 py-1">
<Input placeholder="Search" onChange={(e) => setSearchQuery(e.target.value)} className="h-8" value={searchQuery} />
</div>

{filteredData && filteredData?.length > 0 ? <div className="divide-y divide-gray-200">
{filteredData?.map((command: CommandResponse) => <CommandComponent key={command.name} name={command.name} help={command.help} />)}
</div> : <div className="text-gray-500">No commands found</div>}
</div>
</div>
<DialogFooter>
<div className="flex flex-col items-end gap-1">
<div className="text-xs text-gray-500">Total {filteredData?.length} Command(s)</div>
<DialogClose asChild>
<Button variant="outline">Close</Button>
</DialogClose>
</div>
</DialogFooter>
</DialogContent>
)
}

const CommandComponent = ({ name, help }: CommandResponse) => {
// Regular expression to detect URLs
const urlRegex = /(https?:\/\/[^\s]+)/g;

// Function to wrap links in anchor tags
const renderHelpText = (text: string) => {
const parts = text.split(urlRegex);
return parts.map((part, index) => {
if (urlRegex.test(part)) {
return <a key={index} href={part} className="text-blue-500 underline" target="_blank" rel="noopener noreferrer">{part}</a>;
}
return part;
});
};

return (
<div className="flex flex-col text-sm py-2 pr-4 pl-1">
<code className="flex justify-between items-start bg-gray-100 px-1 border-2 border-gray-200 rounded-md text-sm">
<div>
{`bench ${name}`}
</div>
<CopyButton value={
`bench ${name}`
} className="h-6 w-6 hover:bg-gray-300" />
</code>
<div className="text-gray-500 text-xs px-1 pt-1">
{renderHelpText(help)}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@ export const YourAppAPIExplorer = () => {
<Card className="flex flex-col sm:flex-row items-start p-2 border rounded-lg w-full h-full shadow-sm bg-white relative">
<div className="flex-grow h-full">
<CardHeader className="pb-4">
<CardTitle>Explore your API's</CardTitle>
<CardTitle>Explore your API's and Bench Commands</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-gray-500 sm:mb-8">
Explore and interact with your site installed apps whitelisted API's effortlessly
using our API Explorer.
Effortlessly explore and interact with the whitelisted API's and Bench commands of your site's installed apps using our API Explorer.
</div>
</CardContent>

Expand Down
5 changes: 2 additions & 3 deletions dashboard/src/components/features/projects/APIExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@ export const APIExplorer = () => {
<Card className="flex flex-col sm:flex-row items-start p-2 border rounded-lg w-full h-full shadow-sm bg-white relative">
<div className="flex-grow h-full">
<CardHeader className="pb-4">
<CardTitle>Explore your API's</CardTitle>
<CardTitle>Explore your API's and Bench Commands</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-gray-500 sm:mb-8">
Explore and interact with your whitelisted API's effortlessly
using our API Explorer.
Effortlessly explore and interact with your whitelisted API's and Bench commands using our API Explorer.
</div>
</CardContent>

Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/pages/features/api_viewer/APIViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface GetAPIResponse {
org_logo?: string,
last_updated: string,
project_branch: string,
path_to_folder: string
}
export const APIViewerContainer = () => {
const { ID } = useParams()
Expand Down Expand Up @@ -57,6 +58,7 @@ export const APIViewer = ({ projectBranch }: { projectBranch: string }) => {
branch_name={data?.message.branch_name ?? ''}
setSelectedEndpoint={setSelectedEndpoint}
selectedEndpoint={selectedendpoint}
path_to_folder={data?.message.path_to_folder}
/>
</div>

Expand Down
1 change: 1 addition & 0 deletions dashboard/src/pages/features/api_viewer/AppAPIViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const AppAPIViewer = ({ appName }: { appName: string }) => {
branch_name={data?.message.branch_name ?? ''}
setSelectedEndpoint={setSelectedEndpoint}
selectedEndpoint={selectedendpoint}
path_to_folder=""
/>

</div>
Expand Down