feat: reload go2rtc config

This commit is contained in:
ZhaiSoul 2025-08-29 09:05:55 +00:00
parent 78e431d5e3
commit 85741c02e4
8 changed files with 577 additions and 84 deletions

View File

@ -32,6 +32,7 @@ from frigate.config import FrigateConfig
from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateTopic,
reload_go2rtc,
)
from frigate.models import Event, Timeline
from frigate.stats.prometheus import get_metrics, update_metrics
@ -424,11 +425,34 @@ def config_set(request: Request, body: AppConfigSetBody):
if field == "add":
settings = config.cameras[camera]
if camera in old_config.cameras:
request.app.config_publisher.publish_update(
CameraConfigUpdateTopic(CameraConfigUpdateEnum.edit, camera),
settings,
)
continue_publish = False
else:
continue_publish = True
elif field == "edit":
settings = config.cameras[camera]
if camera in old_config.cameras:
logger.info(f"Publishing edit event for camera {camera}")
continue_publish = True
else:
request.app.config_publisher.publish_update(
CameraConfigUpdateTopic(CameraConfigUpdateEnum.add, camera),
settings,
)
continue_publish = False
elif field == "remove":
settings = old_config.cameras[camera]
continue_publish = True
else:
settings = config.get_nested_object(body.update_topic)
continue_publish = True
if continue_publish:
logger.info(f"Publishing {field} event for camera {camera}")
request.app.config_publisher.publish_update(
CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera),
settings,
@ -530,6 +554,23 @@ def nvinfo():
return JSONResponse(content=get_nvidia_driver_info())
@router.post("/go2rtc/reload", dependencies=[Depends(require_role(["admin"]))])
def reload_go2rtc_endpoint():
"""reload go2rtc service"""
try:
reload_go2rtc()
return JSONResponse(
content={"success": True, "message": "go2rtc reloaded"},
status_code=200,
)
except Exception as e:
logger.error(f"reload go2rtc failed: {e}")
return JSONResponse(
content={"success": False, "message": f"{str(e)}"},
status_code=500,
)
@router.get("/logs/{service}", tags=[Tags.logs])
async def logs(
service: str = Path(enum=["frigate", "nginx", "go2rtc"]),

View File

@ -50,6 +50,7 @@ class CameraMaintainer(threading.Thread):
[
CameraConfigUpdateEnum.add,
CameraConfigUpdateEnum.remove,
CameraConfigUpdateEnum.edit,
],
)
self.shm_count = self.__calculate_shm_frame_count()
@ -194,6 +195,21 @@ class CameraMaintainer(threading.Thread):
for update_type, updated_cameras in updates.items():
if update_type == CameraConfigUpdateEnum.add.name:
for camera in updated_cameras:
self.__start_camera_processor(
camera,
self.update_subscriber.camera_configs[camera],
runtime=True,
)
self.__start_camera_capture(
camera,
self.update_subscriber.camera_configs[camera],
runtime=True,
)
elif update_type == CameraConfigUpdateEnum.edit.name:
for camera in updated_cameras:
self.__stop_camera_capture_process(camera)
self.__stop_camera_process(camera)
self.__start_camera_processor(
camera,
self.update_subscriber.camera_configs[camera],
@ -205,6 +221,7 @@ class CameraMaintainer(threading.Thread):
runtime=True,
)
elif update_type == CameraConfigUpdateEnum.remove.name:
for camera in updated_cameras:
self.__stop_camera_capture_process(camera)
self.__stop_camera_process(camera)

View File

