forked from Github/frigate
Compare commits
8 Commits
0.16
...
dynamic-ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38c8d79b56 | ||
|
|
c2dca6b495 | ||
|
|
3947e79086 | ||
|
|
91ab1071d2 | ||
|
|
409e911752 | ||
|
|
9983bd8d92 | ||
|
|
32c71c4108 | ||
|
|
ef6952e3ea |
@@ -215,7 +215,6 @@ ENV TRANSFORMERS_NO_ADVISORY_WARNINGS=1
|
|||||||
ENV OPENCV_FFMPEG_LOGLEVEL=8
|
ENV OPENCV_FFMPEG_LOGLEVEL=8
|
||||||
|
|
||||||
ENV PATH="/usr/local/go2rtc/bin:/usr/local/tempio/bin:/usr/local/nginx/sbin:${PATH}"
|
ENV PATH="/usr/local/go2rtc/bin:/usr/local/tempio/bin:/usr/local/nginx/sbin:${PATH}"
|
||||||
ENV LIBAVFORMAT_VERSION_MAJOR=60
|
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=bind,source=docker/main/install_deps.sh,target=/deps/install_deps.sh \
|
RUN --mount=type=bind,source=docker/main/install_deps.sh,target=/deps/install_deps.sh \
|
||||||
|
|||||||
@@ -42,8 +42,14 @@ function migrate_db_path() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function set_libva_version() {
|
||||||
|
local ffmpeg_path=$(python3 docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py)
|
||||||
|
export LIBAVFORMAT_VERSION_MAJOR=$($ffmpeg_path -version | grep -Po "libavformat\W+\K\d+")
|
||||||
|
}
|
||||||
|
|
||||||
echo "[INFO] Preparing Frigate..."
|
echo "[INFO] Preparing Frigate..."
|
||||||
migrate_db_path
|
migrate_db_path
|
||||||
|
set_libva_version
|
||||||
echo "[INFO] Starting Frigate..."
|
echo "[INFO] Starting Frigate..."
|
||||||
|
|
||||||
cd /opt/frigate || echo "[ERROR] Failed to change working directory to /opt/frigate"
|
cd /opt/frigate || echo "[ERROR] Failed to change working directory to /opt/frigate"
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ function get_ip_and_port_from_supervisor() {
|
|||||||
export FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL="${ip_address}:${webrtc_port}"
|
export FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL="${ip_address}:${webrtc_port}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function set_libva_version() {
|
||||||
|
local ffmpeg_path=$(python3 /usr/local/ffmpeg/get_ffmpeg_path.py)
|
||||||
|
export LIBAVFORMAT_VERSION_MAJOR=$($ffmpeg_path -version | grep -Po "libavformat\W+\K\d+")
|
||||||
|
}
|
||||||
|
|
||||||
if [[ -f "/dev/shm/go2rtc.yaml" ]]; then
|
if [[ -f "/dev/shm/go2rtc.yaml" ]]; then
|
||||||
echo "[INFO] Removing stale config from last run..."
|
echo "[INFO] Removing stale config from last run..."
|
||||||
rm /dev/shm/go2rtc.yaml
|
rm /dev/shm/go2rtc.yaml
|
||||||
@@ -61,6 +66,8 @@ else
|
|||||||
echo "[WARNING] Unable to remove existing go2rtc config. Changes made to your frigate config file may not be recognized. Please remove the /dev/shm/go2rtc.yaml from your docker host manually."
|
echo "[WARNING] Unable to remove existing go2rtc config. Changes made to your frigate config file may not be recognized. Please remove the /dev/shm/go2rtc.yaml from your docker host manually."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
set_libva_version
|
||||||
|
|
||||||
readonly config_path="/config"
|
readonly config_path="/config"
|
||||||
|
|
||||||
if [[ -x "${config_path}/go2rtc" ]]; then
|
if [[ -x "${config_path}/go2rtc" ]]; then
|
||||||
|
|||||||
45
docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py
Normal file
45
docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
sys.path.insert(0, "/opt/frigate")
|
||||||
|
from frigate.const import (
|
||||||
|
DEFAULT_FFMPEG_VERSION,
|
||||||
|
INCLUDED_FFMPEG_VERSIONS,
|
||||||
|
)
|
||||||
|
|
||||||
|
sys.path.remove("/opt/frigate")
|
||||||
|
|
||||||
|
yaml = YAML()
|
||||||
|
|
||||||
|
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
|
||||||
|
|
||||||
|
# Check if we can use .yaml instead of .yml
|
||||||
|
config_file_yaml = config_file.replace(".yml", ".yaml")
|
||||||
|
if os.path.isfile(config_file_yaml):
|
||||||
|
config_file = config_file_yaml
|
||||||
|
|
||||||
|
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:
|
||||||
|
config: dict[str, any] = {}
|
||||||
|
|
||||||
|
path = config.get("ffmpeg", {}).get("path", "default")
|
||||||
|
if path == "default":
|
||||||
|
if shutil.which("ffmpeg") is None:
|
||||||
|
print(f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg")
|
||||||
|
else:
|
||||||
|
print("ffmpeg")
|
||||||
|
elif path in INCLUDED_FFMPEG_VERSIONS:
|
||||||
|
print(f"/usr/lib/ffmpeg/{path}/bin/ffmpeg")
|
||||||
|
else:
|
||||||
|
print(f"{path}/bin/ffmpeg")
|
||||||
@@ -22,6 +22,6 @@ ADD https://github.com/MarcA711/rknn-toolkit2/releases/download/v2.0.0/librknnrt
|
|||||||
|
|
||||||
RUN rm -rf /usr/lib/btbn-ffmpeg/bin/ffmpeg
|
RUN rm -rf /usr/lib/btbn-ffmpeg/bin/ffmpeg
|
||||||
RUN rm -rf /usr/lib/btbn-ffmpeg/bin/ffprobe
|
RUN rm -rf /usr/lib/btbn-ffmpeg/bin/ffprobe
|
||||||
ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-5/ffmpeg /usr/lib/ffmpeg/6.0/bin/
|
ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-7/ffmpeg /usr/lib/ffmpeg/6.0/bin/
|
||||||
ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-5/ffprobe /usr/lib/ffmpeg/6.0/bin/
|
ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-7/ffprobe /usr/lib/ffmpeg/6.0/bin/
|
||||||
ENV PATH="/usr/lib/ffmpeg/6.0/bin/:${PATH}"
|
ENV PATH="/usr/lib/ffmpeg/6.0/bin/:${PATH}"
|
||||||
|
|||||||
@@ -12,7 +12,5 @@ RUN rm -rf /usr/lib/btbn-ffmpeg/
|
|||||||
RUN --mount=type=bind,source=docker/rpi/install_deps.sh,target=/deps/install_deps.sh \
|
RUN --mount=type=bind,source=docker/rpi/install_deps.sh,target=/deps/install_deps.sh \
|
||||||
/deps/install_deps.sh
|
/deps/install_deps.sh
|
||||||
|
|
||||||
ENV LIBAVFORMAT_VERSION_MAJOR=58
|
|
||||||
|
|
||||||
WORKDIR /opt/frigate/
|
WORKDIR /opt/frigate/
|
||||||
COPY --from=rootfs / /
|
COPY --from=rootfs / /
|
||||||
|
|||||||
@@ -548,6 +548,8 @@ genai:
|
|||||||
|
|
||||||
# Optional: Restream configuration
|
# Optional: Restream configuration
|
||||||
# Uses https://github.com/AlexxIT/go2rtc (v1.9.2)
|
# Uses https://github.com/AlexxIT/go2rtc (v1.9.2)
|
||||||
|
# NOTE: The default go2rtc API port (1984) must be used,
|
||||||
|
# changing this port for the integrated go2rtc instance is not supported.
|
||||||
go2rtc:
|
go2rtc:
|
||||||
|
|
||||||
# Optional: Live stream configuration for WebUI.
|
# Optional: Live stream configuration for WebUI.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ that card.
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
When configuring the integration, you will be asked for the `URL` of your Frigate instance which needs to be pointed at the internal unauthenticated port (`5000`) for your instance. This may look like `http://<host>:5000/`.
|
When configuring the integration, you will be asked for the `URL` of your Frigate instance which can be pointed at the internal unauthenticated port (`5000`) or the authenticated port (`8971`) for your instance. This may look like `http://<host>:5000/`.
|
||||||
|
|
||||||
### Docker Compose Examples
|
### Docker Compose Examples
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ If you are running Home Assistant Core and Frigate with Docker Compose on the sa
|
|||||||
|
|
||||||
#### Home Assistant running with host networking
|
#### Home Assistant running with host networking
|
||||||
|
|
||||||
It is not recommended to run Frigate in host networking mode. In this example, you would use `http://172.17.0.1:5000` when configuring the integration.
|
It is not recommended to run Frigate in host networking mode. In this example, you would use `http://172.17.0.1:5000` or `http://172.17.0.1:8971` when configuring the integration.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -75,7 +75,7 @@ services:
|
|||||||
|
|
||||||
#### Home Assistant _not_ running with host networking or in a separate compose file
|
#### Home Assistant _not_ running with host networking or in a separate compose file
|
||||||
|
|
||||||
In this example, you would use `http://frigate:5000` when configuring the integration. There is no need to map the port for the Frigate container.
|
In this example, it is recommended to connect to the authenticated port, for example, `http://frigate:8971` when configuring the integration. There is no need to map the port for the Frigate container.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -103,14 +103,15 @@ If you are using HassOS with the addon, the URL should be one of the following d
|
|||||||
| Frigate NVR (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
| Frigate NVR (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
||||||
| Frigate NVR Beta | `http://ccab4aaf-frigate-beta:5000` |
|
| Frigate NVR Beta | `http://ccab4aaf-frigate-beta:5000` |
|
||||||
| Frigate NVR Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
|
| Frigate NVR Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
|
||||||
|
| Frigate NVR HailoRT Beta | `http://ccab4aaf-frigate-hailo-beta:5000` |
|
||||||
|
|
||||||
### Frigate running on a separate machine
|
### Frigate running on a separate machine
|
||||||
|
|
||||||
If you run Frigate on a separate device within your local network, Home Assistant will need access to port 5000.
|
If you run Frigate on a separate device within your local network, Home Assistant will need access to port 8971.
|
||||||
|
|
||||||
#### Local network
|
#### Local network
|
||||||
|
|
||||||
Use `http://<frigate_device_ip>:5000` as the URL for the integration. If you want to protect access to port 5000, you can use firewall rules to limit access to the device running Home Assistant.
|
Use `http://<frigate_device_ip>:8971` as the URL for the integration so that authentication is required.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -118,7 +119,7 @@ services:
|
|||||||
image: ghcr.io/blakeblackshear/frigate:stable
|
image: ghcr.io/blakeblackshear/frigate:stable
|
||||||
...
|
...
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "8971:8971"
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -195,12 +196,30 @@ To load a snapshot for a tracked object:
|
|||||||
https://HA_URL/api/frigate/notifications/<event-id>/snapshot.jpg
|
https://HA_URL/api/frigate/notifications/<event-id>/snapshot.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
To load a video clip of a tracked object:
|
To load a video clip of a tracked object using an Android device:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://HA_URL/api/frigate/notifications/<event-id>/clip.mp4
|
https://HA_URL/api/frigate/notifications/<event-id>/clip.mp4
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To load a video clip of a tracked object using an iOS device:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://HA_URL/api/frigate/notifications/<event-id>/master.m3u8
|
||||||
|
```
|
||||||
|
|
||||||
|
To load a preview gif of a tracked object:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://HA_URL/api/frigate/notifications/<event-id>/event_preview.gif
|
||||||
|
```
|
||||||
|
|
||||||
|
To load a preview gif of a review item:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://HA_URL/api/frigate/notifications/<review-id>/review_preview.gif
|
||||||
|
```
|
||||||
|
|
||||||
<a name="streams"></a>
|
<a name="streams"></a>
|
||||||
|
|
||||||
## RTSP stream
|
## RTSP stream
|
||||||
|
|||||||
@@ -133,6 +133,15 @@ def latest_frame(
|
|||||||
"regions": params.regions,
|
"regions": params.regions,
|
||||||
}
|
}
|
||||||
quality = params.quality
|
quality = params.quality
|
||||||
|
mime_type = extension
|
||||||
|
|
||||||
|
if extension == "png":
|
||||||
|
quality_params = None
|
||||||
|
elif extension == "webp":
|
||||||
|
quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), quality]
|
||||||
|
else:
|
||||||
|
quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
|
||||||
|
mime_type = "jpeg"
|
||||||
|
|
||||||
if camera_name in request.app.frigate_config.cameras:
|
if camera_name in request.app.frigate_config.cameras:
|
||||||
frame = frame_processor.get_current_frame(camera_name, draw_options)
|
frame = frame_processor.get_current_frame(camera_name, draw_options)
|
||||||
@@ -173,13 +182,11 @@ def latest_frame(
|
|||||||
|
|
||||||
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
||||||
|
|
||||||
ret, img = cv2.imencode(
|
ret, img = cv2.imencode(f".{extension}", frame, quality_params)
|
||||||
f".{extension}", frame, [int(cv2.IMWRITE_WEBP_QUALITY), quality]
|
|
||||||
)
|
|
||||||
return Response(
|
return Response(
|
||||||
content=img.tobytes(),
|
content=img.tobytes(),
|
||||||
media_type=f"image/{extension}",
|
media_type=f"image/{mime_type}",
|
||||||
headers={"Content-Type": f"image/{extension}", "Cache-Control": "no-store"},
|
headers={"Content-Type": f"image/{mime_type}", "Cache-Control": "no-store"},
|
||||||
)
|
)
|
||||||
elif camera_name == "birdseye" and request.app.frigate_config.birdseye.restream:
|
elif camera_name == "birdseye" and request.app.frigate_config.birdseye.restream:
|
||||||
frame = cv2.cvtColor(
|
frame = cv2.cvtColor(
|
||||||
@@ -192,13 +199,11 @@ def latest_frame(
|
|||||||
|
|
||||||
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
||||||
|
|
||||||
ret, img = cv2.imencode(
|
ret, img = cv2.imencode(f".{extension}", frame, quality_params)
|
||||||
f".{extension}", frame, [int(cv2.IMWRITE_WEBP_QUALITY), quality]
|
|
||||||
)
|
|
||||||
return Response(
|
return Response(
|
||||||
content=img.tobytes(),
|
content=img.tobytes(),
|
||||||
media_type=f"image/{extension}",
|
media_type=f"image/{mime_type}",
|
||||||
headers={"Content-Type": f"image/{extension}", "Cache-Control": "no-store"},
|
headers={"Content-Type": f"image/{mime_type}", "Cache-Control": "no-store"},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -241,6 +246,7 @@ def get_snapshot_from_recording(
|
|||||||
recording: Recordings = recording_query.get()
|
recording: Recordings = recording_query.get()
|
||||||
time_in_segment = frame_time - recording.start_time
|
time_in_segment = frame_time - recording.start_time
|
||||||
codec = "png" if format == "png" else "mjpeg"
|
codec = "png" if format == "png" else "mjpeg"
|
||||||
|
mime_type = "png" if format == "png" else "jpeg"
|
||||||
config: FrigateConfig = request.app.frigate_config
|
config: FrigateConfig = request.app.frigate_config
|
||||||
|
|
||||||
image_data = get_image_from_recording(
|
image_data = get_image_from_recording(
|
||||||
@@ -257,7 +263,7 @@ def get_snapshot_from_recording(
|
|||||||
),
|
),
|
||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
return Response(image_data, headers={"Content-Type": f"image/{format}"})
|
return Response(image_data, headers={"Content-Type": f"image/{mime_type}"})
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
|
|||||||
@@ -50,16 +50,9 @@ class LibvaGpuSelector:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
FPS_VFR_PARAM = (
|
LIBAV_VERSION = int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59") or "59")
|
||||||
"-fps_mode vfr"
|
FPS_VFR_PARAM = "-fps_mode vfr" if LIBAV_VERSION >= 59 else "-vsync 2"
|
||||||
if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59") or "59") >= 59
|
TIMEOUT_PARAM = "-timeout" if LIBAV_VERSION >= 59 else "-stimeout"
|
||||||
else "-vsync 2"
|
|
||||||
)
|
|
||||||
TIMEOUT_PARAM = (
|
|
||||||
"-timeout"
|
|
||||||
if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59") or "59") >= 59
|
|
||||||
else "-stimeout"
|
|
||||||
)
|
|
||||||
|
|
||||||
_gpu_selector = LibvaGpuSelector()
|
_gpu_selector = LibvaGpuSelector()
|
||||||
_user_agent_args = [
|
_user_agent_args = [
|
||||||
@@ -71,8 +64,8 @@ PRESETS_HW_ACCEL_DECODE = {
|
|||||||
"preset-rpi-64-h264": "-c:v:1 h264_v4l2m2m",
|
"preset-rpi-64-h264": "-c:v:1 h264_v4l2m2m",
|
||||||
"preset-rpi-64-h265": "-c:v:1 hevc_v4l2m2m",
|
"preset-rpi-64-h265": "-c:v:1 hevc_v4l2m2m",
|
||||||
FFMPEG_HWACCEL_VAAPI: f"-hwaccel_flags allow_profile_mismatch -hwaccel vaapi -hwaccel_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format vaapi",
|
FFMPEG_HWACCEL_VAAPI: f"-hwaccel_flags allow_profile_mismatch -hwaccel vaapi -hwaccel_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format vaapi",
|
||||||
"preset-intel-qsv-h264": f"-hwaccel qsv -qsv_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format qsv -c:v h264_qsv -bsf:v dump_extra", # https://trac.ffmpeg.org/ticket/9766#comment:17
|
"preset-intel-qsv-h264": f"-hwaccel qsv -qsv_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format qsv -c:v h264_qsv{' -bsf:v dump_extra' if LIBAV_VERSION >= 61 else ''}", # https://trac.ffmpeg.org/ticket/9766#comment:17
|
||||||
"preset-intel-qsv-h265": f"-load_plugin hevc_hw -hwaccel qsv -qsv_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format qsv -c:v hevc_qsv -bsf:v dump_extra", # https://trac.ffmpeg.org/ticket/9766#comment:17
|
"preset-intel-qsv-h265": f"-load_plugin hevc_hw -hwaccel qsv -qsv_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format qsv{' -bsf:v dump_extra' if LIBAV_VERSION >= 61 else ''}", # https://trac.ffmpeg.org/ticket/9766#comment:17
|
||||||
FFMPEG_HWACCEL_NVIDIA: "-hwaccel cuda -hwaccel_output_format cuda",
|
FFMPEG_HWACCEL_NVIDIA: "-hwaccel cuda -hwaccel_output_format cuda",
|
||||||
"preset-jetson-h264": "-c:v h264_nvmpi -resize {1}x{2}",
|
"preset-jetson-h264": "-c:v h264_nvmpi -resize {1}x{2}",
|
||||||
"preset-jetson-h265": "-c:v hevc_nvmpi -resize {1}x{2}",
|
"preset-jetson-h265": "-c:v hevc_nvmpi -resize {1}x{2}",
|
||||||
|
|||||||
@@ -543,16 +543,16 @@ function ObjectDetailsTab({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(config?.cameras[search.camera].genai.enabled && search.end_time) ||
|
{((config?.cameras[search.camera].genai.enabled && search.end_time) ||
|
||||||
(!config?.cameras[search.camera].genai.enabled && (
|
!config?.cameras[search.camera].genai.enabled) && (
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
aria-label="Save"
|
aria-label="Save"
|
||||||
onClick={updateDescription}
|
onClick={updateDescription}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user