mirror of
https://github.com/gradio-app/gradio.git
synced 2025-02-17 11:29:58 +08:00
Move scripts from old website to CI (#5271)
* delete old version docs script * script and workflow * if merged * syntax * changes * add missing demos * hide state component * fix html component demo * fix model3d guide * fix workflow * fix issues with oauth and main * demo notebooks * add changeset * demo notebook * refactor into existing workflows * remove assert in networking server * add changeset * Fix type check * lint --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: freddyaboulton <alfonsoboulton@gmail.com>
This commit is contained in:
parent
6466652583
commit
97c3c7b173
6
.changeset/silent-pens-decide.md
Normal file
6
.changeset/silent-pens-decide.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"gradio": minor
|
||||
"website": minor
|
||||
---
|
||||
|
||||
feat:Move scripts from old website to CI
|
9
.github/workflows/build-pr.yml
vendored
9
.github/workflows/build-pr.yml
vendored
@ -1,6 +1,9 @@
|
||||
name: Build PR Artifacts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
@ -35,9 +38,13 @@ jobs:
|
||||
- name: Get PR Number
|
||||
id: get_pr_number
|
||||
run: |
|
||||
echo "GRADIO_VERSION=$(python -c 'import requests;print(requests.get("https://pypi.org/pypi/gradio/json").json()["info"]["version"])')" >> $GITHUB_OUTPUT
|
||||
if ${{ github.event_name == 'pull_request' }}; then
|
||||
python -c "import os;print(os.environ['GITHUB_REF'].split('/')[2])" > pr_number.txt
|
||||
echo "PR_NUMBER=$(cat pr_number.txt)" >> $GITHUB_OUTPUT
|
||||
echo "GRADIO_VERSION=$(python -c 'import requests;print(requests.get("https://pypi.org/pypi/gradio/json").json()["info"]["version"])')" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "PR_NUMBER='main'" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Build pr package
|
||||
run: |
|
||||
echo ${{ steps.get_pr_number.outputs.GRADIO_VERSION }} > gradio/version.txt
|
||||
|
43
.github/workflows/build-version-docs.yml
vendored
43
.github/workflows/build-version-docs.yml
vendored
@ -1,43 +0,0 @@
|
||||
# This workflow will build gradio and create the version's docs file when a new stable version has been released
|
||||
|
||||
name: Create Version's Docs
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: closed
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'gradio/version.txt'
|
||||
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install requirements
|
||||
run: python -m pip install -r website/homepage/requirements.txt
|
||||
- name: Check new pypi version
|
||||
run: cd website && python check_version.py
|
||||
build-docs:
|
||||
needs: check-version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install requirements
|
||||
run: python -m pip install -r website/homepage/requirements.txt
|
||||
- name: Pin httpx, httpcore, and h11
|
||||
run: pip install h11==0.12.0 httpcore==0.15.0 httpx==0.23.0
|
||||
- name: Wait 5 min
|
||||
run: sleep 300
|
||||
- name: Install gradio
|
||||
run: python -m pip install gradio==$(cat gradio/version.txt)
|
||||
- name: Build Docs
|
||||
run: |
|
||||
export HF_AUTH_TOKEN=${{ secrets.HF_AUTH_TOKEN }}
|
||||
cd website/homepage && python build-version-docs.py
|
14
.github/workflows/deploy-pr-to-spaces.yml
vendored
14
.github/workflows/deploy-pr-to-spaces.yml
vendored
@ -14,9 +14,6 @@ jobs:
|
||||
sha: ${{ steps.set-outputs.outputs.gh_sha }}
|
||||
gradio_version: ${{ steps.set-outputs.outputs.gradio_version }}
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Python
|
||||
@ -51,6 +48,9 @@ jobs:
|
||||
- run: unzip all_demos.zip -d all_demos
|
||||
- run: cp -R all_demos/* demo/all_demos
|
||||
- name: Upload demo to spaces
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
id: upload-demo
|
||||
run: |
|
||||
python scripts/upload_demo_to_space.py all_demos \
|
||||
@ -58,6 +58,14 @@ jobs:
|
||||
${{ secrets.SPACES_DEPLOY_TOKEN }} \
|
||||
--gradio-version ${{ steps.set-outputs.outputs.gradio_version }} > url.txt
|
||||
echo "SPACE_URL=$(cat url.txt)" >> $GITHUB_OUTPUT
|
||||
- name: Upload website demos
|
||||
if: >
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
run: |
|
||||
python scripts/upload_website_demos.py --AUTH_TOKEN ${{ secrets.SPACES_DEPLOY_TOKEN }} \
|
||||
--WHEEL_URL https://gradio-builds.s3.amazonaws.com/${{ steps.set-outputs.outputs.gh_sha }}/ \
|
||||
|
||||
comment-spaces-success:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [deploy-current-pr]
|
||||
|
1
demo/duplicatebutton_component/run.ipynb
Normal file
1
demo/duplicatebutton_component/run.ipynb
Normal file
@ -0,0 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: duplicatebutton_component"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr \n", "\n", "with gr.Blocks() as demo:\n", " gr.DuplicateButton()\n", "\n", "demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
6
demo/duplicatebutton_component/run.py
Normal file
6
demo/duplicatebutton_component/run.py
Normal file
@ -0,0 +1,6 @@
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
gr.DuplicateButton()
|
||||
|
||||
demo.launch()
|
@ -1 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: html_component"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr \n", "\n", "with gr.Blocks() as demo:\n", " gr.HTML(value=\"<p style='margin-top: 1rem, margin-bottom: 1rem'>Gradio Docs Readers: <img src='https://visitor-badge.glitch.me/badge?page_id=gradio-docs-visitor-badge' alt='visitor badge' style='display: inline-block'/></p>\")\n", "\n", "demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
||||
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: html_component"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr \n", "\n", "with gr.Blocks() as demo:\n", " gr.HTML(value=\"<p style='margin-top: 1rem, margin-bottom: 1rem'>This <em>example</em> was <strong>written</strong> in <a href='https://en.wikipedia.org/wiki/HTML' _target='blank'>HTML</a> </p>\")\n", "\n", "demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
@ -1,6 +1,6 @@
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
gr.HTML(value="<p style='margin-top: 1rem, margin-bottom: 1rem'>Gradio Docs Readers: <img src='https://visitor-badge.glitch.me/badge?page_id=gradio-docs-visitor-badge' alt='visitor badge' style='display: inline-block'/></p>")
|
||||
gr.HTML(value="<p style='margin-top: 1rem, margin-bottom: 1rem'>This <em>example</em> was <strong>written</strong> in <a href='https://en.wikipedia.org/wiki/HTML' _target='blank'>HTML</a> </p>")
|
||||
|
||||
demo.launch()
|
1
demo/loginbutton_component/requirements.txt
Normal file
1
demo/loginbutton_component/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
gradio[oauth]
|
1
demo/loginbutton_component/run.ipynb
Normal file
1
demo/loginbutton_component/run.ipynb
Normal file
@ -0,0 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: loginbutton_component"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio gradio[oauth]"]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr \n", "\n", "with gr.Blocks() as demo:\n", " gr.LoginButton()\n", "\n", "demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
6
demo/loginbutton_component/run.py
Normal file
6
demo/loginbutton_component/run.py
Normal file
@ -0,0 +1,6 @@
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
gr.LoginButton()
|
||||
|
||||
demo.launch()
|
1
demo/logoutbutton_component/requirements.txt
Normal file
1
demo/logoutbutton_component/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
gradio[oauth]
|
1
demo/logoutbutton_component/run.ipynb
Normal file
1
demo/logoutbutton_component/run.ipynb
Normal file
@ -0,0 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: logoutbutton_component"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio gradio[oauth]"]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr \n", "\n", "with gr.Blocks() as demo:\n", " gr.LogoutButton()\n", "\n", "demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
6
demo/logoutbutton_component/run.py
Normal file
6
demo/logoutbutton_component/run.py
Normal file
@ -0,0 +1,6 @@
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
gr.LogoutButton()
|
||||
|
||||
demo.launch()
|
@ -42,7 +42,7 @@ class JupyterReloader(BaseReloader):
|
||||
@property
|
||||
def running_app(self) -> App:
|
||||
assert self.running_demo.server
|
||||
return self.running_demo.server.running_app
|
||||
return self.running_demo.server.running_app # type: ignore
|
||||
|
||||
@property
|
||||
def running_demo(self):
|
||||
|
@ -43,7 +43,6 @@ class Server(uvicorn.Server):
|
||||
def __init__(
|
||||
self, config: Config, reloader: SourceFileReloader | None = None
|
||||
) -> None:
|
||||
assert isinstance(config.app, App)
|
||||
self.running_app = config.app
|
||||
super().__init__(config)
|
||||
self.reloader = reloader
|
||||
|
@ -1,6 +1,6 @@
|
||||
# How to Use the 3D Model Component
|
||||
|
||||
Related spaces: https://huggingface.co/spaces/dawood/Model3D, https://huggingface.co/spaces/radames/PIFu-Clothed-Human-Digitization, https://huggingface.co/spaces/radames/dpt-depth-estimation-3d-obj
|
||||
Related spaces: https://huggingface.co/spaces/gradio/Model3D, https://huggingface.co/spaces/gradio/PIFu-Clothed-Human-Digitization, https://huggingface.co/spaces/gradio/dpt-depth-estimation-3d-obj
|
||||
Tags: VISION, IMAGE
|
||||
|
||||
## Introduction
|
||||
@ -9,7 +9,7 @@ Tags: VISION, IMAGE
|
||||
|
||||
This guide will show you how to build a demo for your 3D image model in a few lines of code; like the one below. Play around with 3D object by clicking around, dragging and zooming:
|
||||
|
||||
<gradio-app space="dawood/Model3D"> </gradio-app>
|
||||
<gradio-app space="gradio/Model3D"> </gradio-app>
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@ -21,24 +21,28 @@ Let's take a look at how to create the minimal interface above. The prediction f
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
import os
|
||||
|
||||
|
||||
def load_mesh(mesh_file_name):
|
||||
return mesh_file_name
|
||||
|
||||
|
||||
demo = gr.Interface(
|
||||
fn=load_mesh,
|
||||
inputs=gr.Model3D(),
|
||||
outputs=gr.Model3D(clear_color=[0.0, 0.0, 0.0, 0.0], label="3D Model"),
|
||||
outputs=gr.Model3D(
|
||||
clear_color=[0.0, 0.0, 0.0, 0.0], label="3D Model"),
|
||||
examples=[
|
||||
["files/Bunny.obj"],
|
||||
["files/Duck.glb"],
|
||||
["files/Fox.gltf"],
|
||||
["files/face.obj"],
|
||||
[os.path.join(os.path.dirname(__file__), "files/Bunny.obj")],
|
||||
[os.path.join(os.path.dirname(__file__), "files/Duck.glb")],
|
||||
[os.path.join(os.path.dirname(__file__), "files/Fox.gltf")],
|
||||
[os.path.join(os.path.dirname(__file__), "files/face.obj")],
|
||||
],
|
||||
cache_examples=True,
|
||||
)
|
||||
|
||||
demo.launch()
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
Let's break down the code above:
|
||||
@ -55,18 +59,14 @@ Creating the Interface:
|
||||
- `examples`: list of 3D model files. The 3D model component can accept _.obj_, _.glb_, & _.gltf_ file types.
|
||||
- `cache_examples`: saves the predicted output for the examples, to save time on inference.
|
||||
|
||||
## Exploring mode complex Model3D Demos:
|
||||
## Exploring a more complex Model3D Demo:
|
||||
|
||||
Below is a demo that uses the DPT model to predict the depth of an image and then uses 3D Point Cloud to create a 3D object. Take a look at the [app.py](https://huggingface.co/spaces/radames/dpt-depth-estimation-3d-obj/blob/main/app.py) file for a peek into the code and the model prediction function.
|
||||
<gradio-app space="radames/dpt-depth-estimation-3d-obj"> </gradio-app>
|
||||
|
||||
Below is a demo that uses the PIFu model to convert an image of a clothed human into a 3D digitized model. Take a look at the [spaces.py](https://huggingface.co/spaces/radames/PIFu-Clothed-Human-Digitization/blob/main/PIFu/spaces.py) file for a peek into the code and the model prediction function.
|
||||
|
||||
<gradio-app space="radames/PIFu-Clothed-Human-Digitization"> </gradio-app>
|
||||
Below is a demo that uses the DPT model to predict the depth of an image and then uses 3D Point Cloud to create a 3D object. Take a look at the [app.py](https://huggingface.co/spaces/gradio/dpt-depth-estimation-3d-obj/blob/main/app.py) file for a peek into the code and the model prediction function.
|
||||
<gradio-app space="gradio/dpt-depth-estimation-3d-obj"> </gradio-app>
|
||||
|
||||
---
|
||||
|
||||
And you're done! That's all the code you need to build an interface for your Model3D model. Here are some references that you may find useful:
|
||||
|
||||
- Gradio's ["Getting Started" guide](https://gradio.app/getting_started/)
|
||||
- The first [3D Model Demo](https://huggingface.co/spaces/dawood/Model3D) and [complete code](https://huggingface.co/spaces/dawood/Model3D/tree/main) (on Hugging Face Spaces)
|
||||
- The first [3D Model Demo](https://huggingface.co/spaces/gradio/Model3D) and [complete code](https://huggingface.co/spaces/gradio/Model3D/tree/main) (on Hugging Face Spaces)
|
||||
|
@ -165,11 +165,13 @@
|
||||
{#if mode === "components"}
|
||||
<div class="embedded-component">
|
||||
{#key obj.name}
|
||||
{#if obj.name !== "State"}
|
||||
<gradio-app
|
||||
space={"gradio/" +
|
||||
obj.name.toLowerCase() +
|
||||
"_component_main"}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
|
109
scripts/upload_website_demos.py
Normal file
109
scripts/upload_website_demos.py
Normal file
@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import textwrap
|
||||
import requests
|
||||
|
||||
import huggingface_hub
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
VERSION_TXT = os.path.abspath(os.path.join(ROOT, "gradio", "version.txt"))
|
||||
DIR = os.path.dirname(__file__)
|
||||
GRADIO_DEMO_DIR = os.path.abspath(os.path.join(ROOT, "demo"))
|
||||
|
||||
with open(VERSION_TXT) as f:
|
||||
gradio_version=f.read()
|
||||
gradio_version = gradio_version.strip()
|
||||
|
||||
# Reasoning:
|
||||
# 1. all_demos includes all demos and is for testing PRs
|
||||
# 2. reset_components includes media files that are only present in all_demos (only for PRs)
|
||||
# 3. custom_path doesn't have .launch since the point is to show how to launch with uvicorn
|
||||
# 4. The same reason as 2 for kitchen_sink_random and blocks_kitchen_sink
|
||||
DEMOS_TO_SKIP = {"all_demos", "clear_components", "custom_path", "kitchen_sink_random", "blocks_kitchen_sink"}
|
||||
|
||||
|
||||
def upload_demo_to_space(
|
||||
demo_name: str,
|
||||
space_id: str,
|
||||
hf_token: str,
|
||||
gradio_version: str | None,
|
||||
gradio_wheel_url: str | None = None
|
||||
):
|
||||
"""Upload a demo in the demo directory to a huggingface space.
|
||||
Parameters:
|
||||
demo_name: The name of the demo to upload.
|
||||
space_id: The id of the space to upload the demo to.
|
||||
hf_token: HF api token. Need to have permission to write to space_id for this to work.
|
||||
gradio_version: If not None, will set the gradio version in the created Space to the given version.
|
||||
gradio_wheel_url: If not None, will install the version of gradio using the wheel url in the created Space.
|
||||
"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
demo_path = pathlib.Path(GRADIO_DEMO_DIR, demo_name)
|
||||
shutil.copytree(demo_path, tmpdir, dirs_exist_ok=True)
|
||||
readme = pathlib.Path(tmpdir, "README.md")
|
||||
readme_content = f"""
|
||||
---
|
||||
title: {space_id.split("/")[-1]}
|
||||
emoji: 🔥
|
||||
colorFrom: indigo
|
||||
colorTo: indigo
|
||||
sdk: gradio
|
||||
sdk_version: {gradio_version}
|
||||
app_file: run.py
|
||||
pinned: false
|
||||
hf_oauth: true
|
||||
---
|
||||
"""
|
||||
readme.open("w").write(textwrap.dedent(readme_content))
|
||||
|
||||
if gradio_wheel_url:
|
||||
requirements_path = os.path.join(tmpdir, "requirements.txt")
|
||||
if not os.path.exists(requirements_path):
|
||||
with open(os.path.join(requirements_path), "w") as f:
|
||||
f.write(gradio_wheel_url)
|
||||
else:
|
||||
with open(os.path.join(requirements_path), "r") as f:
|
||||
content = f.read()
|
||||
with open(os.path.join(requirements_path), "w") as f:
|
||||
f.seek(0, 0)
|
||||
f.write(gradio_wheel_url + '\n' + content)
|
||||
|
||||
api = huggingface_hub.HfApi()
|
||||
huggingface_hub.create_repo(
|
||||
space_id,
|
||||
space_sdk="gradio",
|
||||
repo_type="space",
|
||||
token=hf_token,
|
||||
exist_ok=True,
|
||||
)
|
||||
api.upload_folder(
|
||||
token=hf_token,
|
||||
repo_id=space_id,
|
||||
repo_type="space",
|
||||
folder_path=tmpdir,
|
||||
path_in_repo="",
|
||||
)
|
||||
return f"https://huggingface.co/spaces/{space_id}"
|
||||
|
||||
demos = os.listdir(GRADIO_DEMO_DIR)
|
||||
|
||||
demos = [demo for demo in demos if demo not in DEMOS_TO_SKIP and os.path.isdir(os.path.join(GRADIO_DEMO_DIR, demo)) and os.path.exists(os.path.join(GRADIO_DEMO_DIR, demo, "run.py"))]
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--WHEEL_URL", type=str, help="aws link to gradio wheel")
|
||||
parser.add_argument("--AUTH_TOKEN", type=str, help="huggingface auth token")
|
||||
args = parser.parse_args()
|
||||
gradio_wheel_url = args.WHEEL_URL + f"gradio-{gradio_version}-py3-none-any.whl"
|
||||
if args.AUTH_TOKEN is not None:
|
||||
hello_world_version = str(huggingface_hub.space_info("gradio/hello_world").cardData["sdk_version"])
|
||||
for demo in demos:
|
||||
if hello_world_version != gradio_version:
|
||||
upload_demo_to_space(demo_name=demo, space_id="gradio/" + demo, hf_token=args.AUTH_TOKEN, gradio_version=gradio_version)
|
||||
upload_demo_to_space(demo_name=demo, space_id="gradio/" + demo + "_main", hf_token=args.AUTH_TOKEN, gradio_version=gradio_version, gradio_wheel_url=gradio_wheel_url)
|
Loading…
Reference in New Issue
Block a user