@ -1,11 +1,20 @@
"""Convenience classes for updating configurations dynamically."""
import logging
import os
import subprocess
from dataclasses import dataclass
from enum import Enum
from typing import Any
from typing import Any, Dict
import requests
from frigate.comms.config_updater import ConfigPublisher, ConfigSubscriber
from frigate.config import CameraConfig, FrigateConfig
from frigate.util.builtin import update_yaml_file_bulk
from frigate.util.config import find_config_file
logger = logging.getLogger(__name__)
class CameraConfigUpdateEnum(str, Enum):
@ -16,6 +25,7 @@ class CameraConfigUpdateEnum(str, Enum):
audio_transcription = "audio_transcription"
birdseye = "birdseye"
detect = "detect"
edit = "edit" # for editing an existing camera
enabled = "enabled"
motion = "motion" # includes motion and motion masks
notifications = "notifications"
@ -76,37 +86,61 @@ class CameraConfigUpdateSubscriber:
self, camera: str, update_type: CameraConfigUpdateEnum, updated_config: Any
) -> None:
if update_type == CameraConfigUpdateEnum.add:
"""Add a new camera and handle go2rtc streams if needed"""
self.config.cameras[camera] = updated_config
self.camera_configs[camera] = updated_config
# Handle potential go2rtc streams for new camera
if hasattr(updated_config, "ffmpeg") and hasattr(
updated_config.ffmpeg, "inputs"
):
streams = self._extract_stream_names(updated_config)
if (
streams
and hasattr(self.config, "go2rtc")
and hasattr(self.config.go2rtc, "streams")
):
go2rtc_updated = False
for stream_name in streams:
logger.info(
f"New go2rtc stream detected for camera {camera}: {stream_name}"
)
go2rtc_updated = True
if go2rtc_updated:
self.reload_go2rtc()
return
elif update_type == CameraConfigUpdateEnum.edit:
"""Update camera configuration without stopping processes"""
old_config = self.camera_configs.get(camera)
self._handle_go2rtc_stream_updates(camera, old_config, updated_config)
self.config.cameras[camera] = updated_config
self.camera_configs[camera] = updated_config
return
elif update_type == CameraConfigUpdateEnum.remove:
"""Remove go2rtc streams with camera"""
camera_config = self.camera_configs.get(camera)
if (
camera_config
and hasattr(self.config, "go2rtc")
and hasattr(camera_config, "ffmpeg")
and hasattr(camera_config.ffmpeg, "inputs")
):
for input_item in camera_config.ffmpeg.inputs:
if hasattr(input_item, "path") and isinstance(input_item.path, str):
if (
"rtsp://" in input_item.path
and "127.0.0.1:8554/" in input_item.path
):
stream_name = (
input_item.path.split("127.0.0.1:8554/")[-1]
.split("&")[0]
.split("?")[0]
)
if (
hasattr(self.config.go2rtc, "streams")
and stream_name in self.config.go2rtc.streams
):
self.config.go2rtc.streams.pop(stream_name)
go2rtc_updated = False
# Use helper methods to handle go2rtc streams
if camera_config:
streams = self._extract_stream_names(camera_config)
for stream_name in streams:
if not self._is_stream_in_use(stream_name, camera):
if self._remove_unused_stream(stream_name):
go2rtc_updated = True
if go2rtc_updated:
self.reload_go2rtc()
self.camera_configs.pop(camera, None)
if camera in self.config.cameras:
self.config.cameras.pop(camera)
self.camera_configs.pop(camera)
return
config = self.camera_configs.get(camera)
@ -170,3 +204,138 @@ class CameraConfigUpdateSubscriber:
def stop(self) -> None:
self.subscriber.stop()
def _extract_stream_names(self, config):
"""Extract go2rtc stream names from configuration"""
streams = []
if not (hasattr(config, "ffmpeg") and hasattr(config.ffmpeg, "inputs")):
return streams
for input_item in config.ffmpeg.inputs:
if not (hasattr(input_item, "path") and isinstance(input_item.path, str)):
continue
path = input_item.path
if "rtsp://" in path and "127.0.0.1:8554/" in path:
stream_name = (
path.split("127.0.0.1:8554/")[-1].split("&")[0].split("?")[0]
)
streams.append(stream_name)
return streams
def _is_stream_in_use(self, stream_name, current_camera):
"""Check if stream is used by other cameras"""
for cam_name, cam_config in self.camera_configs.items():
if cam_name == current_camera:
continue
if not (
hasattr(cam_config, "ffmpeg") and hasattr(cam_config.ffmpeg, "inputs")
):
continue
for input_item in cam_config.ffmpeg.inputs:
if (
hasattr(input_item, "path")
and isinstance(input_item.path, str)
and "127.0.0.1:8554/" + stream_name in input_item.path
):
return True
return False
def _remove_unused_stream(self, stream_name):
"""Remove unused go2rtc stream"""
if not (
hasattr(self.config, "go2rtc")
and hasattr(self.config.go2rtc, "streams")
and stream_name in self.config.go2rtc.streams
):
return False
logger.info(f"Removing unused go2rtc stream: {stream_name}")
self.config.go2rtc.streams.pop(stream_name)
config_file = find_config_file()
updates: Dict[str, Any] = {f"go2rtc.streams.{stream_name}": ""}
update_yaml_file_bulk(config_file, updates)
return True
def _handle_go2rtc_stream_updates(self, camera, old_config, updated_config):
"""Handle go2rtc stream configuration updates"""
if not (
old_config
and hasattr(self.config, "go2rtc")
and hasattr(self.config.go2rtc, "streams")
):
return
old_streams = self._extract_stream_names(old_config)
new_streams = self._extract_stream_names(updated_config)
if not (old_streams or new_streams):
return
removed_streams = [s for s in old_streams if s not in new_streams]
added_streams = [s for s in new_streams if s not in old_streams]
common_streams = [s for s in old_streams if s in new_streams]
go2rtc_updated = False
for stream_name in added_streams:
logger.info(f"New go2rtc stream detected: {stream_name}")
go2rtc_updated = True
for stream_name in removed_streams:
if not self._is_stream_in_use(stream_name, camera):
if self._remove_unused_stream(stream_name):
go2rtc_updated = True
for stream_name in common_streams:
if stream_name in self.config.go2rtc.streams:
for input_item in (
updated_config.ffmpeg.inputs
if (
hasattr(updated_config, "ffmpeg")
and hasattr(updated_config.ffmpeg, "inputs")
)
else []
):
if (
hasattr(input_item, "path")
and isinstance(input_item.path, str)
and "127.0.0.1:8554/" + stream_name in input_item.path
):
logger.info(
f"Checking for changes in go2rtc stream: {stream_name}"
)
go2rtc_updated = True
break
if go2rtc_updated:
self.reload_go2rtc()
def reload_go2rtc(self):
"""Regenerate go2rtc config and restart the service."""
try:
create_config_path = "/usr/local/go2rtc/create_config.py"
if os.path.exists(create_config_path):
subprocess.run(["python3", create_config_path], check=True)
logger.info("Successfully regenerated go2rtc config")
else:
logger.warning(f"Could not find {create_config_path}")
# Restart go2rtc service
try:
response = requests.post("http://127.0.0.1:1984/api/restart", timeout=5)
if response.status_code == 200:
logger.info("Successfully restarted go2rtc service")
else:
logger.warning(
f"Failed to restart go2rtc service: {response.status_code}"
)
except requests.RequestException as e:
logger.error(f"Error restarting go2rtc service: {e}")
except Exception as e:
logger.error(f"Error reloading go2rtc: {e}")

