This commit is contained in:
Vyn 2025-06-11 09:50:52 +02:00
commit 61cbd57af1
168 changed files with 31208 additions and 0 deletions

View file

@ -0,0 +1,3 @@
export default function AppName() {
return <span className="text-4xl"><span className="text-app-primary">Awary</span></span>
}

View file

@ -0,0 +1,41 @@
import {ButtonHTMLAttributes, useState} from "react";
import Spinner from "./Spinner";
const buttons: {[index: string]: string} = {
primary: `inline-block px-6 py-2.5 bg-primary text-white font-medium text-xs leading-tight uppercase rounded hover:bg-primary-hover focus:bg-primary focus:outline-none focus:ring-0 active:bg-primary-active transition duration-150 ease-in-out`,
danger: "inline-block px-6 py-2.5 bg-red-600 text-white font-medium text-xs leading-tight uppercase rounded hover:bg-red-700 focus:bg-red-700 focus:outline-none focus:ring-0 active:bg-red-800 transition duration-150 ease-in-out",
light: "inline-block px-6 py-2.5 bg-gray-200 text-gray-700 font-medium text-xs leading-tight uppercase rounded hover:bg-gray-300 focus:bg-gray-300 focus:outline-none focus:ring-0 active:bg-gray-400 transition duration-150 ease-in-out",
dark: "inline-block px-6 py-2.5 bg-neutral-800 text-white font-medium text-xs leading-tight uppercase rounded hover:bg-neutral-900 focus:bg-neutral-900 focus:outline-none focus:ring-0 active:bg-neutral-900 transition duration-150 ease-in-out",
"brighter-dark": "inline-block px-6 py-2.5 bg-neutral-700 text-white font-medium text-xs leading-tight uppercase rounded hover:bg-neutral-900 focus:bg-neutral-900 focus:outline-none focus:ring-0 active:bg-neutral-900 transition duration-150 ease-in-out",
}
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
text: string
color?: string
onClickAsync?: () => Promise<void>
spinner?: boolean
}
export default function Button({text, color, className, onClickAsync, spinner, ...props}: ButtonProps) {
const colorKey = color ? color : "primary"
const buttonStyle = buttons[colorKey] ? buttons[colorKey] : buttons.primary
const [showSpinner, SetShowSnipper] = useState(false)
let onClick = props.onClick
if (onClickAsync) {
onClick = async () => {
SetShowSnipper(true)
try {
await onClickAsync()
} catch (e) {console.error(e)}
SetShowSnipper(false)
}
}
const shouldShowSpinner = spinner || showSpinner
return (
<button {...props} onClick={onClick} className={`${buttonStyle} w-[fit-content] font-bold flex flex-row items-center gap-4 ${className}`} disabled={shouldShowSpinner}>{text} {shouldShowSpinner && <Spinner/>}</button>
)
}

View file

@ -0,0 +1,26 @@
import {HTMLAttributes, ReactElement} from "react";
interface CardProps extends HTMLAttributes<HTMLElement> {
header?: string | ReactElement<any, any>
hideSeparator?: boolean
className?: string
onClick?: () => void
}
export default function Card({header, hideSeparator, children, onClick, className}: CardProps) {
return (
<div
className={`bg-neutral-600 rounded-md text-left p-4 ${className}`}
onClick={() => {onClick?.()}}
>
{header && <>
<div className="text-xl bg-neutral-600">{header}</div>
{!hideSeparator && <div className="border-b border-b-neutral-500 my-4"/>}
</>}
<div>
{children}
</div>
</div>
)
}

View file