View File

@ -102,6 +102,7 @@ class EmbeddingMaintainer(threading.Thread):
[
CameraConfigUpdateEnum.add,
CameraConfigUpdateEnum.remove,
CameraConfigUpdateEnum.edit,
CameraConfigUpdateEnum.object_genai,
CameraConfigUpdateEnum.review_genai,
CameraConfigUpdateEnum.semantic_search,

View File

@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Creates a go2rtc config file with proper permissions."""
import json
import os
import sys
import shutil
from pathlib import Path
from typing import Any
try:
from ruamel.yaml import YAML
except ImportError:
print("Installing required packages...")
import subprocess
subprocess.check_call([sys.executable, "-m", "pip", "install", "ruamel.yaml"])
from ruamel.yaml import YAML
# Add frigate to path
sys.path.insert(0, "/opt/frigate")
try:
from frigate.const import (
BIRDSEYE_PIPE,
DEFAULT_FFMPEG_VERSION,
INCLUDED_FFMPEG_VERSIONS,
LIBAVFORMAT_VERSION_MAJOR,
)
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
from frigate.util.config import find_config_file
except ImportError:
# Fallback if imports fail
print("Warning: Could not import from frigate module. Using fallback values.")
BIRDSEYE_PIPE = "/tmp/birdseye"
DEFAULT_FFMPEG_VERSION = "v5.1.2"
INCLUDED_FFMPEG_VERSIONS = ["v5.1.2", "v4.4"]
LIBAVFORMAT_VERSION_MAJOR = 59
def parse_preset_hardware_acceleration_encode(ffmpeg_path, hwaccel_args, input_args, output_args):
return f"{ffmpeg_path} {hwaccel_args} {input_args} {output_args}"
def find_config_file():
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
if os.path.exists(config_file):
return config_file
# Try common locations
for ext in [".yml", ".yaml", ".json"]:
for path in ["/config/config", "/etc/frigate/config"]:
if os.path.exists(f"{path}{ext}"):
return f"{path}{ext}"
return "/config/config.yml"
# Remove frigate from path
if "/opt/frigate" in sys.path:
sys.path.remove("/opt/frigate")
yaml = YAML()
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
# read docker secret files as env vars too
if os.path.isdir("/run/secrets"):
for secret_file in os.listdir("/run/secrets"):
if secret_file.startswith("FRIGATE_"):
FRIGATE_ENV_VARS[secret_file] = (
Path(os.path.join("/run/secrets", secret_file)).read_text().strip()
)
config_file = find_config_file()
print(f"Using config file: {config_file}")
try:
with open(config_file) as f:
raw_config = f.read()
if config_file.endswith((".yaml", ".yml")):
config: dict[str, Any] = yaml.load(raw_config)
elif config_file.endswith(".json"):
config: dict[str, Any] = json.loads(raw_config)
except FileNotFoundError:
print(f"Config file not found: {config_file}")
config: dict[str, Any] = {}
go2rtc_config: dict[str, Any] = config.get("go2rtc", {})
# Need to enable CORS for go2rtc so the frigate integration / card work automatically
if go2rtc_config.get("api") is None:
go2rtc_config["api"] = {"origin": "*"}
elif go2rtc_config["api"].get("origin") is None:
go2rtc_config["api"]["origin"] = "*"
# Need to set default location for HA config
if go2rtc_config.get("hass") is None:
go2rtc_config["hass"] = {"config": "/homeassistant"}
# we want to ensure that logs are easy to read
if go2rtc_config.get("log") is None:
go2rtc_config["log"] = {"format": "text"}
elif go2rtc_config["log"].get("format") is None:
go2rtc_config["log"]["format"] = "text"
# ensure there is a default webrtc config
if go2rtc_config.get("webrtc") is None:
go2rtc_config["webrtc"] = {}
if go2rtc_config["webrtc"].get("candidates") is None:
default_candidates = []
# use internal candidate if it was discovered when running through the add-on
internal_candidate = os.environ.get("FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL")
if internal_candidate is not None:
default_candidates.append(internal_candidate)
# should set default stun server so webrtc can work
default_candidates.append("stun:8555")
go2rtc_config["webrtc"]["candidates"] = default_candidates
if go2rtc_config.get("rtsp", {}).get("username") is not None:
go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format(
**FRIGATE_ENV_VARS
)
if go2rtc_config.get("rtsp", {}).get("password") is not None:
go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format(
**FRIGATE_ENV_VARS
)
# ensure ffmpeg path is set correctly
path = config.get("ffmpeg", {}).get("path", "default")
if path == "default":
ffmpeg_path = f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg"
elif path in INCLUDED_FFMPEG_VERSIONS:
ffmpeg_path = f"/usr/lib/ffmpeg/{path}/bin/ffmpeg"
else:
ffmpeg_path = f"{path}/bin/ffmpeg"
if go2rtc_config.get("ffmpeg") is None:
go2rtc_config["ffmpeg"] = {"bin": ffmpeg_path}
elif go2rtc_config["ffmpeg"].get("bin") is None:
go2rtc_config["ffmpeg"]["bin"] = ffmpeg_path
# need to replace ffmpeg command when using ffmpeg4
if LIBAVFORMAT_VERSION_MAJOR < 59:
rtsp_args = "-fflags nobuffer -flags low_delay -stimeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}"
if go2rtc_config.get("ffmpeg") is None:
go2rtc_config["ffmpeg"] = {"rtsp": rtsp_args}
elif go2rtc_config["ffmpeg"].get("rtsp") is None:
go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args
for name in go2rtc_config.get("streams", {}):
stream = go2rtc_config["streams"][name]
if isinstance(stream, str):
try:
go2rtc_config["streams"][name] = go2rtc_config["streams"][name].format(
**FRIGATE_ENV_VARS
)
except KeyError as e:
print(
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
)
sys.exit(e)
elif isinstance(stream, list):
for i, stream in enumerate(stream):
try:
go2rtc_config["streams"][name][i] = stream.format(**FRIGATE_ENV_VARS)
except KeyError as e:
print(
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
)
sys.exit(e)
# add birdseye restream stream if enabled
if config.get("birdseye", {}).get("restream", False):
birdseye: dict[str, Any] = config.get("birdseye")
input = f"-f rawvideo -pix_fmt yuv420p -video_size {birdseye.get('width', 1280)}x{birdseye.get('height', 720)} -r 10 -i {BIRDSEYE_PIPE}"
ffmpeg_cmd = f"exec:{parse_preset_hardware_acceleration_encode(ffmpeg_path, config.get('ffmpeg', {}).get('hwaccel_args', ''), input, '-rtsp_transport tcp -f rtsp {output}')}"
if go2rtc_config.get("streams"):
go2rtc_config["streams"]["birdseye"] = ffmpeg_cmd
else:
go2rtc_config["streams"] = {"birdseye": ffmpeg_cmd}
# Write go2rtc_config to multiple possible locations with proper permissions
config_paths = [
"/dev/shm/go2rtc.yaml", # Primary location
"/tmp/go2rtc.yaml", # Fallback location 1
os.path.join(os.path.dirname(os.path.abspath(__file__)), "go2rtc.yaml") # Fallback location 2
]
success = False
for config_path in config_paths:
try:
# Try to create directory if it doesn't exist
os.makedirs(os.path.dirname(config_path), exist_ok=True)
# Write to a temporary file first
temp_path = f"{config_path}.tmp"
with open(temp_path, "w") as f:
yaml.dump(go2rtc_config, f)
# Make the file world-readable and writable
os.chmod(temp_path, 0o666)
# Move the temporary file to the final location
shutil.move(temp_path, config_path)
print(f"Successfully wrote go2rtc config to {config_path}")
success = True
break
except (PermissionError, OSError) as e:
print(f"Failed to write to {config_path}: {e}")
if not success:
print("ERROR: Could not write go2rtc config to any location")
sys.exit(1)

View File

@ -192,6 +192,7 @@
"edit": "Edit Camera",
"description": "Configure camera settings including stream inputs and roles.",
"name": "Camera Name",
"nameOnlyChangeToFriendlyName": "This will only change the Friendly Name.",
"nameRequired": "Camera name is required",
"nameLength": "Camera name must be less than 24 characters.",
"namePlaceholder": "e.g., front_door",
@ -200,7 +201,7 @@
"inputs": "Input Streams",
"go2rtc": {
"title": "Use go2rtc stream",
"description": "will use go2rtc",
"description": "It will utilize go2rtc, enabling a smoother live viewing experience and allowing you to hear the camera audio.",
"compatibility": {
"title": "Video codec compatibility (Advanced)",
"description": "Here you can configure common go2rtc FFmpeg encoding parameters. Some of these parameters may help resolve certain video or audio issues.",

View File

@ -250,7 +250,7 @@ export default function CameraEditForm({
if (cameraName && config?.cameras[cameraName]) {
const camera = config.cameras[cameraName];
defaultValues.enabled = camera.enabled ?? true;
defaultValues.ffmpeg.inputs = camera.ffmpeg?.inputs?.length
defaultValues.ffmpeg.inputs = camera.ffmpeg?.inputs
? camera.ffmpeg.inputs.map((input) => {
const isGo2rtcPath = input.path.match(
/^rtsp:\/\/127\.0\.0\.1:8554\/(.+)$/,
@ -263,10 +263,7 @@ export default function CameraEditForm({
if (go2rtcStreamName && config.go2rtc?.streams) {
Object.entries(config.go2rtc.streams).forEach(
([streamKey, streamConfig]) => {
if (
streamKey === go2rtcStreamName ||
streamKey.startsWith(`${cameraName}_`)
) {
if (streamKey === go2rtcStreamName) {
if (Array.isArray(streamConfig) && streamConfig.length >= 1) {
originalPath = streamConfig[0] || "";
ffmpegParams = streamConfig[1] || "";
@ -319,7 +316,9 @@ export default function CameraEditForm({
let friendly_name: string | undefined = undefined;
const isValidName = /^[a-zA-Z0-9_-]+$/.test(values.cameraName);
if (!isValidName) {
finalCameraName = generateFixedHash(finalCameraName);
finalCameraName = cameraName
? cameraName
: generateFixedHash(finalCameraName);
friendly_name = values.cameraName;
}
@ -366,6 +365,8 @@ export default function CameraEditForm({
// Add update_topic for new cameras
if (!cameraName) {
requestBody.update_topic = `config/cameras/${finalCameraName}/add`;
} else {
requestBody.update_topic = `config/cameras/${finalCameraName}/edit`;
}
axios
@ -399,51 +400,7 @@ export default function CameraEditForm({
};
const onSubmit = (values: FormValues) => {
if (cameraName && values.cameraName !== cameraName) {
// If camera name changed, delete old camera config
const deleteRequestBody: ConfigSetBody = {
requires_restart: 1,
config_data: {
cameras: {
[cameraName]: "",
},
},
update_topic: `config/cameras/${cameraName}/remove`,
};
const camera = config?.cameras[cameraName];
if (values.ffmpeg.inputs.some((input) => input.go2rtc)) {
camera?.ffmpeg.inputs.map((input) => {
const isGo2rtcPath = input.path.match(
/^rtsp:\/\/127\.0\.0\.1:8554\/(.+)$/,
);
const go2rtcStreamName = isGo2rtcPath ? isGo2rtcPath[1] : "";
deleteRequestBody.config_data.go2rtc = {
streams: {
[go2rtcStreamName]: "",
},
};
});
}
axios
.put("config/set", deleteRequestBody)
.then(() => saveCameraConfig(values))
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("toast.save.error.title", { errorMessage, ns: "common" }),
{ position: "top-center" },
);
})
.finally(() => {
setIsLoading(false);
});
} else {
saveCameraConfig(values);
}
};
// Determine available roles for new streams
@ -486,11 +443,19 @@ export default function CameraEditForm({
render={({ field }) => (
<FormItem>
<FormLabel>{t("camera.cameraConfig.name")}</FormLabel>
{cameraName ? (
<>
<div className="my-3 text-sm text-muted-foreground">
{t("camera.cameraConfig.nameOnlyChangeToFriendlyName")}
</div>
</>
) : (
""
)}
<FormControl>
<Input
placeholder={t("camera.cameraConfig.namePlaceholder")}
{...field}
disabled={!!cameraName} // Prevent editing name for existing cameras
/>
</FormControl>
<FormMessage />

View File

@ -0,0 +1,83 @@
import * as React from "react"
import { Input, InputProps } from "./input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue
} from "./select"
export interface SelectInputProps extends InputProps {
options: { value: string; label: string }[]
onSelectChange?: (value: string) => void
selectPlaceholder?: string
selectClassName?: string
selectPosition?: "before" | "after"
}
const SelectInput = React.forwardRef<HTMLInputElement, SelectInputProps>(
({
className,
options,
onSelectChange,
selectPlaceholder = "Select...",
selectClassName = "w-[180px]",
selectPosition = "after",
...props
}, ref) => {
const [selectedValue, setSelectedValue] = React.useState<string>("")
const handleValueChange = (value: string) => {
setSelectedValue(value)
if (onSelectChange) {
onSelectChange(value)
}
}
return (
<div className="flex items-center gap-2">
{selectPosition === "before" && (
<Select value={selectedValue} onValueChange={handleValueChange}>
<SelectTrigger className={selectClassName}>
<SelectValue placeholder={selectPlaceholder} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
<Input className={className} ref={ref} {...props} />
{selectPosition === "after" && (
<Select value={selectedValue} onValueChange={handleValueChange}>
<SelectTrigger className={selectClassName}>
<SelectValue placeholder={selectPlaceholder} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
</div>
)
}
)
SelectInput.displayName = "SelectInput"
export { SelectInput }