@ -0,0 +1,98 @@
import moment from "moment";
import { useState } from "react";
import {Bar, ComposedChart} from "recharts";
import {Line, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts";
import {formatTimestamp} from "../utils/formatTimestamp";
import Select from "./Select";
const dayDuration = 1000 * 60 * 60 * 24 // Duration of a day in milliseconds
function lastDays(data: {value: number | null, date: number}[], alwaysUseLastValue: boolean, daysCount: number = 30) {
data.sort((a, b) => b.date - a.date)
const processedData: {date: string, value: number | null}[] = []
const startingPoint = moment(Date.now()).endOf("day").valueOf()
for (let currentDay = 0; currentDay < daysCount; ++currentDay) {
const beforeThreshold = startingPoint - (currentDay * dayDuration)
let last = data.find(x => x.date < beforeThreshold)
if (!alwaysUseLastValue && last && last?.date < beforeThreshold - (dayDuration)) {
last = undefined
}
processedData.push({
date: formatTimestamp(beforeThreshold),
value: last && last.value !== null ? last.value : null
})
}
return processedData.reverse()
}
type ChartProps = {
charts: {
name: string
type: string
color: string
alwaysUseLastValue: boolean
data: {
value: number
date: number
}[]
}[]
}
export default function Chart({charts}: ChartProps) {
const [daysRange, setDaysRange] = useState<number>(30)
const processedData = charts.map(d => ({
name: d.name,
type: d.type,
color: d.color,
data: lastDays(d.data, d.alwaysUseLastValue, daysRange)
}))
const chartData: {
values: number[]
date: string
}[] = []
for (let i = 0; i < processedData.length; ++i) {
for (let j = 0; j < daysRange; ++j) {
if (i === 0) {
chartData.push({date: processedData[i].data[j].date, values: [processedData[i].data[j].value || 0]})
continue;
}
chartData[j].values.push(processedData[i].data[j].value || 0)
}
}
return (
<>
<div className="mb-2">
<div className="f-r items-center gap-2">
<span>Show:</span>
<Select onChange={(event) => setDaysRange(parseInt(event.target.value))}>
<option value={7}>last 7 days</option>
<option value={15}>last 15 days</option>
<option selected value={30}>last 30 days</option>
<option value={60}>last 60 days</option>
<option value={90}>last 90 days</option>
<option value={360}>last 365 days</option>
</Select>
</div>
</div>
<div className="border-b border-b-neutral-500 my-4"/>
<ResponsiveContainer width="99%" height={180}>
<ComposedChart data={chartData}>
{chartData.map((_, index) => {
if (charts[index]?.type === "line")
return <Line type="monotone" dataKey={`values[${index}]`} dot={false} name={charts[index]?.name} stroke={charts[index].color} />
if (charts[index]?.type === "bar")
return <Bar fill={charts[index]?.color} dataKey={`values[${index}]`} name={charts[index]?.name} stroke={charts[index].color} />
})}
<XAxis dataKey="date" stroke="#AAAAAA"/>
<YAxis stroke="#AAAAAA"/>
<Tooltip contentStyle={{backgroundColor: "#444444"}}/>
</ComposedChart>
</ResponsiveContainer>
</>
)
}

View file

@ -0,0 +1,44 @@
import {useState} from "react";
import {Link, useNavigate} from "react-router-dom";
import {useApi} from "../api";
import AppName from "./AppName";
import {MenuList, MenuListItem} from "./MenuList";
export default function HeaderBar() {
const api = useApi();
const navigate = useNavigate()
const [showMenu, setShowMenu] = useState(false)
if (!api.IsLogged()) {
return null;
}
return (
<div className="bg-neutral-800 py-4 f-r justify-between items-center gap-2 px-0 xl:px-96">
<div className="f-r flex-1 justify-start">
<Link to={api.IsLogged() ? "/projects" : "/"}>
<AppName/>
</Link>
</div>
<div className="flex-1 f-r items-center justify-end">
{
api.GetUserData() ?
<>
<div className="p-1 px-2 border border-neutral-500 rounded-md relative">
<div className="cursor-pointer" onClick={() => { setShowMenu(!showMenu) }}>
{api.GetUserData().email}
</div>
<MenuList show={showMenu}>
<MenuListItem text="Logout" onClick={async () => {
await api.Logout();
navigate("/")
}}/>
</MenuList>
</div>
</>
: ""
}
</div>
</div>
)
}

View file

@ -0,0 +1,49 @@
import {InputHTMLAttributes, TextareaHTMLAttributes} from "react"
import {UseFormRegisterReturn} from "react-hook-form"
interface LineEditProps {
state?: string,
stateHandler?: (value: string) => void,
register?: UseFormRegisterReturn,
label?: string,
labelError?: string,
isValid?: (value: string) => boolean
value?: string | number
onChange?: (value: string) => void
required?: boolean
type?: string
minLength?: number
multiline?: boolean
className?: string
inputClassName?: string
rows?: number
cols?: number
}
export default function LineEdit({state, stateHandler, label, labelError, isValid, register, value, ...props}: LineEditProps) {
const inputProps: InputHTMLAttributes<HTMLInputElement> & TextareaHTMLAttributes<HTMLTextAreaElement> = {
className: `bg-neutral-700 text-white rounded-md p-2 flex-1 min-w-0 ${props.inputClassName}`,
onChange: ((e: any) => props.onChange && props.onChange(e.target.value)),
required: props.required,
type: props.type,
minLength: props.minLength,
rows: props.rows,
cols: props.cols
}
if (!register) {
inputProps.value = value
}
return (
<div className={`text-left min-w-0 ${props.className}`}>
{label && <div className="text-sm mb-1 ml-1">{label}</div>}
<div className="f-r">
{!props.multiline && <input {...inputProps} {...register}/>}
{props.multiline && <textarea {...inputProps} {...register}/>}
</div>
{labelError && <div className="text-red-300">{labelError}</div>}
</div>
)
}

View file

@ -0,0 +1,69 @@
import {TrashIcon} from "@heroicons/react/outline";
import moment from "moment";
import {useState} from "react";
import {useApi} from "../api";
import {Log} from "../core/Log";
import {Project} from "../core/Project";
import {TagSticker} from "./TagSticker";
import {useModal} from "../services/ModalService";
import {formatTimestamp} from "../utils/formatTimestamp";
export type LogCardProps = {
project: Project
log: Log
onDelete?: () => void
}
export function LogCard({log, project, onDelete}: LogCardProps) {
const [showContent, setShowContent] = useState(false);
const modalService = useModal()
const api = useApi()
const deleteLog = () => {
modalService.confirmation("Delete the log ?", async () => {
await api.deleteLog(log.projectId, log.id)
onDelete && onDelete();
})
}
return (
<div className="card p-4 cursor-pointer" onClick={() => {setShowContent(true)}}>
<div className="f-c gap-2" >
<div className="f-r gap-4 items-center">
<span className="text-xl">{log.title}</span>
<div className="f-r gap-2">
{log.tags.map(tag => <TagSticker tag={tag}/>)}
</div>
<span className="text-neutral-400 text-sm ml-auto">
{formatTimestamp(log.createdAt)}
</span>
<TrashIcon
className="w-4 h-4 text-red-300 cursor-pointer"
onClick={deleteLog}
/>
</div>
</div>
{showContent && <p className="text-sm text-left pt-4">{log.content}</p>}
</div>
)
}
export function TinyLogCard({log, project}: LogCardProps) {
let borderStyle = {}
if (log.tags.length > 0) {
borderStyle = {borderLeftColor: `${log.tags[0].color}`}
}
return (
<div className="f-c gap-2">
<div className="f-r items-stretch">
<div className="border-l-2 border-neutral-500 pr-2" style={borderStyle}/>
<p>{log.title}</p>
<p className='mt-auto ml-auto text-xs text-neutral-400'>{moment(log.createdAt).format("YYYY-MM-DD hh:mm a")}</p>
</div>
<div className="w-full border-b border-neutral-500"/>
</div>
)
}

View file

@ -0,0 +1,25 @@
import {HTMLAttributes} from "react";
export function MenuListItem(props: (HTMLAttributes<HTMLDivElement> & {text: string})) {
return (
<div
{...props}
className={`text-left p-4 hover:bg-neutral-700 cursor-pointer rounded-md ${props.className}`}
>
{props.text}
</div>
)
}
export function MenuList({show, ...props}: (HTMLAttributes<HTMLDivElement> & {show: boolean})) {
return (
<div
{...props}
className={`bg-neutral-900 absolute top-0 left-0 translate-y-[-110%] right-0 w-full rounded-md ${show ? "block" : "hidden"} ${props.className}`}
>
{props.children}
</div>
)
}

View file

@ -0,0 +1,42 @@
import {useEffect, useState} from "react"
interface ModalProps {
show: boolean
children?: JSX.Element | null
onClose: () => void
customId?: string
ignoreInputs?: boolean
}
export default function Modal({show, onClose, children, customId, ignoreInputs}: ModalProps) {
const [createdAt] = useState(Date.now())
useEffect(() => {
if (!show)
return;
function handleClose(e: any) {
if (!ignoreInputs && !document.getElementById(customId || 'modal-content')?.contains(e.target) && Date.now() - createdAt > 1000) {
onClose()
}
}
document.addEventListener('click', handleClose)
document.body.style.overflow = "hidden"
return () => {
document.removeEventListener('click', handleClose)
document.body.style.overflow = "visible"
}
}, [show, onClose, customId, createdAt])
if (!show) {
return <></>
}
return (
<div className="fixed top-0 left-0 w-screen h-screen bg-black bg-opacity-50 flex justify-center items-center overflow-scroll">
<div id={customId || 'modal-content'} className="bg-neutral-600 p-2 rounded-md max-h-[90%] overflow-scroll">
{children}
</div>
</div>
)
}

View file

@ -0,0 +1,30 @@
import { ClipboardCopyIcon } from "@heroicons/react/outline";
import { Project } from "../core/Project";
import { useModal } from "../services/ModalService";
import { copyToClipboard } from "../utils/copyToClipboard";
type ProjectHeaderProps = {
project: Project
middle?: JSX.Element
right?: JSX.Element
}
export default function ProjectHeader({project, middle, right}: ProjectHeaderProps) {
const modalService = useModal();
return (
<div className="f-r pb-4 border-b border-b-neutral-500">
<div className="flex-1 f-r items-center gap-2 text-left text-2xl">
<h2 className="text-3xl">{project.name}</h2>
<ClipboardCopyIcon
className="w-4 h-4 cursor-pointer"
onClick={() => {
copyToClipboard(project.id)
modalService.info("Project id copied to clipboard")
}}
/>
</div>
<div className="flex-1 f-c justify-center">{middle}</div>
<div className="flex-1 f-c justify-center">{right}</div>
</div>
)
}

View file

@ -0,0 +1,53 @@
import {useCallback, useEffect, useState} from "react";
import {Route, Routes, useParams} from "react-router-dom";
import {useApi} from "../api";
import {Project} from "../core/Project";
import { MetricHistoryPage } from "../pages/MetricHistory";
import {ProjectApiKeysPage} from "../pages/ProjectApiKeys";
import ProjectLogsPage from "../pages/ProjectLogs";
import {ProjectMetricsPage} from "../pages/ProjectMetrics";
import ProjectPage from "../pages/projectPage";
import {ProjectSideBar} from "./ProjectSideBar";
export default function ProjectPageBase() {
const api = useApi();
const {projectId} = useParams();
const [project, setProject] = useState<Project | null>(null);
const fetchProject = useCallback(async () => {
const project = await api.fetchProject(projectId as string);
setProject(project);
}, [projectId, api])
useEffect(() => {fetchProject()}, [fetchProject])
if (!project) {
return <div>Loading project...</div>
}
return (
<div className="flex-1 f-r h-full overflow-hidden">
<ProjectSideBar project={project}/>
<div className="flex-1 f-c gap-4 p-4 overflow-hidden">
<Routes>
<Route path="/" element={
<ProjectPage/>
}/>
<Route path="/logs" element={
<ProjectLogsPage/>
}/>
<Route path="/metrics" element={
<ProjectMetricsPage/>
}/>
<Route path="/metrics/:metricId/history" element={
<MetricHistoryPage/>
}/>
<Route path="/api-keys" element={
<ProjectApiKeysPage/>
}/>
</Routes>
</div>
</div>
)
}

View file

@ -0,0 +1,84 @@
import {useState} from "react";
import {Link, useNavigate} from "react-router-dom";
import {useApi} from "../api";
import AppName from "./AppName";
import {MenuList, MenuListItem} from "./MenuList";
import {BookOpenIcon, ChartBarIcon, CollectionIcon, KeyIcon, ViewGridIcon} from "@heroicons/react/outline"
import {Project} from "../core/Project";
const topSide = [
{
name: "Dashboard",
icon: ViewGridIcon,
route: "/"
},
{
name: "Logs",
icon: CollectionIcon,
route: "/logs"
},
{
name: "Metrics",
icon: ChartBarIcon,
route: "/metrics"
},
{
name: "Api keys",
icon: KeyIcon,
route: "/api-keys"
}
]
export type ProjectSideBarProps = {
project?: Project
}
export function ProjectSideBar({project}: ProjectSideBarProps) {
const api = useApi();
const navigate = useNavigate()
const [showMenu, setShowMenu] = useState(false)
if (!api.IsLogged()) {
return null;
}
return (
<div className="bg-neutral-800 p-4 flex flex-col justify-between items-center gap-2">
<div className="f-c flex-1 justify-start">
<Link to={api.IsLogged() ? "/projects" : "/"} className="mb-8">
<AppName/>
</Link>
{project && topSide.map(elem => (
<Link key={elem.route} to={`/projects/${project.id}${elem.route}`} className="f-r items-center gap-4 p-4 hover:bg-neutral-600 rounded-md duration-200">
<elem.icon className="w-8 h-8"/>
<div className="text-xl">{elem.name}</div>
</Link>)
)}
</div>
<div className="flex-1 f-c gap-4 items-center justify-end">
<a href={process.env.REACT_APP_DOCS_URL} target="_blank" className="f-r items-center gap-4 p-4 hover:bg-neutral-600 rounded-md duration-200">
<BookOpenIcon className="w-8 h-8"/>
<div className="text-xl">Docs</div>
</a>
{
api.GetUserData() ?
<>
<div className="p-1 px-2 border border-neutral-500 rounded-md relative">
<div className="cursor-pointer" onClick={() => { setShowMenu(!showMenu) }}>
{api.GetUserData().email}
</div>
<MenuList show={showMenu}>
{/*<MenuListItem text="Account settings" onClick={() => { console.log("Account settings") }}/>*/}
<MenuListItem text="Logout" onClick={async () => {
await api.Logout();
navigate("/")
}}/>
</MenuList>
</div>
</>
: ""
}
</div>
</div>
)
}

View file

@ -0,0 +1,20 @@
import {useState} from "react"
import {Navigate} from "react-router-dom"
import {useApi} from "../api"
import {useModal} from "../services/ModalService"
export function RequireAuth ({children}: {children: JSX.Element}) {
const api = useApi()
const modalService = useModal()
const [hasError, setHasError] = useState(false)
if (!api.IsLogged()) {
return <Navigate to="/"/>
}
if (!api.IsLoadingUser() && !api.GetUserData() && !hasError) {
setHasError(true)
modalService.error("Could not retrieve user data")
}
return children
}

View file

@ -0,0 +1,32 @@
import {SelectHTMLAttributes} from "react"
import {UseFormRegisterReturn} from "react-hook-form"
interface LineEditProps extends SelectHTMLAttributes<HTMLSelectElement> {
register?: UseFormRegisterReturn,
label?: string,
labelError?: string,
isValid?: (value: string) => boolean
children: JSX.Element | JSX.Element[]
}
export default function Select({children, label, labelError, isValid, register, ...props}: LineEditProps) {
let labelDiv = null
if (label) {
labelDiv = <div className="text-sm mb-1 ml-1">{label}</div>
}
let labelErrorDiv = null
if (labelError) {
labelErrorDiv = <div className="text-red-300">{labelError}</div>
}
return (
<div className="text-left">
{labelDiv}
<select {...props} {...register} className="bg-neutral-700 text-white rounded-md p-2">
{children}
</select>
{labelErrorDiv}
</div>
)
}

View file

@ -0,0 +1,60 @@
import {useState} from "react";
import {Link, useNavigate} from "react-router-dom";
import {useApi} from "../api";
import AppName from "./AppName";
import {MenuList, MenuListItem} from "./MenuList";
import {CollectionIcon} from "@heroicons/react/outline"
const topSide = [
{
name: "Projects",
icon: CollectionIcon,
route: "/projects"
}
]
export default function SideBar() {
const api = useApi();
const navigate = useNavigate()
const [showMenu, setShowMenu] = useState(false)
if (!api.IsLogged()) {
return null;
}
return (
<div className="bg-neutral-800 p-4 flex flex-col justify-between items-center gap-2">
<div className="f-c flex-1 justify-start">
<Link to={api.IsLogged() ? "/projects" : "/"} className="mb-8">
<AppName/>
</Link>
{topSide.map(elem => (
<Link key={elem.route} to={elem.route} className="f-r items-center gap-4 p-4 hover:bg-neutral-600 rounded-md duration-200">
<elem.icon className="w-8 h-8"/>
<div className="text-xl">{elem.name}</div>
</Link>)
)}
</div>
<div className="flex-1 flex flex-col items-center justify-end">
{
api.GetUserData() ?
<>
<div className="p-1 px-2 border border-neutral-500 rounded-md relative">
<div className="cursor-pointer" onClick={() => { setShowMenu(!showMenu) }}>
{api.GetUserData().email}
</div>
<MenuList show={showMenu}>
{/*<MenuListItem text="Account settings" onClick={() => { console.log("Account settings") }}/>*/}
<MenuListItem text="Logout" onClick={async () => {
await api.Logout();
navigate("/")
}}/>
</MenuList>
</div>
</>
: ""
}
</div>
</div>
)
}

View file

@ -0,0 +1,35 @@
import {useState} from "react"
interface SideMenuProps {
items: {
label: string
}[]
onChange: (index: number) => void
}
export function SideMenu({items, onChange}: SideMenuProps) {
const [selectedIndex, setSelectedIndex] = useState(0)
return (
<div>
<div className="f-c p-2 gap-2 bg-neutral-600 rounded-md text-left cursor-pointer">
{items.map((item, index) => {
return (
<div
key={index}
className={index === selectedIndex
? "bg-neutral-500 p-1 px-2 rounded-md whitespace-nowrap"
: "p-1 px-2 hover:bg-neutral-500 duration-100 rounded-md whitespace-nowrap"}
onClick={() => {
setSelectedIndex(index)
onChange(index)
}}>{item.label}
</div>
)
})}
</div>
</div>
)
}

View file

@ -0,0 +1,13 @@
export interface SpinnerProps {
size?: number
}
export default function Spinner({size = 3}: SpinnerProps) {
return (
<div className="flex justify-center items-center">
<div className="spinner-border animate-spin inline-block border-2 rounded-full" role="status" style={{width: size, height: size}}>
<span className="visually-hidden">Loading...</span>
</div>
</div>
)
}

View file

@ -0,0 +1,37 @@
import {useState} from "react"
interface TabsProps {
tabs: {
name: string,
renderer: JSX.Element
}[]
}
export default function Tabs({tabs}: TabsProps) {
const [selectedTab, SetSelectedTab] = useState(0)
const selectedButtonClassName = "border-b border-b-app-primary text-awary-primary box-border p-4 cursor-pointer"
const unselectedButtonClassName = "border-b border-neutral-600 box-border p-4 cursor-pointer"
const bar = tabs.map((tab, index) =>
<div
key={index}
className={index === selectedTab ? selectedButtonClassName : unselectedButtonClassName}
onClick={() => {SetSelectedTab(index)}}
>
{tab.name}
</div>
)
return (
<>
<div className="flex flex-row border-b border-b-neutral-500">
{bar}
</div>
<div>
{tabs[selectedTab].renderer}
</div>
</>
)
}

View file

@ -0,0 +1,12 @@
import { TagData } from "../core/Tag"
export function TagSticker({tag}: {tag: TagData}) {
return (
<div
className="border border-neutral-400 rounded-md text-xs py-1 px-4"
style={{color: tag.color, borderColor: tag.color}}
>
{tag.name}
</div>
)
}

View file

@ -0,0 +1,45 @@
import {UseFormRegisterReturn} from "react-hook-form"
interface URLEditProps {
state?: string,
stateHandler?: (value: string) => void,
registerMethod?: UseFormRegisterReturn,
registerUrl?: UseFormRegisterReturn,
label?: string,
labelError?: string,
isValid?: (value: string) => boolean
value?: string
onChange?: (value: string) => void
required?: boolean
type?: string
minLength?: number
className?: string
inputClassName?: string
}
export default function URLEdit({state, stateHandler, label, labelError, isValid, registerMethod, registerUrl, value, ...props}: URLEditProps) {
const inputProps = {
className: `bg-neutral-700 text-white rounded-r-md p-2 flex-1 min-w-0 ${props.inputClassName}`,
onChange: ((e: any) => props.onChange && props.onChange(e.target.value)),
required: props.required,
type: props.type,
minLength: props.minLength,
}
return (
<div className={`text-left min-w-0 ${props.className}`}>
{label && <div className="text-sm mb-1 ml-1">{label}</div>}
<div className="f-r">
<select {...registerMethod} className="bg-neutral-700 text-white rounded-l-md p-2 border-r border-r-neutral-500">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
</select>
<input {...inputProps} {...registerUrl}/>
</div>
{labelError && <div className="text-red-300">{labelError}</div>}
</div>
)
}