mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 20:24:41 +08:00
Compare commits
61 Commits
fix-select
...
zoltan/pos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8555bcba5b | ||
|
|
4c601944e1 | ||
|
|
a632f1f26a | ||
|
|
4d82e4295f | ||
|
|
a91f64b9f5 | ||
|
|
b0e1ff8073 | ||
|
|
5c455ec2bc | ||
|
|
d5215a5be2 | ||
|
|
15c93100ab | ||
|
|
ab9b070eb0 | ||
|
|
e40673b298 | ||
|
|
7ea009c7f8 | ||
|
|
fef6196195 | ||
|
|
b50cf6e067 | ||
|
|
ccdb6ff261 | ||
|
|
692712961b | ||
|
|
b2e1b257b3 | ||
|
|
5bd73f3264 | ||
|
|
5a8a730cfe | ||
|
|
7c5457a75e | ||
|
|
dab64addf8 | ||
|
|
310662a4d0 | ||
|
|
3490c3b0fd | ||
|
|
8bf3ac9710 | ||
|
|
d0977b5245 | ||
|
|
78b1ae4f27 | ||
|
|
592c599ca6 | ||
|
|
8e11851bb0 | ||
|
|
e9ba45ca4f | ||
|
|
0f9d0317dc | ||
|
|
d1cbef9157 | ||
|
|
5b89d3b807 | ||
|
|
74c7b5a292 | ||
|
|
0adb2461e9 | ||
|
|
7cd10aa49e | ||
|
|
bf042afa98 | ||
|
|
b19e546254 | ||
|
|
5ac702a4c1 | ||
|
|
8a0fa93aec | ||
|
|
65817794b5 | ||
|
|
b719aea078 | ||
|
|
75eb820c73 | ||
|
|
be99781176 | ||
|
|
0a51779107 | ||
|
|
c74af430a6 | ||
|
|
88478a851e | ||
|
|
a8d174ccef | ||
|
|
b3648f0823 | ||
|
|
c53500378e | ||
|
|
10ad94af38 | ||
|
|
4969df8a83 | ||
|
|
f07cc211bd | ||
|
|
ff33237052 | ||
|
|
0291f6d1e7 | ||
|
|
26c52796f6 | ||
|
|
ff97bfc772 | ||
|
|
665daa5a5d | ||
|
|
42661bed36 | ||
|
|
c90677831b | ||
|
|
59ec85b936 | ||
|
|
bcaf94f219 |
@@ -1,77 +0,0 @@
|
||||
name: Usability Review Agent
|
||||
description: Runs AI-powered usability testing using OpenAI Computer Use with Playwright
|
||||
|
||||
inputs:
|
||||
target_url:
|
||||
description: The URL to test for usability
|
||||
required: true
|
||||
openai_api_key:
|
||||
description: OpenAI API key with Computer Use access
|
||||
required: true
|
||||
openai_org:
|
||||
description: OpenAI organization ID
|
||||
required: false
|
||||
grafana_username:
|
||||
description: Grafana username for authentication
|
||||
required: false
|
||||
grafana_password:
|
||||
description: Grafana password for authentication
|
||||
required: false
|
||||
workflow_name:
|
||||
description: The workflow or feature to test
|
||||
required: false
|
||||
default: "the application interface"
|
||||
prompt_file:
|
||||
description: Custom prompt file to use (relative to action directory)
|
||||
required: false
|
||||
default: "prompt.txt"
|
||||
output_text_path:
|
||||
description: Path to save the review output text
|
||||
required: false
|
||||
default: "usability-review.txt"
|
||||
screenshot_path:
|
||||
description: Path to save the final screenshot
|
||||
required: false
|
||||
default: "usability-screenshot.png"
|
||||
|
||||
outputs:
|
||||
review_output:
|
||||
description: Path to the review output file
|
||||
value: ${{ inputs.output_text_path }}
|
||||
screenshot_output:
|
||||
description: Path to the screenshot file
|
||||
value: ${{ inputs.screenshot_path }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ${{ github.action_path }}
|
||||
run: |
|
||||
python -m pip install -U pip
|
||||
pip install -r requirements.txt
|
||||
python -m playwright install --with-deps chromium
|
||||
echo "System info:"
|
||||
free -h || echo "free command not available"
|
||||
df -h | head -5
|
||||
|
||||
- name: Run usability review agent
|
||||
shell: bash
|
||||
working-directory: ${{ github.action_path }}
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ inputs.openai_api_key }}
|
||||
OPENAI_ORG: ${{ inputs.openai_org }}
|
||||
TARGET_URL: ${{ inputs.target_url }}
|
||||
GRAFANA_USERNAME: ${{ inputs.grafana_username }}
|
||||
GRAFANA_PASSWORD: ${{ inputs.grafana_password }}
|
||||
OUTPUT_TEXT_PATH: ${{ github.workspace }}/${{ inputs.output_text_path }}
|
||||
SCREENSHOT_PATH: ${{ github.workspace }}/${{ inputs.screenshot_path }}
|
||||
PROMPT_FILE: ${{ github.action_path }}/${{ inputs.prompt_file }}
|
||||
run: |
|
||||
python run_agent.py
|
||||
@@ -1,11 +0,0 @@
|
||||
from . import default
|
||||
from . import contrib
|
||||
from .computer import Computer
|
||||
from .config import computers_config
|
||||
|
||||
__all__ = [
|
||||
"default",
|
||||
"contrib",
|
||||
"Computer",
|
||||
"computers_config",
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
from typing import Protocol, List, Literal, Dict
|
||||
|
||||
|
||||
class Computer(Protocol):
|
||||
"""Defines the 'shape' (methods/properties) our loop expects."""
|
||||
|
||||
def get_environment(self) -> Literal["windows", "mac", "linux", "browser"]: ...
|
||||
|
||||
def get_dimensions(self) -> tuple[int, int]: ...
|
||||
|
||||
def screenshot(self) -> str: ...
|
||||
|
||||
def click(self, x: int, y: int, button: str = "left") -> None: ...
|
||||
|
||||
def double_click(self, x: int, y: int) -> None: ...
|
||||
|
||||
def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None: ...
|
||||
|
||||
def type(self, text: str) -> None: ...
|
||||
|
||||
def wait(self, ms: int = 1000) -> None: ...
|
||||
|
||||
def move(self, x: int, y: int) -> None: ...
|
||||
|
||||
def keypress(self, keys: List[str]) -> None: ...
|
||||
|
||||
def drag(self, path: List[Dict[str, int]]) -> None: ...
|
||||
|
||||
def get_current_url() -> str: ...
|
||||
@@ -1,10 +0,0 @@
|
||||
from .default import *
|
||||
from .contrib import *
|
||||
|
||||
computers_config = {
|
||||
"local-playwright": LocalPlaywrightBrowser,
|
||||
"docker": DockerComputer,
|
||||
"browserbase": BrowserbaseBrowser,
|
||||
"scrapybara-browser": ScrapybaraBrowser,
|
||||
"scrapybara-ubuntu": ScrapybaraUbuntu,
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
from .browserbase import BrowserbaseBrowser
|
||||
from .local_playwright import LocalPlaywrightBrowser
|
||||
from .docker import DockerComputer
|
||||
from .scrapybara import ScrapybaraBrowser, ScrapybaraUbuntu
|
||||
@@ -1,200 +0,0 @@
|
||||
import os
|
||||
from typing import Tuple, Dict, List, Union, Optional
|
||||
from playwright.sync_api import Browser, Page, BrowserContext, Error as PlaywrightError
|
||||
from ..shared.base_playwright import BasePlaywrightComputer
|
||||
from browserbase import Browserbase
|
||||
from dotenv import load_dotenv
|
||||
import base64
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class BrowserbaseBrowser(BasePlaywrightComputer):
|
||||
"""
|
||||
Browserbase is a headless browser platform that offers a remote browser API. You can use it to control thousands of browsers from anywhere.
|
||||
You can find more information about Browserbase at https://www.browserbase.com/computer-use or view our OpenAI CUA Quickstart at https://docs.browserbase.com/integrations/openai-cua/introduction.
|
||||
|
||||
IMPORTANT: This Browserbase computer requires the use of the `goto` tool defined in playwright_with_custom_functions.py.
|
||||
Make sure to include this tool in your configuration when using the Browserbase computer.
|
||||
"""
|
||||
|
||||
def get_dimensions(self):
|
||||
return self.dimensions
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: int = 1024,
|
||||
height: int = 768,
|
||||
region: str = "us-west-2",
|
||||
proxy: bool = False,
|
||||
virtual_mouse: bool = True,
|
||||
ad_blocker: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize the Browserbase instance. Additional configuration options for features such as persistent cookies, ad blockers, file downloads and more can be found in the Browserbase API documentation: https://docs.browserbase.com/reference/api/create-a-session
|
||||
|
||||
Args:
|
||||
width (int): The width of the browser viewport. Default is 1024.
|
||||
height (int): The height of the browser viewport. Default is 768.
|
||||
region (str): The region for the Browserbase session. Default is "us-west-2". Pick a region close to you for better performance. https://docs.browserbase.com/guides/multi-region
|
||||
proxy (bool): Whether to use a proxy for the session. Default is False. Turn on proxies if you're browsing is frequently interrupted. https://docs.browserbase.com/features/proxies
|
||||
virtual_mouse (bool): Whether to enable the virtual mouse cursor. Default is True.
|
||||
ad_blocker (bool): Whether to enable the built-in ad blocker. Default is False.
|
||||
"""
|
||||
super().__init__()
|
||||
self.bb = Browserbase(api_key=os.getenv("BROWSERBASE_API_KEY"))
|
||||
self.project_id = os.getenv("BROWSERBASE_PROJECT_ID")
|
||||
self.session = None
|
||||
self.dimensions = (width, height)
|
||||
self.region = region
|
||||
self.proxy = proxy
|
||||
self.virtual_mouse = virtual_mouse
|
||||
self.ad_blocker = ad_blocker
|
||||
|
||||
def _get_browser_and_page(self) -> Tuple[Browser, Page]:
|
||||
"""
|
||||
Create a Browserbase session and connect to it.
|
||||
|
||||
Returns:
|
||||
Tuple[Browser, Page]: A tuple containing the connected browser and page objects.
|
||||
"""
|
||||
# Create a session on Browserbase with specified parameters
|
||||
width, height = self.dimensions
|
||||
session_params = {
|
||||
"project_id": self.project_id,
|
||||
"browser_settings": {
|
||||
"viewport": {"width": width, "height": height},
|
||||
"blockAds": self.ad_blocker,
|
||||
},
|
||||
"region": self.region,
|
||||
"proxies": self.proxy,
|
||||
}
|
||||
self.session = self.bb.sessions.create(**session_params)
|
||||
|
||||
# Print the live session URL
|
||||
print(
|
||||
f"Watch and control this browser live at https://www.browserbase.com/sessions/{self.session.id}"
|
||||
)
|
||||
|
||||
# Connect to the remote session
|
||||
browser = self._playwright.chromium.connect_over_cdp(
|
||||
self.session.connect_url, timeout=60000
|
||||
)
|
||||
context = browser.contexts[0]
|
||||
|
||||
# Add event listeners for page creation and closure
|
||||
context.on("page", self._handle_new_page)
|
||||
|
||||
# Only add the init script if virtual_mouse is True
|
||||
if self.virtual_mouse:
|
||||
context.add_init_script(
|
||||
"""
|
||||
// Only run in the top frame
|
||||
if (window.self === window.top) {
|
||||
function initCursor() {
|
||||
const CURSOR_ID = '__cursor__';
|
||||
|
||||
// Check if cursor element already exists
|
||||
if (document.getElementById(CURSOR_ID)) return;
|
||||
|
||||
const cursor = document.createElement('div');
|
||||
cursor.id = CURSOR_ID;
|
||||
Object.assign(cursor.style, {
|
||||
position: 'fixed',
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundImage: 'url("data:image/svg+xml;utf8,<svg xmlns=\\'http://www.w3.org/2000/svg\\' viewBox=\\'0 0 24 24\\' fill=\\'black\\' stroke=\\'white\\' stroke-width=\\'1\\' stroke-linejoin=\\'round\\' stroke-linecap=\\'round\\'><polygon points=\\'2,2 2,22 8,16 14,22 17,19 11,13 20,13\\'/></svg>")',
|
||||
backgroundSize: 'cover',
|
||||
pointerEvents: 'none',
|
||||
zIndex: '99999',
|
||||
transform: 'translate(-2px, -2px)',
|
||||
});
|
||||
|
||||
document.body.appendChild(cursor);
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
cursor.style.top = e.clientY + "px";
|
||||
cursor.style.left = e.clientX + "px";
|
||||
});
|
||||
}
|
||||
|
||||
// Use requestAnimationFrame for early execution
|
||||
requestAnimationFrame(function checkBody() {
|
||||
if (document.body) {
|
||||
initCursor();
|
||||
} else {
|
||||
requestAnimationFrame(checkBody);
|
||||
}
|
||||
});
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
page = context.pages[0]
|
||||
page.on("close", self._handle_page_close)
|
||||
|
||||
page.goto("https://bing.com")
|
||||
|
||||
return browser, page
|
||||
|
||||
def _handle_new_page(self, page: Page):
|
||||
"""Handle the creation of a new page."""
|
||||
print("New page created")
|
||||
self._page = page
|
||||
page.on("close", self._handle_page_close)
|
||||
|
||||
def _handle_page_close(self, page: Page):
|
||||
"""Handle the closure of a page."""
|
||||
print("Page closed")
|
||||
if self._page == page:
|
||||
if self._browser.contexts[0].pages:
|
||||
self._page = self._browser.contexts[0].pages[-1]
|
||||
else:
|
||||
print("Warning: All pages have been closed.")
|
||||
self._page = None
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""
|
||||
Clean up resources when exiting the context manager.
|
||||
|
||||
Args:
|
||||
exc_type: The type of the exception that caused the context to be exited.
|
||||
exc_val: The exception instance that caused the context to be exited.
|
||||
exc_tb: A traceback object encapsulating the call stack at the point where the exception occurred.
|
||||
"""
|
||||
if self._page:
|
||||
self._page.close()
|
||||
if self._browser:
|
||||
self._browser.close()
|
||||
if self._playwright:
|
||||
self._playwright.stop()
|
||||
|
||||
if self.session:
|
||||
print(
|
||||
f"Session completed. View replay at https://browserbase.com/sessions/{self.session.id}"
|
||||
)
|
||||
|
||||
def screenshot(self) -> str:
|
||||
"""
|
||||
Capture a screenshot of the current viewport using CDP.
|
||||
|
||||
Returns:
|
||||
str: A base64 encoded string of the screenshot.
|
||||
"""
|
||||
try:
|
||||
# Get CDP session from the page
|
||||
cdp_session = self._page.context.new_cdp_session(self._page)
|
||||
|
||||
# Capture screenshot using CDP
|
||||
result = cdp_session.send(
|
||||
"Page.captureScreenshot", {"format": "png", "fromSurface": True}
|
||||
)
|
||||
|
||||
return result["data"]
|
||||
except PlaywrightError as error:
|
||||
print(
|
||||
f"CDP screenshot failed, falling back to standard screenshot: {error}"
|
||||
)
|
||||
return super().screenshot()
|
||||
@@ -1,174 +0,0 @@
|
||||
import subprocess
|
||||
import time
|
||||
import shlex
|
||||
|
||||
|
||||
class DockerComputer:
|
||||
def get_environment(self):
|
||||
return "linux"
|
||||
|
||||
def get_dimensions(self):
|
||||
return (1280, 720) # Default fallback; will be updated in __enter__.
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
container_name="cua-sample-app",
|
||||
image="ghcr.io/openai/openai-cua-sample-app:latest",
|
||||
display=":99",
|
||||
port_mapping="5900:5900",
|
||||
):
|
||||
self.container_name = container_name
|
||||
self.image = image
|
||||
self.display = display
|
||||
self.port_mapping = port_mapping
|
||||
|
||||
def __enter__(self):
|
||||
# Check if the container is running
|
||||
result = subprocess.run(
|
||||
["docker", "ps", "-q", "-f", f"name={self.container_name}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if not result.stdout.strip():
|
||||
raise RuntimeError(
|
||||
f"Container {self.container_name} is not running. Build and run with:\n"
|
||||
f"docker build -t {self.container_name} .\n"
|
||||
f"docker run --rm -it --name {self.container_name} "
|
||||
f"-p {self.port_mapping} -e DISPLAY={self.display} {self.container_name}"
|
||||
)
|
||||
|
||||
# Fetch display geometry
|
||||
geometry = self._exec(
|
||||
f"DISPLAY={self.display} xdotool getdisplaygeometry"
|
||||
).strip()
|
||||
if geometry:
|
||||
w, h = geometry.split()
|
||||
self.dimensions = (int(w), int(h))
|
||||
# print("Starting Docker container...")
|
||||
# # Run the container detached, removing it automatically when it stops
|
||||
# subprocess.check_call(
|
||||
# [
|
||||
# "docker",
|
||||
# "run",
|
||||
# "-d",
|
||||
# "--rm",
|
||||
# "--name",
|
||||
# self.container_name,
|
||||
# "-p",
|
||||
# self.port_mapping,
|
||||
# self.image,
|
||||
# ]
|
||||
# )
|
||||
# # Give the container a moment to start
|
||||
# time.sleep(3)
|
||||
# print("Entering DockerComputer context")
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
# print("Stopping Docker container...")
|
||||
# subprocess.check_call(["docker", "stop", self.container_name])
|
||||
# print("Exiting DockerComputer context")
|
||||
pass
|
||||
|
||||
def _exec(self, cmd: str) -> str:
|
||||
"""
|
||||
Run 'cmd' in the container.
|
||||
We wrap cmd in double quotes and escape any double quotes inside it,
|
||||
so spaces or quotes don't break the shell call.
|
||||
"""
|
||||
# Escape any existing double quotes in cmd
|
||||
safe_cmd = cmd.replace('"', '\\"')
|
||||
|
||||
# Then wrap the entire cmd in double quotes for `sh -c`
|
||||
docker_cmd = f'docker exec {self.container_name} sh -c "{safe_cmd}"'
|
||||
|
||||
return subprocess.check_output(docker_cmd, shell=True).decode(
|
||||
"utf-8", errors="ignore"
|
||||
)
|
||||
|
||||
def screenshot(self) -> str:
|
||||
"""
|
||||
Takes a screenshot with ImageMagick (import), returning base64-encoded PNG.
|
||||
Requires 'import'.
|
||||
"""
|
||||
# cmd = (
|
||||
# f"export DISPLAY={self.display} && "
|
||||
# "import -window root /tmp/screenshot.png && "
|
||||
# "base64 /tmp/screenshot.png"
|
||||
# )
|
||||
cmd = (
|
||||
f"export DISPLAY={self.display} && "
|
||||
"import -window root png:- | base64 -w 0"
|
||||
)
|
||||
|
||||
return self._exec(cmd)
|
||||
|
||||
def click(self, x: int, y: int, button: str = "left") -> None:
|
||||
button_map = {"left": 1, "middle": 2, "right": 3}
|
||||
b = button_map.get(button, 1)
|
||||
self._exec(f"DISPLAY={self.display} xdotool mousemove {x} {y} click {b}")
|
||||
|
||||
def double_click(self, x: int, y: int) -> None:
|
||||
self._exec(
|
||||
f"DISPLAY={self.display} xdotool mousemove {x} {y} click --repeat 2 1"
|
||||
)
|
||||
|
||||
def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
|
||||
"""
|
||||
For simple vertical scrolling: xdotool click 4 (scroll up) or 5 (scroll down).
|
||||
"""
|
||||
self._exec(f"DISPLAY={self.display} xdotool mousemove {x} {y}")
|
||||
clicks = abs(scroll_y)
|
||||
button = 4 if scroll_y < 0 else 5
|
||||
for _ in range(clicks):
|
||||
self._exec(f"DISPLAY={self.display} xdotool click {button}")
|
||||
|
||||
def type(self, text: str) -> None:
|
||||
"""
|
||||
Type the given text via xdotool, preserving spaces and quotes.
|
||||
"""
|
||||
# Escape single quotes in the user text: ' -> '\'\''
|
||||
safe_text = text.replace("'", "'\\''")
|
||||
# Then wrap everything in single quotes for xdotool
|
||||
cmd = f"DISPLAY={self.display} xdotool type -- '{safe_text}'"
|
||||
self._exec(cmd)
|
||||
|
||||
def wait(self, ms: int = 1000) -> None:
|
||||
time.sleep(ms / 1000)
|
||||
|
||||
def move(self, x: int, y: int) -> None:
|
||||
self._exec(f"DISPLAY={self.display} xdotool mousemove {x} {y}")
|
||||
|
||||
def keypress(self, keys: list[str]) -> None:
|
||||
mapping = {
|
||||
"ENTER": "Return",
|
||||
"LEFT": "Left",
|
||||
"RIGHT": "Right",
|
||||
"UP": "Up",
|
||||
"DOWN": "Down",
|
||||
"ESC": "Escape",
|
||||
"SPACE": "space",
|
||||
"BACKSPACE": "BackSpace",
|
||||
"TAB": "Tab",
|
||||
}
|
||||
mapped_keys = [mapping.get(key, key) for key in keys]
|
||||
combo = "+".join(mapped_keys)
|
||||
self._exec(f"DISPLAY={self.display} xdotool key {combo}")
|
||||
|
||||
def drag(self, path: list[dict[str, int]]) -> None:
|
||||
if not path:
|
||||
return
|
||||
start_x = path[0]["x"]
|
||||
start_y = path[0]["y"]
|
||||
self._exec(
|
||||
f"DISPLAY={self.display} xdotool mousemove {start_x} {start_y} mousedown 1"
|
||||
)
|
||||
for point in path[1:]:
|
||||
self._exec(
|
||||
f"DISPLAY={self.display} xdotool mousemove {point['x']} {point['y']}"
|
||||
)
|
||||
self._exec(f"DISPLAY={self.display} xdotool mouseup 1")
|
||||
|
||||
def get_current_url(self):
|
||||
return None
|
||||
@@ -1,165 +0,0 @@
|
||||
import os
|
||||
from playwright.sync_api import Browser, Page
|
||||
from ..shared.base_playwright import BasePlaywrightComputer
|
||||
|
||||
|
||||
class LocalPlaywrightBrowser(BasePlaywrightComputer):
|
||||
"""Launches a local Chromium instance using Playwright."""
|
||||
|
||||
def __init__(self, headless: bool = False):
|
||||
super().__init__()
|
||||
self.headless = headless
|
||||
|
||||
def _get_browser_and_page(self) -> tuple[Browser, Page]:
|
||||
width, height = self.get_dimensions()
|
||||
launch_args = [
|
||||
f"--window-size={width},{height}",
|
||||
"--disable-extensions",
|
||||
"--disable-file-system",
|
||||
]
|
||||
browser = self._playwright.chromium.launch(
|
||||
chromium_sandbox=False,
|
||||
headless=self.headless,
|
||||
args=launch_args,
|
||||
env={"DISPLAY": ":0"},
|
||||
)
|
||||
|
||||
context = browser.new_context(
|
||||
user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
locale="en-US",
|
||||
timezone_id="UTC"
|
||||
)
|
||||
|
||||
# Add event listeners for page creation and closure
|
||||
context.on("page", self._handle_new_page)
|
||||
|
||||
page = context.new_page()
|
||||
page.set_viewport_size({"width": width, "height": height})
|
||||
page.on("close", self._handle_page_close)
|
||||
|
||||
# Add logging for debugging
|
||||
page.on("console", lambda msg: print(f"Browser console: {msg.text}"))
|
||||
page.on("pageerror", lambda err: print(f"Page error: {err}"))
|
||||
|
||||
target_url = os.environ.get("TARGET_URL", "https://grafana.com/docs/")
|
||||
grafana_username = os.environ.get("GRAFANA_USERNAME")
|
||||
grafana_password = os.environ.get("GRAFANA_PASSWORD")
|
||||
|
||||
# If credentials provided, log in first
|
||||
if grafana_username and grafana_password:
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
base_url = f"{urlparse(target_url).scheme}://{urlparse(target_url).netloc}"
|
||||
login_url = urljoin(base_url, "/login")
|
||||
|
||||
print(f"Logging in to: {login_url}")
|
||||
page.goto(login_url, timeout=60000)
|
||||
|
||||
print(f"Page loaded, current URL: {page.url}")
|
||||
print(f"Page title: {page.title()}")
|
||||
|
||||
try:
|
||||
# Wait for login form - try multiple selector strategies
|
||||
print("Waiting for login form to appear...")
|
||||
|
||||
# Try to find by placeholder first (more reliable for older versions)
|
||||
username_field = page.get_by_placeholder("email or username")
|
||||
password_field = page.get_by_placeholder("password", exact=True)
|
||||
|
||||
username_field.wait_for(state="visible", timeout=60000)
|
||||
print("Login form detected")
|
||||
|
||||
# Take screenshot after form is visible
|
||||
screenshot_path = os.environ.get("GITHUB_WORKSPACE", ".") + "/login_page.png"
|
||||
page.screenshot(path=screenshot_path)
|
||||
print(f"Screenshot saved as {screenshot_path}")
|
||||
|
||||
# Fill credentials using placeholder selectors
|
||||
print(f"Filling username (length: {len(grafana_username)})")
|
||||
username_field.fill(grafana_username)
|
||||
|
||||
print(f"Filling password (length: {len(grafana_password)})")
|
||||
password_field.fill(grafana_password)
|
||||
|
||||
print("Credentials filled successfully")
|
||||
|
||||
# Click login button by text
|
||||
print("Clicking login button...")
|
||||
page.get_by_role("button", name="Log in").click()
|
||||
print("Login form submitted")
|
||||
|
||||
# Wait for login to complete
|
||||
print("Waiting for post-login navigation...")
|
||||
|
||||
# Try to wait for multiple possible indicators of successful login
|
||||
# The page might redirect to setup guide, dashboard, or other pages
|
||||
try:
|
||||
# Wait for either: navigation away from login OR any logged-in UI element
|
||||
page.locator('body:not(:has-text("Welcome to Grafana Cloud"))').or_(
|
||||
page.locator('[aria-label="Profile"]')
|
||||
).or_(
|
||||
page.locator('a:has-text("Home")')
|
||||
).first.wait_for(state="attached", timeout=15000)
|
||||
|
||||
print(f"Post-login navigation detected, current URL: {page.url}")
|
||||
|
||||
# Verify we actually left the login page
|
||||
if "/login" in page.url:
|
||||
raise Exception("Still on login page after navigation")
|
||||
|
||||
except Exception as wait_err:
|
||||
print(f"Login completion wait failed: {wait_err}")
|
||||
if "/login" in page.url:
|
||||
raise Exception(f"Login failed - still on login page: {page.url}")
|
||||
else:
|
||||
print(f"Continuing anyway - URL shows we're logged in: {page.url}")
|
||||
|
||||
print(f"Login successful, current URL: {page.url}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Login failed: {e}")
|
||||
print(f"Current URL at error: {page.url}")
|
||||
print(f"Page title at error: {page.title()}")
|
||||
|
||||
# Get page content for debugging
|
||||
try:
|
||||
body_text = page.locator('body').text_content()
|
||||
print(f"Page body text (first 500 chars): {body_text[:500] if body_text else 'No body text'}")
|
||||
except Exception as content_err:
|
||||
print(f"Could not get page content: {content_err}")
|
||||
|
||||
error_screenshot_path = os.environ.get("GITHUB_WORKSPACE", ".") + "/login_error.png"
|
||||
page.screenshot(path=error_screenshot_path)
|
||||
print(f"Error screenshot saved as {error_screenshot_path}")
|
||||
raise
|
||||
|
||||
# Set up console and error logging
|
||||
page.on("console", lambda msg: print(f"Console [{msg.type}]: {msg.text}"))
|
||||
page.on("pageerror", lambda err: print(f"Page error: {err}"))
|
||||
|
||||
print(f"Navigating to: {target_url}")
|
||||
page.goto(target_url, wait_until="domcontentloaded", timeout=180000)
|
||||
print(f"Page loaded, URL: {page.url}")
|
||||
|
||||
# Wait a bit for Grafana to initialize
|
||||
print("Waiting for Grafana to initialize...")
|
||||
page.wait_for_timeout(10000) # Wait 10 seconds for app to settle
|
||||
print(f"Final URL after waiting: {page.url}")
|
||||
|
||||
return browser, page
|
||||
|
||||
def _handle_new_page(self, page: Page):
|
||||
"""Handle the creation of a new page."""
|
||||
print("New page created")
|
||||
self._page = page
|
||||
page.on("close", self._handle_page_close)
|
||||
|
||||
def _handle_page_close(self, page: Page):
|
||||
"""Handle the closure of a page."""
|
||||
print("Page closed")
|
||||
if self._page == page:
|
||||
if self._browser.contexts[0].pages:
|
||||
self._page = self._browser.contexts[0].pages[-1]
|
||||
else:
|
||||
print("Warning: All pages have been closed.")
|
||||
self._page = None
|
||||
@@ -1,220 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
from dotenv import load_dotenv
|
||||
from scrapybara import Scrapybara
|
||||
from playwright.sync_api import sync_playwright, Browser, Page
|
||||
from utils import BLOCKED_DOMAINS
|
||||
|
||||
load_dotenv()
|
||||
|
||||
CUA_KEY_TO_SCRAPYBARA_KEY = {
|
||||
"/": "slash",
|
||||
"\\": "backslash",
|
||||
"arrowdown": "Down",
|
||||
"arrowleft": "Left",
|
||||
"arrowright": "Right",
|
||||
"arrowup": "Up",
|
||||
"backspace": "BackSpace",
|
||||
"capslock": "Caps_Lock",
|
||||
"cmd": "Meta_L",
|
||||
"delete": "Delete",
|
||||
"end": "End",
|
||||
"enter": "Return",
|
||||
"esc": "Escape",
|
||||
"home": "Home",
|
||||
"insert": "Insert",
|
||||
"option": "Alt_L",
|
||||
"pagedown": "Page_Down",
|
||||
"pageup": "Page_Up",
|
||||
"tab": "Tab",
|
||||
"win": "Meta_L",
|
||||
}
|
||||
|
||||
|
||||
class ScrapybaraBrowser:
|
||||
"""
|
||||
Scrapybara provides virtual desktops and browsers in the cloud. https://scrapybara.com
|
||||
You can try OpenAI CUA for free at https://computer.new or read our CUA Quickstart at https://computer.new/cua.
|
||||
"""
|
||||
|
||||
def get_environment(self):
|
||||
return "browser"
|
||||
|
||||
def get_dimensions(self):
|
||||
return (1024, 768)
|
||||
|
||||
def __init__(self):
|
||||
self.client = Scrapybara(api_key=os.getenv("SCRAPYBARA_API_KEY"))
|
||||
self._playwright = None
|
||||
self._browser: Browser | None = None
|
||||
self._page: Page | None = None
|
||||
|
||||
def __enter__(self):
|
||||
print("Starting scrapybara browser")
|
||||
blocked_domains = [
|
||||
domain.replace("https://", "").replace("www.", "")
|
||||
for domain in BLOCKED_DOMAINS
|
||||
]
|
||||
self.instance = self.client.start_browser(blocked_domains=blocked_domains)
|
||||
print("Scrapybara browser started ₍ᐢ•(ܫ)•ᐢ₎")
|
||||
print(
|
||||
f"You can view and interact with the stream at {self.instance.get_stream_url().stream_url}"
|
||||
)
|
||||
self._playwright = sync_playwright().start()
|
||||
self._browser = self._playwright.chromium.connect_over_cdp(
|
||||
self.instance.get_cdp_url().cdp_url
|
||||
)
|
||||
self._page = self._browser.contexts[0].pages[0]
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
print("Stopping scrapybara browser")
|
||||
self.instance.stop()
|
||||
print("Scrapybara browser stopped ₍ᐢ-(ェ)-ᐢ₎")
|
||||
|
||||
def goto(self, url: str) -> None:
|
||||
self._page.goto(url)
|
||||
|
||||
def get_current_url(self) -> str:
|
||||
return self.instance.get_current_url().current_url
|
||||
|
||||
def screenshot(self) -> str:
|
||||
return self.instance.screenshot().base_64_image
|
||||
|
||||
def click(self, x: int, y: int, button: str = "left") -> None:
|
||||
button = "middle" if button == "wheel" else button
|
||||
self.instance.computer(
|
||||
action="click_mouse",
|
||||
click_type="click",
|
||||
button=button,
|
||||
coordinates=[x, y],
|
||||
num_clicks=1,
|
||||
)
|
||||
|
||||
def double_click(self, x: int, y: int) -> None:
|
||||
self.instance.computer(
|
||||
action="click_mouse",
|
||||
click_type="click",
|
||||
button="left",
|
||||
coordinates=[x, y],
|
||||
num_clicks=2,
|
||||
)
|
||||
|
||||
def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
|
||||
self.instance.computer(
|
||||
action="scroll",
|
||||
coordinates=[x, y],
|
||||
delta_x=scroll_x // 20,
|
||||
delta_y=scroll_y // 20,
|
||||
)
|
||||
|
||||
def type(self, text: str) -> None:
|
||||
self.instance.computer(action="type_text", text=text)
|
||||
|
||||
def wait(self, ms: int = 1000) -> None:
|
||||
time.sleep(ms / 1000)
|
||||
# Scrapybara also has `self.instance.computer(action="wait", duration=ms / 1000)`
|
||||
|
||||
def move(self, x: int, y: int) -> None:
|
||||
self.instance.computer(action="move_mouse", coordinates=[x, y])
|
||||
|
||||
def keypress(self, keys: list[str]) -> None:
|
||||
mapped_keys = [
|
||||
CUA_KEY_TO_SCRAPYBARA_KEY.get(key.lower(), key.lower()) for key in keys
|
||||
]
|
||||
self.instance.computer(action="press_key", keys=mapped_keys)
|
||||
|
||||
def drag(self, path: list[dict[str, int]]) -> None:
|
||||
if not path:
|
||||
return
|
||||
path = [[point["x"], point["y"]] for point in path]
|
||||
self.instance.computer(action="drag_mouse", path=path)
|
||||
|
||||
|
||||
class ScrapybaraUbuntu:
|
||||
"""
|
||||
Scrapybara provides virtual desktops and browsers in the cloud.
|
||||
You can try OpenAI CUA for free at https://computer.new or read our CUA Quickstart at https://computer.new/cua.
|
||||
"""
|
||||
|
||||
def get_environment(self):
|
||||
return "linux"
|
||||
|
||||
def get_dimensions(self):
|
||||
return (1024, 768)
|
||||
|
||||
def __init__(self):
|
||||
self.client = Scrapybara(api_key=os.getenv("SCRAPYBARA_API_KEY"))
|
||||
|
||||
def __enter__(self):
|
||||
print("Starting Scrapybara Ubuntu instance")
|
||||
blocked_domains = [
|
||||
domain.replace("https://", "").replace("www.", "")
|
||||
for domain in BLOCKED_DOMAINS
|
||||
]
|
||||
self.instance = self.client.start_ubuntu(blocked_domains=blocked_domains)
|
||||
print("Scrapybara Ubuntu instance started ₍ᐢ•(ܫ)•ᐢ₎")
|
||||
print(
|
||||
f"You can view and interact with the stream at {self.instance.get_stream_url().stream_url}"
|
||||
)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
print("Stopping Scrapybara Ubuntu instance")
|
||||
self.instance.stop()
|
||||
print("Scrapybara Ubuntu instance stopped ₍ᐢ-(ェ)-ᐢ₎")
|
||||
|
||||
def screenshot(self) -> str:
|
||||
return self.instance.screenshot().base_64_image
|
||||
|
||||
def click(self, x: int, y: int, button: str = "left") -> None:
|
||||
button = "middle" if button == "wheel" else button
|
||||
self.instance.computer(
|
||||
action="click_mouse",
|
||||
click_type="click",
|
||||
button=button,
|
||||
coordinates=[x, y],
|
||||
num_clicks=1,
|
||||
)
|
||||
|
||||
def double_click(self, x: int, y: int) -> None:
|
||||
self.instance.computer(
|
||||
action="click_mouse",
|
||||
click_type="click",
|
||||
button="left",
|
||||
coordinates=[x, y],
|
||||
num_clicks=2,
|
||||
)
|
||||
|
||||
def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
|
||||
self.instance.computer(
|
||||
action="scroll",
|
||||
coordinates=[x, y],
|
||||
delta_x=scroll_x // 20,
|
||||
delta_y=scroll_y // 20,
|
||||
)
|
||||
|
||||
def type(self, text: str) -> None:
|
||||
self.instance.computer(action="type_text", text=text)
|
||||
|
||||
def wait(self, ms: int = 1000) -> None:
|
||||
time.sleep(ms / 1000)
|
||||
# Scrapybara also has `self.instance.computer(action="wait", duration=ms / 1000)`
|
||||
|
||||
def move(self, x: int, y: int) -> None:
|
||||
self.instance.computer(action="move_mouse", coordinates=[x, y])
|
||||
|
||||
def keypress(self, keys: list[str]) -> None:
|
||||
mapped_keys = [
|
||||
CUA_KEY_TO_SCRAPYBARA_KEY.get(key.lower(), key.lower()) for key in keys
|
||||
]
|
||||
self.instance.computer(action="press_key", keys=mapped_keys)
|
||||
|
||||
def drag(self, path: list[dict[str, int]]) -> None:
|
||||
if not path:
|
||||
return
|
||||
path = [[point["x"], point["y"]] for point in path]
|
||||
self.instance.computer(action="drag_mouse", path=path)
|
||||
|
||||
def get_current_url(self):
|
||||
return None
|
||||
@@ -1,154 +0,0 @@
|
||||
import time
|
||||
import base64
|
||||
from typing import List, Dict, Literal
|
||||
from playwright.sync_api import sync_playwright, Browser, Page
|
||||
from utils import check_blocklisted_url
|
||||
|
||||
# Optional: key mapping if your model uses "CUA" style keys
|
||||
CUA_KEY_TO_PLAYWRIGHT_KEY = {
|
||||
"/": "Divide",
|
||||
"\\": "Backslash",
|
||||
"alt": "Alt",
|
||||
"arrowdown": "ArrowDown",
|
||||
"arrowleft": "ArrowLeft",
|
||||
"arrowright": "ArrowRight",
|
||||
"arrowup": "ArrowUp",
|
||||
"backspace": "Backspace",
|
||||
"capslock": "CapsLock",
|
||||
"cmd": "Meta",
|
||||
"ctrl": "Control",
|
||||
"delete": "Delete",
|
||||
"end": "End",
|
||||
"enter": "Enter",
|
||||
"esc": "Escape",
|
||||
"home": "Home",
|
||||
"insert": "Insert",
|
||||
"option": "Alt",
|
||||
"pagedown": "PageDown",
|
||||
"pageup": "PageUp",
|
||||
"shift": "Shift",
|
||||
"space": " ",
|
||||
"super": "Meta",
|
||||
"tab": "Tab",
|
||||
"win": "Meta",
|
||||
}
|
||||
|
||||
|
||||
class BasePlaywrightComputer:
|
||||
"""
|
||||
Abstract base for Playwright-based computers:
|
||||
|
||||
- Subclasses override `_get_browser_and_page()` to do local or remote connection,
|
||||
returning (Browser, Page).
|
||||
- This base class handles context creation (`__enter__`/`__exit__`),
|
||||
plus standard "Computer" actions like click, scroll, etc.
|
||||
- We also have extra browser actions: `goto(url)` and `back()`.
|
||||
"""
|
||||
|
||||
def get_environment(self):
|
||||
return "browser"
|
||||
|
||||
def get_dimensions(self):
|
||||
return (1024, 768)
|
||||
|
||||
def __init__(self):
|
||||
self._playwright = None
|
||||
self._browser: Browser | None = None
|
||||
self._page: Page | None = None
|
||||
|
||||
def __enter__(self):
|
||||
# Start Playwright and call the subclass hook for getting browser/page
|
||||
self._playwright = sync_playwright().start()
|
||||
self._browser, self._page = self._get_browser_and_page()
|
||||
|
||||
# Set up network interception to flag URLs matching domains in BLOCKED_DOMAINS
|
||||
def handle_route(route, request):
|
||||
|
||||
url = request.url
|
||||
if check_blocklisted_url(url):
|
||||
print(f"Flagging blocked domain: {url}")
|
||||
route.abort()
|
||||
else:
|
||||
route.continue_()
|
||||
|
||||
self._page.route("**/*", handle_route)
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self._browser:
|
||||
self._browser.close()
|
||||
if self._playwright:
|
||||
self._playwright.stop()
|
||||
|
||||
def get_current_url(self) -> str:
|
||||
return self._page.url
|
||||
|
||||
# --- Common "Computer" actions ---
|
||||
def screenshot(self) -> str:
|
||||
"""Capture only the viewport (not full_page)."""
|
||||
png_bytes = self._page.screenshot(full_page=False)
|
||||
return base64.b64encode(png_bytes).decode("utf-8")
|
||||
|
||||
def click(self, x: int, y: int, button: str = "left") -> None:
|
||||
match button:
|
||||
case "back":
|
||||
self.back()
|
||||
case "forward":
|
||||
self.forward()
|
||||
case "wheel":
|
||||
self._page.mouse.wheel(x, y)
|
||||
case _:
|
||||
button_mapping = {"left": "left", "right": "right"}
|
||||
button_type = button_mapping.get(button, "left")
|
||||
self._page.mouse.click(x, y, button=button_type)
|
||||
|
||||
def double_click(self, x: int, y: int) -> None:
|
||||
self._page.mouse.dblclick(x, y)
|
||||
|
||||
def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
|
||||
self._page.mouse.move(x, y)
|
||||
self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
|
||||
|
||||
def type(self, text: str) -> None:
|
||||
self._page.keyboard.type(text)
|
||||
|
||||
def wait(self, ms: int = 1000) -> None:
|
||||
time.sleep(ms / 1000)
|
||||
|
||||
def move(self, x: int, y: int) -> None:
|
||||
self._page.mouse.move(x, y)
|
||||
|
||||
def keypress(self, keys: List[str]) -> None:
|
||||
mapped_keys = [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys]
|
||||
for key in mapped_keys:
|
||||
self._page.keyboard.down(key)
|
||||
for key in reversed(mapped_keys):
|
||||
self._page.keyboard.up(key)
|
||||
|
||||
def drag(self, path: List[Dict[str, int]]) -> None:
|
||||
if not path:
|
||||
return
|
||||
self._page.mouse.move(path[0]["x"], path[0]["y"])
|
||||
self._page.mouse.down()
|
||||
for point in path[1:]:
|
||||
self._page.mouse.move(point["x"], point["y"])
|
||||
self._page.mouse.up()
|
||||
|
||||
# --- Extra browser-oriented actions ---
|
||||
def goto(self, url: str) -> None:
|
||||
try:
|
||||
return self._page.goto(url)
|
||||
except Exception as e:
|
||||
print(f"Error navigating to {url}: {e}")
|
||||
|
||||
def back(self) -> None:
|
||||
return self._page.go_back()
|
||||
|
||||
def forward(self) -> None:
|
||||
return self._page.go_forward()
|
||||
|
||||
# --- Subclass hook ---
|
||||
def _get_browser_and_page(self) -> tuple[Browser, Page]:
|
||||
"""Subclasses must implement, returning (Browser, Page)."""
|
||||
raise NotImplementedError
|
||||
@@ -1,24 +0,0 @@
|
||||
openai>=1.56.0
|
||||
annotated-types==0.7.0
|
||||
anyio==4.8.0
|
||||
browserbase==1.2.0
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
distro==1.9.0
|
||||
greenlet==3.1.1
|
||||
h11==0.14.0
|
||||
httpcore==1.0.7
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
jiter==0.8.2
|
||||
pillow==11.1.0
|
||||
playwright==1.50.0
|
||||
pydantic==2.10.6
|
||||
pydantic_core==2.27.2
|
||||
pyee==12.1.1
|
||||
python-dotenv==1.0.1
|
||||
requests==2.32.3
|
||||
scrapybara>=2.3.6
|
||||
sniffio==1.3.1
|
||||
typing_extensions==4.12.2
|
||||
urllib3==2.3.0
|
||||
156
.github/actions/usability-review-agent/run_agent.py
vendored
156
.github/actions/usability-review-agent/run_agent.py
vendored
@@ -1,156 +0,0 @@
|
||||
import os
|
||||
import base64
|
||||
from computers import Computer
|
||||
from computers.default import LocalPlaywrightBrowser
|
||||
from utils import create_response, check_blocklisted_url
|
||||
|
||||
|
||||
def load_prompt():
|
||||
"""Load prompt from prompt file (defaults to prompt.txt, can be overridden with PROMPT_FILE env var)."""
|
||||
prompt_file = os.environ.get("PROMPT_FILE")
|
||||
if not prompt_file:
|
||||
prompt_file = os.path.join(os.path.dirname(__file__), "prompt.txt")
|
||||
|
||||
if not os.path.exists(prompt_file):
|
||||
raise FileNotFoundError(f"Prompt file not found: {prompt_file}")
|
||||
|
||||
with open(prompt_file, "r", encoding="utf-8") as f:
|
||||
return f.read().strip()
|
||||
|
||||
def acknowledge_safety_check_callback(message: str) -> bool:
|
||||
# Auto-approve in CI/non-interactive environments
|
||||
print(f"Safety Check Warning: {message} - Auto-approving in CI mode")
|
||||
return True
|
||||
|
||||
|
||||
def handle_item(item, computer: Computer):
|
||||
"""Handle each item; may cause a computer action + screenshot."""
|
||||
if item["type"] == "message": # print messages
|
||||
print(item["content"][0]["text"])
|
||||
|
||||
if item["type"] == "computer_call": # perform computer actions
|
||||
action = item["action"]
|
||||
action_type = action["type"]
|
||||
action_args = {k: v for k, v in action.items() if k != "type"}
|
||||
print(f"{action_type}({action_args})")
|
||||
|
||||
# give our computer environment action to perform
|
||||
getattr(computer, action_type)(**action_args)
|
||||
|
||||
screenshot_base64 = computer.screenshot()
|
||||
|
||||
pending_checks = item.get("pending_safety_checks", [])
|
||||
for check in pending_checks:
|
||||
if not acknowledge_safety_check_callback(check["message"]):
|
||||
raise ValueError(f"Safety check failed: {check['message']}")
|
||||
|
||||
# return value informs model of the latest screenshot
|
||||
call_output = {
|
||||
"type": "computer_call_output",
|
||||
"call_id": item["call_id"],
|
||||
"acknowledged_safety_checks": pending_checks,
|
||||
"output": {
|
||||
"type": "input_image",
|
||||
"image_url": f"data:image/png;base64,{screenshot_base64}",
|
||||
},
|
||||
}
|
||||
|
||||
# additional URL safety checks for browser environments
|
||||
if computer.get_environment() == "browser":
|
||||
current_url = computer.get_current_url()
|
||||
call_output["output"]["current_url"] = current_url
|
||||
check_blocklisted_url(current_url)
|
||||
|
||||
return [call_output]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the CUA (Computer Use Assistant) loop, using Local Playwright."""
|
||||
output_text_path = os.environ.get("OUTPUT_TEXT_PATH", "output.txt")
|
||||
screenshot_path = os.environ.get("SCREENSHOT_PATH", "output.png")
|
||||
all_messages = [] # Collect all model messages
|
||||
last_screenshot_base64 = None
|
||||
|
||||
with LocalPlaywrightBrowser(headless=True) as computer:
|
||||
dimensions = computer.get_dimensions()
|
||||
tools = [
|
||||
{
|
||||
"type": "computer-preview",
|
||||
"display_width": dimensions[0],
|
||||
"display_height": dimensions[1],
|
||||
"environment": computer.get_environment(),
|
||||
}
|
||||
]
|
||||
|
||||
items = []
|
||||
# Load the task prompt from prompt.txt
|
||||
user_input = load_prompt()
|
||||
items.append({"role": "user", "content": user_input})
|
||||
|
||||
while True: # keep looping until we get a final response
|
||||
response = create_response(
|
||||
model="computer-use-preview",
|
||||
input=items,
|
||||
tools=tools,
|
||||
truncation="auto",
|
||||
)
|
||||
|
||||
if "output" not in response:
|
||||
print(response)
|
||||
raise ValueError("No output from model")
|
||||
|
||||
items += response["output"]
|
||||
|
||||
for item in response["output"]:
|
||||
# Collect all message output from the model
|
||||
if item.get("type") == "message":
|
||||
content = item.get("content", [])
|
||||
for content_item in content:
|
||||
if isinstance(content_item, dict) and "text" in content_item:
|
||||
text = content_item["text"]
|
||||
all_messages.append(text)
|
||||
|
||||
result = handle_item(item, computer)
|
||||
items += result
|
||||
|
||||
# Capture last screenshot from computer_call outputs
|
||||
if result and len(result) > 0:
|
||||
for output_item in result:
|
||||
if output_item.get("type") == "computer_call_output":
|
||||
output = output_item.get("output", {})
|
||||
if output.get("type") == "input_image":
|
||||
image_url = output.get("image_url", "")
|
||||
if image_url.startswith("data:image/png;base64,"):
|
||||
last_screenshot_base64 = image_url.split(",", 1)[1]
|
||||
|
||||
if items[-1].get("role") == "assistant":
|
||||
break
|
||||
|
||||
# Take one final screenshot before closing
|
||||
if not last_screenshot_base64:
|
||||
try:
|
||||
last_screenshot_base64 = computer.screenshot() # Returns base64 string directly
|
||||
except:
|
||||
pass
|
||||
|
||||
# Save the last screenshot to file
|
||||
if last_screenshot_base64:
|
||||
os.makedirs(os.path.dirname(screenshot_path) or ".", exist_ok=True)
|
||||
with open(screenshot_path, "wb") as f:
|
||||
f.write(base64.b64decode(last_screenshot_base64))
|
||||
|
||||
# Save all model output messages to file
|
||||
os.makedirs(os.path.dirname(output_text_path) or ".", exist_ok=True)
|
||||
with open(output_text_path, "w") as f:
|
||||
if all_messages:
|
||||
# Join all messages with double newlines for readability
|
||||
f.write("\n\n".join(all_messages))
|
||||
else:
|
||||
# Fallback: save error message if no messages were captured
|
||||
f.write("No model output messages were captured.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
37
.github/actions/usability-review-agent/utils.py
vendored
37
.github/actions/usability-review-agent/utils.py
vendored
@@ -1,37 +0,0 @@
|
||||
import os
|
||||
import requests
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# Example blocked domains - customize for your use case
|
||||
BLOCKED_DOMAINS = [
|
||||
"example-malicious-site.com",
|
||||
]
|
||||
|
||||
|
||||
def create_response(**kwargs):
|
||||
url = "https://api.openai.com/v1/responses"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
openai_org = os.getenv("OPENAI_ORG")
|
||||
if openai_org:
|
||||
headers["Openai-Organization"] = openai_org
|
||||
|
||||
response = requests.post(url, headers=headers, json=kwargs)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"Error: {response.status_code} {response.text}")
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def check_blocklisted_url(url: str) -> None:
|
||||
"""Raise ValueError if the given URL (including subdomains) is in the blocklist."""
|
||||
hostname = urlparse(url).hostname or ""
|
||||
if any(
|
||||
hostname == blocked or hostname.endswith(f".{blocked}")
|
||||
for blocked in BLOCKED_DOMAINS
|
||||
):
|
||||
raise ValueError(f"Blocked URL: {url}")
|
||||
189
.github/scripts/README-metrics.md
vendored
189
.github/scripts/README-metrics.md
vendored
@@ -1,189 +0,0 @@
|
||||
# BabyBot Metrics Export
|
||||
|
||||
Export and analyze BabyBot usability review metrics from GitHub.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Export to both CSV and JSON
|
||||
python .github/scripts/export-babybot-metrics.py
|
||||
|
||||
# Export only CSV
|
||||
python .github/scripts/export-babybot-metrics.py --format csv --output my-metrics
|
||||
|
||||
# Export only JSON
|
||||
python .github/scripts/export-babybot-metrics.py --format json
|
||||
```
|
||||
|
||||
## Output Files
|
||||
|
||||
### CSV Format (`babybot-metrics.csv`)
|
||||
Detailed row-per-comment data, suitable for Excel, Google Sheets, or data analysis tools.
|
||||
|
||||
| Column | Description |
|
||||
|--------|-------------|
|
||||
| comment_id | GitHub comment ID |
|
||||
| pr_number | PR number |
|
||||
| comment_type | `resolvable` or `general` |
|
||||
| severity | Critical, Major, or Minor |
|
||||
| confidence | Low, Medium, or High |
|
||||
| file | File path (for resolvable comments) |
|
||||
| created_at | Timestamp |
|
||||
| url | Link to comment |
|
||||
| total_reactions | Number of reactions (👍, ❤️, 🎉, etc.) |
|
||||
| has_reply | Boolean - whether comment has replies |
|
||||
| engagement_score | Calculated engagement score |
|
||||
|
||||
### JSON Format (`babybot-metrics.json`)
|
||||
Aggregated metrics with summaries by PR, severity, and confidence.
|
||||
|
||||
```json
|
||||
{
|
||||
"total_comments": 42,
|
||||
"resolvable_comments": 38,
|
||||
"general_comments": 4,
|
||||
"by_severity": {
|
||||
"Critical": 5,
|
||||
"Major": 20,
|
||||
"Minor": 17
|
||||
},
|
||||
"by_confidence": {
|
||||
"High": 10,
|
||||
"Medium": 25,
|
||||
"Low": 7
|
||||
},
|
||||
"engagement": {
|
||||
"comments_with_reactions": 15,
|
||||
"comments_with_replies": 8,
|
||||
"total_reactions": 45,
|
||||
"avg_reactions_per_comment": 1.07,
|
||||
"engagement_rate": 35.7
|
||||
},
|
||||
"by_pr": {
|
||||
"114646": {
|
||||
"count": 3,
|
||||
"severities": {"Major": 2, "Minor": 1},
|
||||
"engaged": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dashboard Integration Options
|
||||
|
||||
### 1. Google Sheets Dashboard
|
||||
```bash
|
||||
# Export CSV and upload to Google Sheets
|
||||
python .github/scripts/export-babybot-metrics.py --format csv
|
||||
# Upload babybot-metrics.csv to Google Sheets
|
||||
# Create pivot tables and charts
|
||||
```
|
||||
|
||||
### 2. Grafana Dashboard
|
||||
```bash
|
||||
# Export JSON and serve via HTTP
|
||||
python .github/scripts/export-babybot-metrics.py --format json
|
||||
# Use JSON API data source in Grafana
|
||||
```
|
||||
|
||||
### 3. Automated Weekly Reports (GitHub Actions)
|
||||
See `.github/workflows/babybot-weekly-report.yml` (create this workflow)
|
||||
|
||||
### 4. Tableau/Power BI
|
||||
Import the CSV file directly into your BI tool.
|
||||
|
||||
## Metrics Tracked
|
||||
|
||||
- **Total comments posted** by BabyBot
|
||||
- **Resolvable vs general comments** (indicates attachment success rate)
|
||||
- **Issues by severity** (Critical/Major/Minor breakdown)
|
||||
- **Issues by confidence** (High/Medium/Low)
|
||||
- **PRs reviewed** (count and distribution)
|
||||
- **Comments per PR** (average and trends)
|
||||
- **Engagement metrics:**
|
||||
- Comments with reactions (👍, ❤️, 🎉, etc.)
|
||||
- Comments with replies (developer responses)
|
||||
- Total reactions count
|
||||
- Average reactions per comment
|
||||
- Engagement rate % (how many comments get any response)
|
||||
|
||||
## Tracking Comment Engagement (Proxy for "Resolved")
|
||||
|
||||
Since GitHub doesn't expose "resolved" status via API, we track **engagement** as a proxy:
|
||||
|
||||
### Reactions as Resolution Indicators
|
||||
|
||||
Establish a reaction convention with your team:
|
||||
- ✅ 👍 (`:+1:`) = Acknowledged/Understood
|
||||
- 🎉 (`:hooray:`) = Fixed/Resolved
|
||||
- 👀 (`:eyes:`) = Looking into it
|
||||
- ❤️ (`:heart:`) = Appreciated/Helpful
|
||||
|
||||
### Query Engagement
|
||||
|
||||
```bash
|
||||
# Get all BabyBot comments with reactions
|
||||
gh api repos/grafana/grafana/pulls/comments --paginate \
|
||||
--jq '.[] | select(.body | contains("BabyBot 🍼")) | {id: .id, reactions: .reactions, pr: .pull_request_url}'
|
||||
|
||||
# Count comments with specific reactions (e.g., "resolved" markers)
|
||||
gh api repos/grafana/grafana/pulls/comments --paginate \
|
||||
--jq '[.[] | select(.body | contains("BabyBot 🍼")) | .reactions.hooray] | add'
|
||||
```
|
||||
|
||||
### Export shows:
|
||||
- `total_reactions`: All reactions on the comment
|
||||
- `has_reply`: Whether developers responded with a comment
|
||||
- `engagement_score`: Weighted score (reactions + reply bonus)
|
||||
|
||||
High engagement score = comment was noticed and actioned! 📊
|
||||
|
||||
## Scheduling Automatic Exports
|
||||
|
||||
Add to `.github/workflows/babybot-weekly-report.yml`:
|
||||
|
||||
```yaml
|
||||
name: BabyBot Weekly Metrics
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * MON' # Every Monday at 9am
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
jobs:
|
||||
export-metrics:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Export metrics
|
||||
run: python .github/scripts/export-babybot-metrics.py
|
||||
|
||||
- name: Upload to artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: babybot-metrics-${{ github.run_number }}
|
||||
path: babybot-metrics.*
|
||||
|
||||
- name: Post to Slack
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
run: |
|
||||
# Parse JSON and send summary to Slack
|
||||
SUMMARY=$(cat babybot-metrics.json | jq -r '"Total Comments: \(.total_comments), Critical: \(.by_severity.Critical // 0), Major: \(.by_severity.Major // 0)"')
|
||||
curl -X POST $SLACK_WEBHOOK_URL \
|
||||
-H 'Content-type: application/json' \
|
||||
-d "{\"text\": \"📊 Weekly BabyBot Metrics: $SUMMARY\"}"
|
||||
```
|
||||
|
||||
## Example Queries
|
||||
|
||||
```bash
|
||||
# Count by PR
|
||||
jq '.by_pr | to_entries | map({pr: .key, count: .value.count})' babybot-metrics.json
|
||||
|
||||
# Average comments per PR
|
||||
jq '[.by_pr[].count] | add / length' babybot-metrics.json
|
||||
|
||||
# Critical issues percentage
|
||||
jq '(.by_severity.Critical / .total_comments * 100)' babybot-metrics.json
|
||||
```
|
||||
323
.github/scripts/export-babybot-metrics.py
vendored
323
.github/scripts/export-babybot-metrics.py
vendored
@@ -1,323 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Export BabyBot usability review metrics from GitHub
|
||||
Usage: python export-babybot-metrics.py [--format csv|json] [--output filename]
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
import csv
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
def get_prs_with_usability_review_label():
|
||||
"""Get all PRs with the usability-review label"""
|
||||
cmd = [
|
||||
'gh', 'api', 'repos/grafana/grafana/issues',
|
||||
'--paginate',
|
||||
'-f', 'state=all',
|
||||
'-f', 'labels=usability-review',
|
||||
'--jq', '.[] | .number'
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
pr_numbers = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
try:
|
||||
pr_numbers.append(line.strip())
|
||||
except:
|
||||
continue
|
||||
|
||||
return pr_numbers
|
||||
|
||||
def get_review_comments(pr_numbers):
|
||||
"""Get all BabyBot review comments (resolvable ones) from specific PRs"""
|
||||
comments = []
|
||||
|
||||
for pr_number in pr_numbers:
|
||||
cmd = [
|
||||
'gh', 'api', f'repos/grafana/grafana/pulls/{pr_number}/comments',
|
||||
'--jq', '''
|
||||
.[] |
|
||||
select(.body | contains("BabyBot 🍼")) |
|
||||
{
|
||||
id: .id,
|
||||
pr_number: (.pull_request_url | split("/") | .[-1]),
|
||||
file: .path,
|
||||
line: .line,
|
||||
created_at: .created_at,
|
||||
updated_at: .updated_at,
|
||||
body: .body,
|
||||
html_url: .html_url,
|
||||
reactions: .reactions,
|
||||
in_reply_to_id: .in_reply_to_id
|
||||
}
|
||||
'''
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
try:
|
||||
comments.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return comments
|
||||
|
||||
def get_general_comments(pr_numbers):
|
||||
"""Get BabyBot general comments (fallback ones) from specific PRs"""
|
||||
comments = []
|
||||
|
||||
for pr_number in pr_numbers:
|
||||
cmd = [
|
||||
'gh', 'api', f'repos/grafana/grafana/issues/{pr_number}/comments',
|
||||
'--jq', '''
|
||||
.[] |
|
||||
select(.body | contains("BabyBot 🍼")) |
|
||||
{
|
||||
id: .id,
|
||||
pr_number: (.html_url | split("/") | .[-3]),
|
||||
created_at: .created_at,
|
||||
updated_at: .updated_at,
|
||||
body: .body,
|
||||
html_url: .html_url,
|
||||
reactions: .reactions
|
||||
}
|
||||
'''
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
try:
|
||||
comments.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return comments
|
||||
|
||||
def extract_severity(body):
|
||||
"""Extract severity from comment body"""
|
||||
if '‼️ Critical' in body:
|
||||
return 'Critical'
|
||||
elif '⚠️ Major' in body:
|
||||
return 'Major'
|
||||
elif '🟢 Minor' in body:
|
||||
return 'Minor'
|
||||
return 'Unknown'
|
||||
|
||||
def extract_confidence(body):
|
||||
"""Extract confidence level from comment body"""
|
||||
import re
|
||||
match = re.search(r'\*\*Confidence:\*\*\s*(Low|Medium|High)', body)
|
||||
return match.group(1) if match else 'Unknown'
|
||||
|
||||
def get_replies_to_comment(pr_number, comment_id):
|
||||
"""Get all replies to a specific comment"""
|
||||
cmd = [
|
||||
'gh', 'api', f'repos/grafana/grafana/pulls/{pr_number}/comments',
|
||||
'--jq', f'.[] | select(.in_reply_to_id == {comment_id})'
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
replies = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
try:
|
||||
replies.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return replies
|
||||
except:
|
||||
return []
|
||||
|
||||
def calculate_engagement_score(comment):
|
||||
"""Calculate engagement score based on reactions and replies"""
|
||||
reactions = comment.get('reactions', {})
|
||||
|
||||
# Count all reaction types
|
||||
total_reactions = 0
|
||||
if isinstance(reactions, dict):
|
||||
total_reactions = reactions.get('total_count', 0)
|
||||
# Weight certain reactions more heavily
|
||||
engagement_reactions = (
|
||||
reactions.get('+1', 0) + # thumbs up
|
||||
reactions.get('hooray', 0) + # party
|
||||
reactions.get('heart', 0) # heart
|
||||
)
|
||||
|
||||
# Check if there are replies
|
||||
has_reply = comment.get('in_reply_to_id') is not None
|
||||
|
||||
# Simple engagement score: reactions + bonus for replies
|
||||
score = total_reactions + (5 if has_reply else 0)
|
||||
|
||||
return {
|
||||
'total_reactions': total_reactions,
|
||||
'engagement_reactions': engagement_reactions,
|
||||
'has_reply': has_reply,
|
||||
'score': score
|
||||
}
|
||||
|
||||
def aggregate_metrics(review_comments, general_comments):
|
||||
"""Aggregate metrics by PR and severity"""
|
||||
metrics = {
|
||||
'total_comments': len(review_comments) + len(general_comments),
|
||||
'resolvable_comments': len(review_comments),
|
||||
'general_comments': len(general_comments),
|
||||
'by_pr': defaultdict(lambda: {'count': 0, 'severities': defaultdict(int), 'engaged': 0}),
|
||||
'by_severity': defaultdict(int),
|
||||
'by_confidence': defaultdict(int),
|
||||
'engagement': {
|
||||
'comments_with_reactions': 0,
|
||||
'comments_with_replies': 0,
|
||||
'total_reactions': 0,
|
||||
'avg_reactions_per_comment': 0,
|
||||
'engagement_rate': 0
|
||||
},
|
||||
'export_date': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
all_comments = review_comments + general_comments
|
||||
|
||||
total_reactions = 0
|
||||
comments_with_reactions = 0
|
||||
comments_with_replies = 0
|
||||
|
||||
for comment in all_comments:
|
||||
pr_num = comment['pr_number']
|
||||
severity = extract_severity(comment['body'])
|
||||
confidence = extract_confidence(comment['body'])
|
||||
|
||||
# Calculate engagement
|
||||
engagement = calculate_engagement_score(comment)
|
||||
|
||||
metrics['by_pr'][pr_num]['count'] += 1
|
||||
metrics['by_pr'][pr_num]['severities'][severity] += 1
|
||||
metrics['by_severity'][severity] += 1
|
||||
metrics['by_confidence'][confidence] += 1
|
||||
|
||||
# Track engagement
|
||||
if engagement['total_reactions'] > 0:
|
||||
comments_with_reactions += 1
|
||||
total_reactions += engagement['total_reactions']
|
||||
metrics['by_pr'][pr_num]['engaged'] += 1
|
||||
|
||||
if engagement['has_reply']:
|
||||
comments_with_replies += 1
|
||||
|
||||
# Calculate engagement metrics
|
||||
total = len(all_comments)
|
||||
metrics['engagement']['comments_with_reactions'] = comments_with_reactions
|
||||
metrics['engagement']['comments_with_replies'] = comments_with_replies
|
||||
metrics['engagement']['total_reactions'] = total_reactions
|
||||
metrics['engagement']['avg_reactions_per_comment'] = round(total_reactions / total, 2) if total > 0 else 0
|
||||
metrics['engagement']['engagement_rate'] = round((comments_with_reactions / total) * 100, 1) if total > 0 else 0
|
||||
|
||||
return metrics
|
||||
|
||||
def export_to_csv(metrics, review_comments, general_comments, filename):
|
||||
"""Export detailed metrics to CSV"""
|
||||
all_comments = review_comments + general_comments
|
||||
|
||||
with open(filename, 'w', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=[
|
||||
'comment_id', 'pr_number', 'comment_type', 'severity',
|
||||
'confidence', 'file', 'created_at', 'url', 'total_reactions',
|
||||
'has_reply', 'engagement_score'
|
||||
])
|
||||
writer.writeheader()
|
||||
|
||||
for comment in all_comments:
|
||||
engagement = calculate_engagement_score(comment)
|
||||
|
||||
writer.writerow({
|
||||
'comment_id': comment['id'],
|
||||
'pr_number': comment['pr_number'],
|
||||
'comment_type': 'resolvable' if 'file' in comment else 'general',
|
||||
'severity': extract_severity(comment['body']),
|
||||
'confidence': extract_confidence(comment['body']),
|
||||
'file': comment.get('file', 'N/A'),
|
||||
'created_at': comment['created_at'],
|
||||
'url': comment['html_url'],
|
||||
'total_reactions': engagement['total_reactions'],
|
||||
'has_reply': engagement['has_reply'],
|
||||
'engagement_score': engagement['score']
|
||||
})
|
||||
|
||||
print(f"✅ Exported detailed metrics to {filename}")
|
||||
|
||||
def export_to_json(metrics, filename):
|
||||
"""Export aggregated metrics to JSON"""
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(metrics, f, indent=2)
|
||||
|
||||
print(f"✅ Exported aggregated metrics to {filename}")
|
||||
|
||||
def print_summary(metrics):
|
||||
"""Print summary to console"""
|
||||
print("\n📊 BabyBot Usability Review Metrics")
|
||||
print("=" * 50)
|
||||
print(f"Total Comments: {metrics['total_comments']}")
|
||||
print(f" - Resolvable (on files): {metrics['resolvable_comments']}")
|
||||
print(f" - General: {metrics['general_comments']}")
|
||||
print(f"\nBy Severity:")
|
||||
for severity, count in metrics['by_severity'].items():
|
||||
print(f" - {severity}: {count}")
|
||||
print(f"\nBy Confidence:")
|
||||
for confidence, count in metrics['by_confidence'].items():
|
||||
print(f" - {confidence}: {count}")
|
||||
print(f"\nEngagement:")
|
||||
eng = metrics['engagement']
|
||||
print(f" - Comments with reactions: {eng['comments_with_reactions']}")
|
||||
print(f" - Comments with replies: {eng['comments_with_replies']}")
|
||||
print(f" - Total reactions: {eng['total_reactions']}")
|
||||
print(f" - Avg reactions per comment: {eng['avg_reactions_per_comment']}")
|
||||
print(f" - Engagement rate: {eng['engagement_rate']}%")
|
||||
print(f"\nPRs Reviewed: {len(metrics['by_pr'])}")
|
||||
print("=" * 50)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Export BabyBot metrics')
|
||||
parser.add_argument('--format', choices=['csv', 'json', 'both'], default='both',
|
||||
help='Export format (default: both)')
|
||||
parser.add_argument('--output', default='babybot-metrics',
|
||||
help='Output filename (without extension)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("🔍 Fetching PRs with 'usability-review' label...")
|
||||
pr_numbers = get_prs_with_usability_review_label()
|
||||
print(f"Found {len(pr_numbers)} PRs with usability-review label")
|
||||
|
||||
if not pr_numbers:
|
||||
print("No PRs found with usability-review label. Exiting.")
|
||||
return
|
||||
|
||||
print("\n🔍 Fetching BabyBot comments from those PRs...")
|
||||
review_comments = get_review_comments(pr_numbers)
|
||||
general_comments = get_general_comments(pr_numbers)
|
||||
|
||||
print(f"Found {len(review_comments)} review comments and {len(general_comments)} general comments")
|
||||
|
||||
print("\n📈 Aggregating metrics...")
|
||||
metrics = aggregate_metrics(review_comments, general_comments)
|
||||
|
||||
print_summary(metrics)
|
||||
|
||||
if args.format in ['csv', 'both']:
|
||||
export_to_csv(metrics, review_comments, general_comments, f"{args.output}.csv")
|
||||
|
||||
if args.format in ['json', 'both']:
|
||||
export_to_json(metrics, f"{args.output}.json")
|
||||
|
||||
print("\n✨ Done!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
6
.github/workflows/add-to-whats-new.yml
vendored
6
.github/workflows/add-to-whats-new.yml
vendored
@@ -1,11 +1,11 @@
|
||||
name: Add comment about adding a What's new note
|
||||
name: Add comment about adding a What's new note for either what's new or breaking changes
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
add-comment:
|
||||
if: ${{ ! github.event.pull_request.head.repo.fork && contains(github.event.pull_request.labels.*.name, 'add to what''s new') }}
|
||||
if: ${{ ! github.event.pull_request.head.repo.fork && (contains(github.event.pull_request.labels.*.name, 'add to what''s new') || contains(github.event.pull_request.labels.*.name, 'breaking change') || contains(github.event.pull_request.labels.*.name, 'levitate breaking change')) }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
- uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
||||
with:
|
||||
message: |
|
||||
Since you've added the `Add to what's new` label, consider drafting a [What's new note](https://admin.grafana.com/content-admin/#/collections/whats-new/new) for this feature.
|
||||
Since you've added the `What's New` or a breaking change label, consider drafting a [What's new note](https://admin.grafana.com/content-admin/#/collections/whats-new/new) for this feature.
|
||||
|
||||
127
.github/workflows/babybot-weekly-report.yml
vendored
127
.github/workflows/babybot-weekly-report.yml
vendored
@@ -1,127 +0,0 @@
|
||||
name: BabyBot Weekly Metrics Report
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * MON' # Every Monday at 9am UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
export-metrics:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Export BabyBot metrics
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
python .github/scripts/export-babybot-metrics.py --format both --output babybot-metrics-$(date +%Y-%m-%d)
|
||||
|
||||
- name: Upload metrics artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: babybot-metrics-${{ github.run_number }}
|
||||
path: |
|
||||
babybot-metrics-*.csv
|
||||
babybot-metrics-*.json
|
||||
retention-days: 90
|
||||
|
||||
- name: Generate summary
|
||||
id: summary
|
||||
run: |
|
||||
METRICS_FILE=$(ls babybot-metrics-*.json | head -1)
|
||||
|
||||
TOTAL=$(jq -r '.total_comments' $METRICS_FILE)
|
||||
RESOLVABLE=$(jq -r '.resolvable_comments' $METRICS_FILE)
|
||||
CRITICAL=$(jq -r '.by_severity.Critical // 0' $METRICS_FILE)
|
||||
MAJOR=$(jq -r '.by_severity.Major // 0' $METRICS_FILE)
|
||||
MINOR=$(jq -r '.by_severity.Minor // 0' $METRICS_FILE)
|
||||
PRS=$(jq -r '.by_pr | length' $METRICS_FILE)
|
||||
|
||||
echo "total=$TOTAL" >> $GITHUB_OUTPUT
|
||||
echo "resolvable=$RESOLVABLE" >> $GITHUB_OUTPUT
|
||||
echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
|
||||
echo "major=$MAJOR" >> $GITHUB_OUTPUT
|
||||
echo "minor=$MINOR" >> $GITHUB_OUTPUT
|
||||
echo "prs=$PRS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Post summary to Slack
|
||||
if: env.SLACK_WEBHOOK_URL != ''
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
TOTAL: ${{ steps.summary.outputs.total }}
|
||||
RESOLVABLE: ${{ steps.summary.outputs.resolvable }}
|
||||
CRITICAL: ${{ steps.summary.outputs.critical }}
|
||||
MAJOR: ${{ steps.summary.outputs.major }}
|
||||
MINOR: ${{ steps.summary.outputs.minor }}
|
||||
PRS: ${{ steps.summary.outputs.prs }}
|
||||
run: |
|
||||
curl -X POST $SLACK_WEBHOOK_URL \
|
||||
-H 'Content-type: application/json' \
|
||||
-d "{
|
||||
\"channel\": \"#hackathon15-agentic-usability-review\",
|
||||
\"username\": \"BabyBot Metrics\",
|
||||
\"icon_emoji\": \":bar_chart:\",
|
||||
\"blocks\": [
|
||||
{
|
||||
\"type\": \"header\",
|
||||
\"text\": {
|
||||
\"type\": \"plain_text\",
|
||||
\"text\": \"📊 BabyBot Weekly Metrics Report\"
|
||||
}
|
||||
},
|
||||
{
|
||||
\"type\": \"section\",
|
||||
\"text\": {
|
||||
\"type\": \"mrkdwn\",
|
||||
\"text\": \"*Total Comments:* $TOTAL\\n*Resolvable:* $RESOLVABLE\\n*PRs Reviewed:* $PRS\"
|
||||
}
|
||||
},
|
||||
{
|
||||
\"type\": \"section\",
|
||||
\"text\": {
|
||||
\"type\": \"mrkdwn\",
|
||||
\"text\": \"*By Severity*\\n• ‼️ Critical: $CRITICAL\\n• ⚠️ Major: $MAJOR\\n• 🟢 Minor: $MINOR\"
|
||||
}
|
||||
},
|
||||
{
|
||||
\"type\": \"actions\",
|
||||
\"elements\": [
|
||||
{
|
||||
\"type\": \"button\",
|
||||
\"text\": {
|
||||
\"type\": \"plain_text\",
|
||||
\"text\": \"Download Full Report\"
|
||||
},
|
||||
\"url\": \"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"
|
||||
|
||||
- name: Create summary comment (optional - for visibility)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "## 📊 BabyBot Metrics Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Total Comments:** ${{ steps.summary.outputs.total }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Resolvable Comments:** ${{ steps.summary.outputs.resolvable }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**PRs Reviewed:** ${{ steps.summary.outputs.prs }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### By Severity" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ‼️ Critical: ${{ steps.summary.outputs.critical }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ⚠️ Major: ${{ steps.summary.outputs.major }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🟢 Minor: ${{ steps.summary.outputs.minor }}" >> $GITHUB_STEP_SUMMARY
|
||||
613
.github/workflows/usability-review.yml
vendored
613
.github/workflows/usability-review.yml
vendored
@@ -1,613 +0,0 @@
|
||||
name: Usability Review Agent
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
OUTPUT_TEXT_PATH: usability-review.txt
|
||||
SCREENSHOT_PATH: usability-screenshot.png
|
||||
|
||||
jobs:
|
||||
usability-review:
|
||||
# Run when either: (1) someone comments '/test-me-baby-one-more-time' on a PR, or (2) 'usability-review' label is added
|
||||
if: ${{ (github.event.issue.pull_request && startsWith(github.event.comment.body, '/test-me-baby-one-more-time')) || github.event.label.name == 'usability-review' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: React to comment
|
||||
if: github.event_name == 'issue_comment'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'rocket'
|
||||
});
|
||||
|
||||
- name: Post acknowledgment
|
||||
if: github.event_name == 'issue_comment'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.issue.number }}
|
||||
run: |
|
||||
gh pr comment $PR_NUMBER --body "🤖 Starting usability review... This may take a few minutes."
|
||||
|
||||
- name: Set PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "issue_comment" ]; then
|
||||
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Extract target URL from ephemeral instance comment
|
||||
id: extract-url
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr-number.outputs.number }}
|
||||
run: |
|
||||
# Extract ephemeral instance URL from PR comments
|
||||
# Looks for the most recent comment containing: https://ephemeral{alphanumeric}.grafana-dev.net
|
||||
#
|
||||
# TODO: Future improvement - auto-trigger deployment if no URL found:
|
||||
# 1. Comment /deploy-to-hg on the PR
|
||||
# 2. Wait ~20 minutes for build to complete
|
||||
# 3. Retry extracting URL
|
||||
# 4. Continue with usability review
|
||||
|
||||
# Get all PR comments, newest first
|
||||
COMMENTS=$(gh pr view $PR_NUMBER --json comments --jq '.comments | reverse | .[].body')
|
||||
|
||||
# Find the most recent comment with ephemeral instance URL (alphanumeric subdomain)
|
||||
TARGET_URL=$(echo "$COMMENTS" | grep -oE 'https://ephemeral[a-zA-Z0-9]+\.grafana-dev\.net' | head -1)
|
||||
|
||||
if [ -z "$TARGET_URL" ]; then
|
||||
echo "::error::No ephemeral instance URL found in PR comments"
|
||||
echo "::notice::Please deploy an ephemeral instance first by commenting /deploy-to-hg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found target URL: $TARGET_URL"
|
||||
echo "TARGET_URL=$TARGET_URL" >> $GITHUB_ENV
|
||||
|
||||
- name: "Get vault secrets (dev)"
|
||||
id: vault-secrets-dev
|
||||
uses: grafana/shared-workflows/actions/get-vault-secrets@get-vault-secrets/v1.3.0
|
||||
with:
|
||||
vault_instance: "dev"
|
||||
repo_secrets: |
|
||||
GCOM_HOST=usability-review-agent:GCOM_HOST
|
||||
GCOM_TOKEN=usability-review-agent:GCOM_TOKEN
|
||||
OPENAI_API_KEY=usability-review-agent:OPENAI_API_KEY
|
||||
GRAFANA_USERNAME=usability-review-agent:GRAFANA_USERNAME
|
||||
GRAFANA_PASSWORD=usability-review-agent:GRAFANA_PASSWORD
|
||||
SLACK_WEBHOOK_URL=usability-review-agent:SLACK_WEBHOOK_URL
|
||||
|
||||
- name: "Get vault secrets (ops) - GitHub App credentials"
|
||||
id: vault-secrets-ops
|
||||
uses: grafana/shared-workflows/actions/get-vault-secrets@main
|
||||
with:
|
||||
repo_secrets: |
|
||||
APP_ID=ephemeral-instances-bot:app-id
|
||||
APP_PEM=ephemeral-instances-bot:app-private-key
|
||||
|
||||
- name: "Generate GitHub App token for private repo access"
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
|
||||
with:
|
||||
app_id: ${{ env.APP_ID }}
|
||||
private_key: ${{ env.APP_PEM }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install OpenAI package
|
||||
run: |
|
||||
python -m pip install -U pip
|
||||
pip install openai
|
||||
|
||||
- name: Get PR details
|
||||
id: pr-details
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr-number.outputs.number }}
|
||||
run: |
|
||||
# Get PR title, body, and changed files
|
||||
PR_DATA=$(gh pr view $PR_NUMBER --json title,body,files)
|
||||
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
PR_BODY=$(echo "$PR_DATA" | jq -r '.body // ""')
|
||||
|
||||
# Get list of changed files
|
||||
CHANGED_FILES=$(echo "$PR_DATA" | jq -r '.files[].path' | head -20)
|
||||
|
||||
# Save to files for next step
|
||||
echo "$PR_TITLE" > pr_title.txt
|
||||
echo "$PR_BODY" > pr_body.txt
|
||||
echo "$CHANGED_FILES" > changed_files.txt
|
||||
|
||||
echo "PR Title: $PR_TITLE"
|
||||
echo "Changed files count: $(echo "$CHANGED_FILES" | wc -l)"
|
||||
|
||||
- name: Generate PR summary
|
||||
id: pr-summary
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ env.OPENAI_API_KEY }}
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
|
||||
# Read PR details
|
||||
with open("pr_title.txt", "r") as f:
|
||||
pr_title = f.read().strip()
|
||||
|
||||
with open("pr_body.txt", "r") as f:
|
||||
pr_body = f.read().strip()
|
||||
|
||||
with open("changed_files.txt", "r") as f:
|
||||
changed_files = f.read().strip()
|
||||
|
||||
# Generate summary of what the PR does
|
||||
system = "You are a technical analyst summarizing pull request changes."
|
||||
user = f"""Analyze this PR and provide a 2-3 sentence summary of what feature/change is being implemented:
|
||||
|
||||
PR Title: {pr_title}
|
||||
|
||||
PR Description: {pr_body if pr_body else "(No description provided)"}
|
||||
|
||||
Changed Files:
|
||||
{changed_files}
|
||||
|
||||
Focus on what the user-facing impact is and what functionality is being added or modified."""
|
||||
|
||||
resp = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user},
|
||||
],
|
||||
)
|
||||
pr_summary = resp.choices[0].message.content.strip()
|
||||
|
||||
with open("pr_summary.txt", "w") as f:
|
||||
f.write(pr_summary)
|
||||
|
||||
print(f"PR Summary: {pr_summary}")
|
||||
PY
|
||||
|
||||
- name: Fetch prompt from external repo
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
# Fetch prompt from private repo using GitHub API
|
||||
gh api \
|
||||
-H "Accept: application/vnd.github.raw" \
|
||||
/repos/grafana/usability-review-agent-github-action/contents/prompts/single/reviewer.md \
|
||||
> base_prompt.txt
|
||||
|
||||
echo "Fetched prompt from external repo"
|
||||
echo "Prompt size: $(wc -c < base_prompt.txt) bytes"
|
||||
echo "First 100 chars: $(head -c 100 base_prompt.txt)"
|
||||
|
||||
- name: Build dynamic prompt
|
||||
id: build-prompt
|
||||
run: |
|
||||
# Read the PR summary
|
||||
PR_SUMMARY=$(cat pr_summary.txt)
|
||||
|
||||
# Read base prompt template from external repo
|
||||
BASE_PROMPT=$(cat base_prompt.txt)
|
||||
|
||||
# Build dynamic prompt with PR context
|
||||
cat > dynamic_prompt.txt <<EOF
|
||||
## Context
|
||||
You are reviewing a pull request that makes the following changes:
|
||||
|
||||
$PR_SUMMARY
|
||||
|
||||
Your task is to test the workflow and provide usability feedback.
|
||||
|
||||
## Instructions
|
||||
$BASE_PROMPT
|
||||
EOF
|
||||
|
||||
# Copy to action directory so it can be used
|
||||
cp dynamic_prompt.txt .github/actions/usability-review-agent/dynamic_prompt.txt
|
||||
|
||||
echo "Dynamic prompt created"
|
||||
|
||||
- name: Extract instance slug from URL
|
||||
run: |
|
||||
INSTANCE_SLUG=$(echo "${{ env.TARGET_URL }}" | sed -E 's|https?://([^.]+)\..*|\1|')
|
||||
echo "Instance slug: $INSTANCE_SLUG"
|
||||
echo "INSTANCE_SLUG=$INSTANCE_SLUG" >> $GITHUB_ENV
|
||||
|
||||
- name: Enable basic auth on instance
|
||||
env:
|
||||
GCOM_HOST: ${{ env.GCOM_HOST }}
|
||||
GCOM_TOKEN: ${{ env.GCOM_TOKEN }}
|
||||
INSTANCE_SLUG: ${{ env.INSTANCE_SLUG }}
|
||||
run: |
|
||||
# Check current config
|
||||
CURRENT_CONFIG=$(curl -s "${GCOM_HOST}/api/instances/${INSTANCE_SLUG}/config" \
|
||||
-H "Authorization: Bearer ${GCOM_TOKEN}")
|
||||
|
||||
DISABLE_LOGIN_FORM=$(echo "$CURRENT_CONFIG" | jq -r '.auth.disable_login_form // true')
|
||||
echo "Current disable_login_form: $DISABLE_LOGIN_FORM"
|
||||
|
||||
if [ "$DISABLE_LOGIN_FORM" != "false" ]; then
|
||||
echo "Enabling basic auth..."
|
||||
curl -X POST "${GCOM_HOST}/api/instances/${INSTANCE_SLUG}/config" \
|
||||
-H "Authorization: Bearer ${GCOM_TOKEN}" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d 'config[auth][disable_login_form]=false'
|
||||
|
||||
echo "Waiting for pod restart..."
|
||||
sleep 300
|
||||
|
||||
# Verify instance is ready
|
||||
for i in {1..10}; do
|
||||
if curl -sf https://${INSTANCE_SLUG}.grafana-dev.net/api/health; then
|
||||
echo "Instance ready!"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $i failed, waiting..."
|
||||
sleep 10
|
||||
done
|
||||
else
|
||||
echo "Basic auth already enabled, skipping..."
|
||||
fi
|
||||
|
||||
- name: Create test user for playwright
|
||||
# Using system realm token with stack-users:write scope
|
||||
env:
|
||||
GCOM_HOST: ${{ env.GCOM_HOST }}
|
||||
GCOM_TOKEN: ${{ env.GCOM_TOKEN }}
|
||||
INSTANCE_SLUG: ${{ env.INSTANCE_SLUG }}
|
||||
GRAFANA_USERNAME: ${{ env.GRAFANA_USERNAME }}
|
||||
GRAFANA_PASSWORD: ${{ env.GRAFANA_PASSWORD }}
|
||||
run: |
|
||||
pip install requests
|
||||
python3 <<'PYTHON'
|
||||
import os, requests, json
|
||||
|
||||
gcom_host = os.environ['GCOM_HOST']
|
||||
gcom_token = os.environ['GCOM_TOKEN']
|
||||
instance = os.environ['INSTANCE_SLUG']
|
||||
username = os.environ['GRAFANA_USERNAME']
|
||||
password = os.environ['GRAFANA_PASSWORD']
|
||||
|
||||
# Create user via GCOM API (proxies to instance /api/admin/users)
|
||||
resp = requests.post(
|
||||
f"{gcom_host}/api/instances/{instance}/api/admin/users",
|
||||
headers={"Authorization": f"Bearer {gcom_token}"},
|
||||
json={"name": "Usability Review Agent", "login": username, "password": password}
|
||||
)
|
||||
|
||||
if resp.status_code == 412:
|
||||
print(f"Test user already exists (412), skipping creation")
|
||||
elif resp.status_code >= 400:
|
||||
print(f"Error creating user: {resp.status_code} {resp.text}")
|
||||
resp.raise_for_status()
|
||||
else:
|
||||
user_id = resp.json()['id']
|
||||
print(f"Created new test user (id: {user_id})")
|
||||
|
||||
# Make user admin via GCOM API (proxies to instance)
|
||||
perm_resp = requests.put(
|
||||
f"{gcom_host}/api/instances/{instance}/api/admin/users/{user_id}/permissions",
|
||||
headers={"Authorization": f"Bearer {gcom_token}"},
|
||||
json={"isGrafanaAdmin": True}
|
||||
)
|
||||
|
||||
if perm_resp.status_code >= 400:
|
||||
print(f"Warning: Failed to set admin permissions: {perm_resp.status_code}")
|
||||
else:
|
||||
print(f"Set admin permissions for test user")
|
||||
|
||||
print(f"Test user setup complete")
|
||||
PYTHON
|
||||
|
||||
- name: Run usability review agent
|
||||
uses: ./.github/actions/usability-review-agent
|
||||
with:
|
||||
target_url: ${{ env.TARGET_URL }}
|
||||
openai_api_key: ${{ env.OPENAI_API_KEY }}
|
||||
grafana_username: ${{ env.GRAFANA_USERNAME }}
|
||||
grafana_password: ${{ env.GRAFANA_PASSWORD }}
|
||||
workflow_name: "the application interface"
|
||||
prompt_file: "dynamic_prompt.txt"
|
||||
output_text_path: ${{ env.OUTPUT_TEXT_PATH }}
|
||||
screenshot_path: ${{ env.SCREENSHOT_PATH }}
|
||||
|
||||
- name: Upload review artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: usability-review-results
|
||||
path: |
|
||||
${{ env.OUTPUT_TEXT_PATH }}
|
||||
${{ env.SCREENSHOT_PATH }}
|
||||
login_page.png
|
||||
login_error.png
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
- name: Post review results to PR
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr-number.outputs.number }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
COMMENT_FILE=$(mktemp)
|
||||
|
||||
echo "## 🔍 Usability Review Results" > "$COMMENT_FILE"
|
||||
echo "" >> "$COMMENT_FILE"
|
||||
echo "**Target URL:** ${{ env.TARGET_URL }}" >> "$COMMENT_FILE"
|
||||
echo "" >> "$COMMENT_FILE"
|
||||
echo "---" >> "$COMMENT_FILE"
|
||||
echo "" >> "$COMMENT_FILE"
|
||||
|
||||
if [ -s ${{ env.OUTPUT_TEXT_PATH }} ]; then
|
||||
cat ${{ env.OUTPUT_TEXT_PATH }} >> "$COMMENT_FILE"
|
||||
else
|
||||
echo "⚠️ No review output was generated." >> "$COMMENT_FILE"
|
||||
fi
|
||||
|
||||
echo "" >> "$COMMENT_FILE"
|
||||
echo "---" >> "$COMMENT_FILE"
|
||||
echo "" >> "$COMMENT_FILE"
|
||||
echo "📸 [View screenshot and full artifacts](https://github.com/${{ github.repository }}/actions/runs/$RUN_ID)" >> "$COMMENT_FILE"
|
||||
|
||||
gh pr comment $PR_NUMBER --body-file "$COMMENT_FILE"
|
||||
rm "$COMMENT_FILE"
|
||||
|
||||
- name: Post individual suggestions as separate comments
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr-number.outputs.number }}
|
||||
OUTPUT_TEXT_PATH: ${{ env.OUTPUT_TEXT_PATH }}
|
||||
run: |
|
||||
python3 <<'PYTHON'
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
import json
|
||||
|
||||
# Read the AI output
|
||||
output_path = os.environ['OUTPUT_TEXT_PATH']
|
||||
if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
|
||||
print("No output file found, skipping individual comments")
|
||||
exit(0)
|
||||
|
||||
with open(output_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract table rows (skip header and separator)
|
||||
# New format: | Issue | Severity | Impact | Confidence | Suggestion |
|
||||
table_pattern = r'\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|'
|
||||
matches = re.findall(table_pattern, content)
|
||||
|
||||
if len(matches) < 3:
|
||||
print(f"Not enough table rows found ({len(matches)}), skipping individual comments")
|
||||
exit(0)
|
||||
|
||||
# Skip first 2 matches (header + separator)
|
||||
issue_rows = matches[2:] # Get all issues after header
|
||||
|
||||
pr_number = os.environ['PR_NUMBER']
|
||||
|
||||
# Get changed files in the PR to attach comments to
|
||||
pr_files_result = subprocess.run(
|
||||
['gh', 'pr', 'view', pr_number, '--json', 'files'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
pr_data = json.loads(pr_files_result.stdout)
|
||||
changed_files = [f['path'] for f in pr_data.get('files', [])]
|
||||
|
||||
# Prefer frontend files for UI issues
|
||||
frontend_files = [f for f in changed_files if any(ext in f for ext in ['.tsx', '.ts', '.jsx', '.js', '.vue', '.css', '.scss'])]
|
||||
target_files = frontend_files if frontend_files else changed_files
|
||||
|
||||
if not target_files:
|
||||
print("No changed files found, posting as general comments instead")
|
||||
target_files = [None] * len(issue_rows)
|
||||
|
||||
for idx, (issue, severity, impact, confidence, suggestion) in enumerate(issue_rows):
|
||||
# Clean up whitespace
|
||||
issue = issue.strip()
|
||||
severity = severity.strip()
|
||||
impact = impact.strip()
|
||||
confidence = confidence.strip()
|
||||
suggestion = suggestion.strip()
|
||||
|
||||
# Extract issue title from markdown (bold text before <br />)
|
||||
issue_title_match = re.search(r'\*\*(.+?)\*\*', issue)
|
||||
issue_title = issue_title_match.group(1) if issue_title_match else issue
|
||||
|
||||
# Create individual comment
|
||||
comment = f"""### {severity} {issue_title}
|
||||
|
||||
**Confidence:** {confidence}
|
||||
|
||||
**Impact:** {impact}
|
||||
|
||||
**Suggestion:** {suggestion}
|
||||
|
||||
---
|
||||
_Posted by BabyBot 🍼 - Usability Review Agent_"""
|
||||
|
||||
# Try to post as review comment (resolvable) on a changed file
|
||||
target_file = target_files[idx % len(target_files)] if target_files[0] is not None else None
|
||||
|
||||
if target_file:
|
||||
# Post as review comment on the file (resolvable!)
|
||||
try:
|
||||
# Create a review comment on line 1 of the file
|
||||
subprocess.run(
|
||||
['gh', 'pr', 'review', pr_number, '--comment', '--body', comment, '--file', target_file, '--line', '1'],
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
print(f"Posted resolvable comment for: {issue_title} (on {target_file})")
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Fallback to general comment if review comment fails
|
||||
print(f"Failed to post review comment, falling back to general comment: {e}")
|
||||
subprocess.run(
|
||||
['gh', 'pr', 'comment', pr_number, '--body', comment],
|
||||
check=True
|
||||
)
|
||||
print(f"Posted general comment for: {issue_title}")
|
||||
else:
|
||||
# Post as general comment
|
||||
subprocess.run(
|
||||
['gh', 'pr', 'comment', pr_number, '--body', comment],
|
||||
check=True
|
||||
)
|
||||
print(f"Posted general comment for: {issue_title}")
|
||||
|
||||
PYTHON
|
||||
|
||||
- name: Send Slack notification on success
|
||||
if: always()
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ env.SLACK_WEBHOOK_URL }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr-number.outputs.number }}
|
||||
OUTPUT_TEXT_PATH: ${{ env.OUTPUT_TEXT_PATH }}
|
||||
run: |
|
||||
# Check if Slack webhook is configured
|
||||
if [ -z "$SLACK_WEBHOOK_URL" ]; then
|
||||
echo "Slack webhook not configured, skipping notification"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if review was successful
|
||||
if [ ! -s ${{ env.OUTPUT_TEXT_PATH }} ]; then
|
||||
echo "No review output found, skipping success notification"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fetch PR details and export for Python
|
||||
PR_DATA=$(gh pr view $PR_NUMBER --json title,url,author)
|
||||
export PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
export PR_URL=$(echo "$PR_DATA" | jq -r '.url')
|
||||
export PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
|
||||
python3 <<'PYTHON'
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
# Read the AI output to extract issues
|
||||
with open(os.environ['OUTPUT_TEXT_PATH'], 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract table rows for issues
|
||||
# New format: | Issue | Severity | Impact | Confidence | Suggestion |
|
||||
table_pattern = r'\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|'
|
||||
matches = re.findall(table_pattern, content)
|
||||
|
||||
# Build issues text
|
||||
issues_text = ""
|
||||
if len(matches) >= 3:
|
||||
issue_rows = matches[2:] # Skip header rows
|
||||
for issue, severity, impact, confidence, suggestion in issue_rows:
|
||||
issue = issue.strip()
|
||||
severity = severity.strip()
|
||||
suggestion = suggestion.strip()
|
||||
|
||||
# Extract issue title from markdown
|
||||
issue_title_match = re.search(r'\*\*(.+?)\*\*', issue)
|
||||
issue_title = issue_title_match.group(1) if issue_title_match else issue
|
||||
|
||||
issues_text += f"• *{severity}* {issue_title}\n → {suggestion}\n"
|
||||
else:
|
||||
issues_text = "No specific issues found"
|
||||
|
||||
pr_number = os.environ['PR_NUMBER']
|
||||
pr_title = os.environ['PR_TITLE']
|
||||
pr_url = os.environ['PR_URL']
|
||||
pr_author = os.environ['PR_AUTHOR']
|
||||
|
||||
# Build Slack message
|
||||
slack_payload = {
|
||||
"channel": "#hackathon15-agentic-usability-review",
|
||||
"username": "Usability Review Agent",
|
||||
"icon_emoji": ":mag:",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": f"✅ Usability Review Complete: PR #{pr_number}",
|
||||
"emoji": True
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"*<{pr_url}|{pr_title}>*\nby @{pr_author}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"*Issues & Suggestions*\n{issues_text}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "View Full Review",
|
||||
"emoji": True
|
||||
},
|
||||
"url": pr_url
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Send to Slack
|
||||
webhook_url = os.environ['SLACK_WEBHOOK_URL']
|
||||
subprocess.run(
|
||||
['curl', '-X', 'POST', '-H', 'Content-type: application/json',
|
||||
'--data', json.dumps(slack_payload), webhook_url],
|
||||
check=True
|
||||
)
|
||||
print("Slack notification sent successfully")
|
||||
PYTHON
|
||||
@@ -8,7 +8,7 @@ require (
|
||||
github.com/google/go-github/v70 v70.0.0
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4
|
||||
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0
|
||||
|
||||
@@ -618,8 +618,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k
|
||||
github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
|
||||
|
||||
@@ -23,8 +23,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 h1:PgMfX4OPENz/iXmtDDIW9+poZY4UD0hhmXm7flVclDo=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28/go.mod h1:av5N0Naq+8VV9MLF7zAkihy/mVq5UbS2EvRSJukDHlY=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
|
||||
@@ -6,7 +6,7 @@ require (
|
||||
github.com/go-kit/log v0.2.1
|
||||
github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
|
||||
@@ -216,14 +216,14 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4=
|
||||
github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
@@ -15,9 +16,14 @@ const (
|
||||
lokiDefaultMaxQuerySize = 65536 // 64kb
|
||||
)
|
||||
|
||||
type LokiConfig struct {
|
||||
lokiclient.LokiConfig
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
type NotificationConfig struct {
|
||||
Enabled bool
|
||||
Loki lokiclient.LokiConfig
|
||||
Loki LokiConfig
|
||||
}
|
||||
|
||||
type RuntimeConfig struct {
|
||||
@@ -27,7 +33,7 @@ type RuntimeConfig struct {
|
||||
|
||||
func (n *NotificationConfig) AddFlagsWithPrefix(prefix string, flags *pflag.FlagSet) {
|
||||
flags.BoolVar(&n.Enabled, prefix+".enabled", false, "Enable notification query endpoints")
|
||||
addLokiFlags(&n.Loki, prefix+".loki", flags)
|
||||
addLokiFlags(&n.Loki.LokiConfig, prefix+".loki", flags)
|
||||
}
|
||||
|
||||
func (r *RuntimeConfig) AddFlagsWithPrefix(prefix string, flags *pflag.FlagSet) {
|
||||
|
||||
@@ -24,10 +24,12 @@ func TestRuntimeConfig(t *testing.T) {
|
||||
expected: RuntimeConfig{
|
||||
Notification: NotificationConfig{
|
||||
Enabled: false,
|
||||
Loki: lokiclient.LokiConfig{
|
||||
ReadPathURL: nil,
|
||||
MaxQueryLength: 721 * time.Hour,
|
||||
MaxQuerySize: 65536,
|
||||
Loki: LokiConfig{
|
||||
LokiConfig: lokiclient.LokiConfig{
|
||||
ReadPathURL: nil,
|
||||
MaxQueryLength: 721 * time.Hour,
|
||||
MaxQuerySize: 65536,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -38,10 +40,12 @@ func TestRuntimeConfig(t *testing.T) {
|
||||
expected: RuntimeConfig{
|
||||
Notification: NotificationConfig{
|
||||
Enabled: true,
|
||||
Loki: lokiclient.LokiConfig{
|
||||
ReadPathURL: nil,
|
||||
MaxQueryLength: 721 * time.Hour,
|
||||
MaxQuerySize: 65536,
|
||||
Loki: LokiConfig{
|
||||
LokiConfig: lokiclient.LokiConfig{
|
||||
ReadPathURL: nil,
|
||||
MaxQueryLength: 721 * time.Hour,
|
||||
MaxQuerySize: 65536,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -57,13 +61,15 @@ func TestRuntimeConfig(t *testing.T) {
|
||||
expected: RuntimeConfig{
|
||||
Notification: NotificationConfig{
|
||||
Enabled: false,
|
||||
Loki: lokiclient.LokiConfig{
|
||||
ReadPathURL: lokiURL,
|
||||
BasicAuthUser: "foo",
|
||||
BasicAuthPassword: "bar",
|
||||
TenantID: "baz",
|
||||
MaxQueryLength: 721 * time.Hour,
|
||||
MaxQuerySize: 65536,
|
||||
Loki: LokiConfig{
|
||||
LokiConfig: lokiclient.LokiConfig{
|
||||
ReadPathURL: lokiURL,
|
||||
BasicAuthUser: "foo",
|
||||
BasicAuthPassword: "bar",
|
||||
TenantID: "baz",
|
||||
MaxQueryLength: 721 * time.Hour,
|
||||
MaxQuerySize: 65536,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/alerting/historian/pkg/app/config"
|
||||
"github.com/grafana/grafana/apps/alerting/historian/pkg/app/logutil"
|
||||
)
|
||||
|
||||
@@ -47,7 +49,7 @@ type LokiReader struct {
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func NewLokiReader(cfg lokiclient.LokiConfig, reg prometheus.Registerer, logger logging.Logger, tracer trace.Tracer) *LokiReader {
|
||||
func NewLokiReader(cfg config.LokiConfig, reg prometheus.Registerer, logger logging.Logger, tracer trace.Tracer) *LokiReader {
|
||||
duration := instrument.NewHistogramCollector(promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
@@ -56,9 +58,13 @@ func NewLokiReader(cfg lokiclient.LokiConfig, reg prometheus.Registerer, logger
|
||||
Buckets: instrument.DefBuckets,
|
||||
}, instrument.HistogramCollectorBuckets))
|
||||
|
||||
requester := &http.Client{
|
||||
Transport: cfg.Transport,
|
||||
}
|
||||
|
||||
gkLogger := logutil.ToGoKitLogger(logger)
|
||||
return &LokiReader{
|
||||
client: lokiclient.NewLokiClient(cfg, lokiclient.NewRequester(), nil, duration, gkLogger, tracer, LokiClientSpanName),
|
||||
client: lokiclient.NewLokiClient(cfg.LokiConfig, requester, nil, duration, gkLogger, tracer, LokiClientSpanName),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/notifications
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/apiserver v0.34.2
|
||||
|
||||
@@ -71,8 +71,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
|
||||
@@ -23,6 +23,12 @@ type Receiver struct {
|
||||
Spec ReceiverSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func NewReceiver() *Receiver {
|
||||
return &Receiver{
|
||||
Spec: *NewReceiverSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Receiver) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"),
|
||||
schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewReceiver(), &ReceiverList{}, resource.WithKind("Receiver"),
|
||||
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{
|
||||
FieldSelector: "spec.title",
|
||||
FieldValueFunc: func(o resource.Object) (string, error) {
|
||||
|
||||
@@ -23,6 +23,12 @@ type RoutingTree struct {
|
||||
Spec RoutingTreeSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func NewRoutingTree() *RoutingTree {
|
||||
return &RoutingTree{
|
||||
Spec: *NewRoutingTreeSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *RoutingTree) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaRoutingTree = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &RoutingTree{}, &RoutingTreeList{}, resource.WithKind("RoutingTree"),
|
||||
schemaRoutingTree = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewRoutingTree(), &RoutingTreeList{}, resource.WithKind("RoutingTree"),
|
||||
resource.WithPlural("routingtrees"), resource.WithScope(resource.NamespacedScope))
|
||||
kindRoutingTree = resource.Kind{
|
||||
Schema: schemaRoutingTree,
|
||||
|
||||
@@ -23,6 +23,12 @@ type TemplateGroup struct {
|
||||
Spec TemplateGroupSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func NewTemplateGroup() *TemplateGroup {
|
||||
return &TemplateGroup{
|
||||
Spec: *NewTemplateGroupSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *TemplateGroup) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TemplateGroup{}, &TemplateGroupList{}, resource.WithKind("TemplateGroup"),
|
||||
schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewTemplateGroup(), &TemplateGroupList{}, resource.WithKind("TemplateGroup"),
|
||||
resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope))
|
||||
kindTemplateGroup = resource.Kind{
|
||||
Schema: schemaTemplateGroup,
|
||||
|
||||
@@ -23,6 +23,12 @@ type TimeInterval struct {
|
||||
Spec TimeIntervalSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func NewTimeInterval() *TimeInterval {
|
||||
return &TimeInterval{
|
||||
Spec: *NewTimeIntervalSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *TimeInterval) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TimeInterval{}, &TimeIntervalList{}, resource.WithKind("TimeInterval"),
|
||||
schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewTimeInterval(), &TimeIntervalList{}, resource.WithKind("TimeInterval"),
|
||||
resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope))
|
||||
kindTimeInterval = resource.Kind{
|
||||
Schema: schemaTimeInterval,
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/rules
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
github.com/prometheus/common v0.67.3
|
||||
k8s.io/apimachinery v0.34.2
|
||||
|
||||
@@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/annotation
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
|
||||
|
||||
@@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/collections
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
k8s.io/apimachinery v0.34.2
|
||||
|
||||
@@ -33,8 +33,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM=
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/correlations
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
|
||||
|
||||
@@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
|
||||
@@ -11,8 +11,7 @@ do-generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generati
|
||||
--tsgenpath=../../packages/grafana-schema/src/schema \
|
||||
--grouping=group \
|
||||
--defencoding=none \
|
||||
--genoperatorstate=false \
|
||||
--noschemasinmanifest
|
||||
--genoperatorstate=false
|
||||
|
||||
.PHONY: post-generate-cleanup
|
||||
post-generate-cleanup: ## Clean up the generated code
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.25.5
|
||||
require (
|
||||
cuelang.org/go v0.11.1
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
|
||||
|
||||
@@ -85,8 +85,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
|
||||
@@ -25,6 +25,13 @@ type Dashboard struct {
|
||||
Status DashboardStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
func NewDashboard() *Dashboard {
|
||||
return &Dashboard{
|
||||
Spec: *NewDashboardSpec(),
|
||||
Status: *NewDashboardStatus(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Dashboard) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"),
|
||||
schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"),
|
||||
resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope))
|
||||
kindDashboard = resource.Kind{
|
||||
Schema: schemaDashboard,
|
||||
|
||||
@@ -23,6 +23,12 @@ type Snapshot struct {
|
||||
Spec SnapshotSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func NewSnapshot() *Snapshot {
|
||||
return &Snapshot{
|
||||
Spec: *NewSnapshotSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Snapshot) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaSnapshot = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", &Snapshot{}, &SnapshotList{}, resource.WithKind("Snapshot"),
|
||||
schemaSnapshot = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", NewSnapshot(), &SnapshotList{}, resource.WithKind("Snapshot"),
|
||||
resource.WithPlural("snapshots"), resource.WithScope(resource.NamespacedScope))
|
||||
kindSnapshot = resource.Kind{
|
||||
Schema: schemaSnapshot,
|
||||
|
||||
@@ -25,6 +25,13 @@ type Dashboard struct {
|
||||
Status DashboardStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
func NewDashboard() *Dashboard {
|
||||
return &Dashboard{
|
||||
Spec: *NewDashboardSpec(),
|
||||
Status: *NewDashboardStatus(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Dashboard) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v1beta1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"),
|
||||
schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v1beta1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"),
|
||||
resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope))
|
||||
kindDashboard = resource.Kind{
|
||||
Schema: schemaDashboard,
|
||||
|
||||
@@ -25,6 +25,13 @@ type Dashboard struct {
|
||||
Status DashboardStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
func NewDashboard() *Dashboard {
|
||||
return &Dashboard{
|
||||
Spec: *NewDashboardSpec(),
|
||||
Status: *NewDashboardStatus(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Dashboard) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2alpha1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"),
|
||||
schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2alpha1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"),
|
||||
resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope))
|
||||
kindDashboard = resource.Kind{
|
||||
Schema: schemaDashboard,
|
||||
|
||||
@@ -25,6 +25,13 @@ type Dashboard struct {
|
||||
Status DashboardStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
func NewDashboard() *Dashboard {
|
||||
return &Dashboard{
|
||||
Spec: *NewDashboardSpec(),
|
||||
Status: *NewDashboardStatus(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Dashboard) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2beta1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"),
|
||||
schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2beta1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"),
|
||||
resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope))
|
||||
kindDashboard = resource.Kind{
|
||||
Schema: schemaDashboard,
|
||||
|
||||
24
apps/dashboard/pkg/apis/dashboard_manifest.go
generated
24
apps/dashboard/pkg/apis/dashboard_manifest.go
generated
File diff suppressed because one or more lines are too long
@@ -7234,9 +7234,9 @@
|
||||
"type": "elasticsearch",
|
||||
"uid": "gdev-elasticsearch"
|
||||
},
|
||||
"baseFilters": null,
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -7335,9 +7335,9 @@
|
||||
},
|
||||
"spec": {
|
||||
"name": "adhoc",
|
||||
"baseFilters": null,
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -307,9 +307,9 @@
|
||||
"kind": "AdhocVariable",
|
||||
"spec": {
|
||||
"name": "adhoc",
|
||||
"baseFilters": null,
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -317,9 +317,9 @@
|
||||
"datasource": {},
|
||||
"spec": {
|
||||
"name": "adhoc",
|
||||
"baseFilters": null,
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -604,9 +604,9 @@
|
||||
"type": "loki",
|
||||
"uid": "PDDA8E780A17E7EF1"
|
||||
},
|
||||
"baseFilters": null,
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"label": "Ad-hoc",
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
|
||||
@@ -616,9 +616,9 @@
|
||||
},
|
||||
"spec": {
|
||||
"name": "adhoc",
|
||||
"baseFilters": null,
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"label": "Ad-hoc",
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
|
||||
@@ -1885,7 +1885,7 @@
|
||||
"name": "Filters",
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -1931,7 +1931,7 @@
|
||||
"name": "Filters",
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -2184,7 +2184,7 @@
|
||||
"name": "Filters",
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -2219,7 +2219,7 @@
|
||||
"name": "Filters",
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -2499,7 +2499,7 @@
|
||||
"name": "Filters",
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -2542,7 +2542,7 @@
|
||||
"name": "Filters",
|
||||
"baseFilters": [],
|
||||
"filters": [],
|
||||
"defaultKeys": null,
|
||||
"defaultKeys": [],
|
||||
"hide": "dontHide",
|
||||
"skipUrlSync": false,
|
||||
"allowCustomValue": true
|
||||
|
||||
@@ -1539,26 +1539,38 @@ func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, comm
|
||||
},
|
||||
}
|
||||
|
||||
// Transform baseFilters if they exist
|
||||
// Transform baseFilters if they exist, otherwise default to empty array
|
||||
if baseFilters, exists := varMap["baseFilters"]; exists {
|
||||
if baseFiltersArray, ok := baseFilters.([]interface{}); ok {
|
||||
adhocVar.Spec.BaseFilters = transformAdHocFilters(baseFiltersArray)
|
||||
}
|
||||
}
|
||||
// Ensure baseFilters is always set (default to empty array if not present or invalid)
|
||||
if adhocVar.Spec.BaseFilters == nil {
|
||||
adhocVar.Spec.BaseFilters = []dashv2alpha1.DashboardAdHocFilterWithLabels{}
|
||||
}
|
||||
|
||||
// Transform filters if they exist
|
||||
// Transform filters if they exist, otherwise default to empty array
|
||||
if filters, exists := varMap["filters"]; exists {
|
||||
if filtersArray, ok := filters.([]interface{}); ok {
|
||||
adhocVar.Spec.Filters = transformAdHocFilters(filtersArray)
|
||||
}
|
||||
}
|
||||
// Ensure filters is always set (default to empty array if not present or invalid)
|
||||
if adhocVar.Spec.Filters == nil {
|
||||
adhocVar.Spec.Filters = []dashv2alpha1.DashboardAdHocFilterWithLabels{}
|
||||
}
|
||||
|
||||
// Transform defaultKeys if they exist
|
||||
// Transform defaultKeys if they exist, otherwise default to empty array
|
||||
if defaultKeys, exists := varMap["defaultKeys"]; exists {
|
||||
if defaultKeysArray, ok := defaultKeys.([]interface{}); ok {
|
||||
adhocVar.Spec.DefaultKeys = transformMetricFindValues(defaultKeysArray)
|
||||
}
|
||||
}
|
||||
// Ensure defaultKeys is always set (default to empty array if not present or invalid)
|
||||
if adhocVar.Spec.DefaultKeys == nil {
|
||||
adhocVar.Spec.DefaultKeys = []dashv2alpha1.DashboardMetricFindValue{}
|
||||
}
|
||||
|
||||
// Only include datasource if datasourceUID exists (matching frontend behavior)
|
||||
if datasourceUID != "" {
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/example
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe
|
||||
k8s.io/apimachinery v0.34.2
|
||||
|
||||
@@ -56,8 +56,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe h1:pPoFj2bQKDBg5EyEdOU+Jn+0hQN+M775Qihk73RbdSs=
|
||||
|
||||
@@ -3,42 +3,67 @@ module github.com/grafana/grafana/apps/folder
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.133.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||
github.com/go-test/deep v1.1.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.3 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/client-go v0.34.2 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -6,6 +10,8 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
|
||||
@@ -16,6 +22,8 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
@@ -23,20 +31,33 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -45,9 +66,29 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q=
|
||||
github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
@@ -58,10 +99,20 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
@@ -77,16 +128,24 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -100,12 +159,18 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
|
||||
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
|
||||
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
|
||||
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
|
||||
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
|
||||
@@ -23,6 +23,12 @@ type Folder struct {
|
||||
Spec FolderSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func NewFolder() *Folder {
|
||||
return &Folder{
|
||||
Spec: *NewFolderSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Folder) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaFolder = resource.NewSimpleSchema("folder.grafana.app", "v1beta1", &Folder{}, &FolderList{}, resource.WithKind("Folder"),
|
||||
schemaFolder = resource.NewSimpleSchema("folder.grafana.app", "v1beta1", NewFolder(), &FolderList{}, resource.WithKind("Folder"),
|
||||
resource.WithPlural("folders"), resource.WithScope(resource.NamespacedScope))
|
||||
kindFolder = resource.Kind{
|
||||
Schema: schemaFolder,
|
||||
|
||||
116
apps/folder/pkg/apis/manifestdata/folder_manifest.go
Normal file
116
apps/folder/pkg/apis/manifestdata/folder_manifest.go
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// This file is generated by grafana-app-sdk
|
||||
// DO NOT EDIT
|
||||
//
|
||||
|
||||
package manifestdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
v1beta1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
)
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "folder",
|
||||
Group: "folder.grafana.app",
|
||||
PreferredVersion: "v1beta1",
|
||||
Versions: []app.ManifestVersion{
|
||||
{
|
||||
Name: "v1beta1",
|
||||
Served: true,
|
||||
Kinds: []app.ManifestVersionKind{
|
||||
{
|
||||
Kind: "Folder",
|
||||
Plural: "Folders",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
Namespaced: map[string]spec3.PathProps{},
|
||||
Cluster: map[string]spec3.PathProps{},
|
||||
Schemas: map[string]spec.Schema{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func LocalManifest() app.Manifest {
|
||||
return app.NewEmbeddedManifest(appManifestData)
|
||||
}
|
||||
|
||||
func RemoteManifest() app.Manifest {
|
||||
return app.NewAPIServerManifest("folder")
|
||||
}
|
||||
|
||||
var kindVersionToGoType = map[string]resource.Kind{
|
||||
"Folder/v1beta1": v1beta1.FolderKind(),
|
||||
}
|
||||
|
||||
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
|
||||
// If there is no association for the provided Kind and Version, exists will return false.
|
||||
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
|
||||
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
var customRouteToGoResponseType = map[string]any{}
|
||||
|
||||
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
|
||||
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
|
||||
// If there is no association for the provided kind, version, custom route path, and method, exists will return false.
|
||||
// Resource routes (those without a kind) should prefix their route with "<namespace>/" if the route is namespaced (otherwise the route is assumed to be cluster-scope)
|
||||
func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) {
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
var customRouteToGoParamsType = map[string]runtime.Object{}
|
||||
|
||||
func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) {
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
var customRouteToGoRequestBodyType = map[string]any{}
|
||||
|
||||
func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) {
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
type GoTypeAssociator struct{}
|
||||
|
||||
func NewGoTypeAssociator() *GoTypeAssociator {
|
||||
return &GoTypeAssociator{}
|
||||
}
|
||||
|
||||
func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) {
|
||||
return ManifestGoTypeAssociator(kind, version)
|
||||
}
|
||||
func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) {
|
||||
return ManifestCustomRouteResponsesAssociator(kind, version, path, verb)
|
||||
}
|
||||
func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) {
|
||||
return ManifestCustomRouteQueryAssociator(kind, version, path, verb)
|
||||
}
|
||||
func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) {
|
||||
return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
github.com/grafana/grafana/apps/folder v0.0.0
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0
|
||||
|
||||
@@ -835,8 +835,8 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO
|
||||
github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI=
|
||||
github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe h1:q+QaVANzNZxvTovycpQvDTfsNZ2rHh4XIIaccMnrIR4=
|
||||
github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
|
||||
@@ -22,4 +22,32 @@ v0alpha1: {
|
||||
serviceaccountv0alpha1,
|
||||
externalGroupMappingv0alpha1
|
||||
]
|
||||
routes: {
|
||||
namespaced: {
|
||||
"/searchTeams": {
|
||||
"GET": {
|
||||
request: {
|
||||
query: {
|
||||
query?: string
|
||||
}
|
||||
}
|
||||
response: {
|
||||
#TeamHit: {
|
||||
name: string
|
||||
title: string
|
||||
email: string
|
||||
provisioned: bool
|
||||
externalUID: string
|
||||
}
|
||||
offset: int64
|
||||
totalHits: int64
|
||||
hits: [...#TeamHit]
|
||||
queryCost: float64
|
||||
maxScore: float64
|
||||
}
|
||||
responseMetadata: objectMeta: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_object_gen.go
generated
Normal file
33
apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_object_gen.go
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
type GetSearchTeamsRequestParamsObject struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
GetSearchTeamsRequestParams `json:",inline"`
|
||||
}
|
||||
|
||||
func NewGetSearchTeamsRequestParamsObject() *GetSearchTeamsRequestParamsObject {
|
||||
return &GetSearchTeamsRequestParamsObject{}
|
||||
}
|
||||
|
||||
func (o *GetSearchTeamsRequestParamsObject) DeepCopyObject() runtime.Object {
|
||||
dst := NewGetSearchTeamsRequestParamsObject()
|
||||
o.DeepCopyInto(dst)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (o *GetSearchTeamsRequestParamsObject) DeepCopyInto(dst *GetSearchTeamsRequestParamsObject) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
dstGetSearchTeamsRequestParams := GetSearchTeamsRequestParams{}
|
||||
_ = resource.CopyObjectInto(&dstGetSearchTeamsRequestParams, &o.GetSearchTeamsRequestParams)
|
||||
}
|
||||
|
||||
var _ runtime.Object = NewGetSearchTeamsRequestParamsObject()
|
||||
12
apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_types_gen.go
generated
Normal file
12
apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_types_gen.go
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
type GetSearchTeamsRequestParams struct {
|
||||
Query *string `json:"query,omitempty"`
|
||||
}
|
||||
|
||||
// NewGetSearchTeamsRequestParams creates a new GetSearchTeamsRequestParams object.
|
||||
func NewGetSearchTeamsRequestParams() *GetSearchTeamsRequestParams {
|
||||
return &GetSearchTeamsRequestParams{}
|
||||
}
|
||||
33
apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_body_types_gen.go
generated
Normal file
33
apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_body_types_gen.go
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Email string `json:"email"`
|
||||
Provisioned bool `json:"provisioned"`
|
||||
ExternalUID string `json:"externalUID"`
|
||||
}
|
||||
|
||||
// NewVersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit creates a new VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit object.
|
||||
func NewVersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit() *VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit {
|
||||
return &VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetSearchTeamsBody struct {
|
||||
Offset int64 `json:"offset"`
|
||||
TotalHits int64 `json:"totalHits"`
|
||||
Hits []VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit `json:"hits"`
|
||||
QueryCost float64 `json:"queryCost"`
|
||||
MaxScore float64 `json:"maxScore"`
|
||||
}
|
||||
|
||||
// NewGetSearchTeamsBody creates a new GetSearchTeamsBody object.
|
||||
func NewGetSearchTeamsBody() *GetSearchTeamsBody {
|
||||
return &GetSearchTeamsBody{
|
||||
Hits: []VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit{},
|
||||
}
|
||||
}
|
||||
37
apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_object_types_gen.go
generated
Normal file
37
apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_object_types_gen.go
generated
Normal file
@@ -0,0 +1,37 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetSearchTeams struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
GetSearchTeamsBody `json:",inline"`
|
||||
}
|
||||
|
||||
func NewGetSearchTeams() *GetSearchTeams {
|
||||
return &GetSearchTeams{}
|
||||
}
|
||||
|
||||
func (t *GetSearchTeamsBody) DeepCopyInto(dst *GetSearchTeamsBody) {
|
||||
_ = resource.CopyObjectInto(dst, t)
|
||||
}
|
||||
|
||||
func (o *GetSearchTeams) DeepCopyObject() runtime.Object {
|
||||
dst := NewGetSearchTeams()
|
||||
o.DeepCopyInto(dst)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (o *GetSearchTeams) DeepCopyInto(dst *GetSearchTeams) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
o.GetSearchTeamsBody.DeepCopyInto(&dst.GetSearchTeamsBody)
|
||||
}
|
||||
|
||||
var _ runtime.Object = NewGetSearchTeams()
|
||||
@@ -317,6 +317,7 @@ func AddAuthNKnownTypes(scheme *runtime.Scheme) error {
|
||||
&ServiceAccountList{},
|
||||
&Team{},
|
||||
&TeamList{},
|
||||
&GetSearchTeams{},
|
||||
&TeamBinding{},
|
||||
&TeamBindingList{},
|
||||
&ExternalGroupMapping{},
|
||||
|
||||
35
apps/iam/pkg/apis/iam/v0alpha1/team_search.go
Normal file
35
apps/iam/pkg/apis/iam/v0alpha1/team_search.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TeamSearchResults struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// Where the query started from
|
||||
Offset int64 `json:"offset,omitempty"`
|
||||
|
||||
// The number of matching results
|
||||
TotalHits int64 `json:"totalHits"`
|
||||
|
||||
// The team body
|
||||
Hits []TeamHit `json:"hits"`
|
||||
|
||||
// Cost of running the query
|
||||
QueryCost float64 `json:"queryCost,omitempty"`
|
||||
|
||||
// Max score
|
||||
MaxScore float64 `json:"maxScore,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
type TeamHit struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Provisioned bool `json:"provisioned,omitempty"`
|
||||
ExternalUID string `json:"externalUID,omitempty"`
|
||||
}
|
||||
177
apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go
generated
177
apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go
generated
@@ -24,6 +24,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroups": schema_pkg_apis_iam_v0alpha1_GetGroups(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroupsBody": schema_pkg_apis_iam_v0alpha1_GetGroupsBody(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeams": schema_pkg_apis_iam_v0alpha1_GetSearchTeams(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeamsBody": schema_pkg_apis_iam_v0alpha1_GetSearchTeamsBody(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": schema_pkg_apis_iam_v0alpha1_GlobalRole(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingList(ref),
|
||||
@@ -80,6 +82,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit(ref),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +567,132 @@ func schema_pkg_apis_iam_v0alpha1_GetGroupsBody(ref common.ReferenceCallback) co
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_iam_v0alpha1_GetSearchTeams(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"offset": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: 0,
|
||||
Type: []string{"integer"},
|
||||
Format: "int64",
|
||||
},
|
||||
},
|
||||
"totalHits": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: 0,
|
||||
Type: []string{"integer"},
|
||||
Format: "int64",
|
||||
},
|
||||
},
|
||||
"hits": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"queryCost": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: 0,
|
||||
Type: []string{"number"},
|
||||
Format: "double",
|
||||
},
|
||||
},
|
||||
"maxScore": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: 0,
|
||||
Type: []string{"number"},
|
||||
Format: "double",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"offset", "totalHits", "hits", "queryCost", "maxScore"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_iam_v0alpha1_GetSearchTeamsBody(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"offset": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: 0,
|
||||
Type: []string{"integer"},
|
||||
Format: "int64",
|
||||
},
|
||||
},
|
||||
"totalHits": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: 0,
|
||||
Type: []string{"integer"},
|
||||
Format: "int64",
|
||||
},
|
||||
},
|
||||
"hits": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"queryCost": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: 0,
|
||||
Type: []string{"number"},
|
||||
Format: "double",
|
||||
},
|
||||
},
|
||||
"maxScore": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: 0,
|
||||
Type: []string{"number"},
|
||||
Format: "double",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"offset", "totalHits", "hits", "queryCost", "maxScore"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_iam_v0alpha1_GlobalRole(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
@@ -2956,3 +3085,51 @@ func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseE
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"title": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"email": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"provisioned": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: false,
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"externalUID": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "title", "email", "provisioned", "externalUID"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
138
apps/iam/pkg/apis/iam_manifest.go
generated
138
apps/iam/pkg/apis/iam_manifest.go
generated
@@ -157,9 +157,139 @@ var appManifestData = app.ManifestData{
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
Namespaced: map[string]spec3.PathProps{},
|
||||
Cluster: map[string]spec3.PathProps{},
|
||||
Schemas: map[string]spec.Schema{},
|
||||
Namespaced: map[string]spec3.PathProps{
|
||||
"/searchTeams": {
|
||||
Get: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
|
||||
OperationId: "getSearchTeams",
|
||||
|
||||
Parameters: []*spec3.Parameter{
|
||||
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "query",
|
||||
In: "query",
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Responses: &spec3.Responses{
|
||||
ResponsesProps: spec3.ResponsesProps{
|
||||
Default: &spec3.Response{
|
||||
ResponseProps: spec3.ResponseProps{
|
||||
Description: "Default OK response",
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
},
|
||||
},
|
||||
"hits": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
},
|
||||
},
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
},
|
||||
},
|
||||
"maxScore": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"number"},
|
||||
},
|
||||
},
|
||||
"offset": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"integer"},
|
||||
},
|
||||
},
|
||||
"queryCost": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"number"},
|
||||
},
|
||||
},
|
||||
"totalHits": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"integer"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{
|
||||
"offset",
|
||||
"totalHits",
|
||||
"hits",
|
||||
"queryCost",
|
||||
"maxScore",
|
||||
"apiVersion",
|
||||
"kind",
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Cluster: map[string]spec3.PathProps{},
|
||||
Schemas: map[string]spec.Schema{
|
||||
"getSearchTeamsTeamHit": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"email": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
},
|
||||
},
|
||||
"externalUID": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
},
|
||||
},
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
},
|
||||
},
|
||||
"provisioned": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"boolean"},
|
||||
},
|
||||
},
|
||||
"title": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{
|
||||
"name",
|
||||
"title",
|
||||
"email",
|
||||
"provisioned",
|
||||
"externalUID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -196,6 +326,8 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist
|
||||
|
||||
var customRouteToGoResponseType = map[string]any{
|
||||
"v0alpha1|Team|groups|GET": v0alpha1.GetGroups{},
|
||||
|
||||
"v0alpha1||<namespace>/searchTeams|GET": v0alpha1.GetSearchTeams{},
|
||||
}
|
||||
|
||||
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/investigations
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
|
||||
|
||||
@@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/logsdrilldown
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
|
||||
|
||||
@@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/client-go v0.34.2
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
|
||||
@@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
|
||||
@@ -10,8 +10,9 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver
|
||||
|
||||
require (
|
||||
github.com/emicklei/go-restful/v3 v3.13.0
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4
|
||||
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@@ -59,7 +60,7 @@ require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/go-test/deep v1.1.1 // indirect
|
||||
@@ -75,9 +76,8 @@ require (
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
|
||||
@@ -142,7 +142,7 @@ require (
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/alertmanager v0.28.0 // indirect
|
||||
github.com/prometheus/alertmanager v0.28.2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.3 // indirect
|
||||
@@ -194,8 +194,8 @@ require (
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
@@ -215,3 +215,6 @@ require (
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
// Use our fork of the upstream Alertmanager.
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
|
||||
@@ -110,8 +110,8 @@ github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo
|
||||
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
|
||||
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
||||
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
@@ -174,8 +174,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 h1:nbxKRtrbuhvOYmI2RhOYauHRJCtpR+vTNIgg1lFUCws=
|
||||
github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181/go.mod h1:VtPNIFlEOJPPEc13Ax6ZTbNV3M/sAzLID72YjgzOPVA=
|
||||
github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4=
|
||||
github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
@@ -184,8 +184,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k
|
||||
github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
|
||||
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
|
||||
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
@@ -196,6 +196,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
|
||||
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
||||
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
@@ -368,8 +370,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/alertmanager v0.28.0 h1:sLN+6HhZet8hrbmGHLAHWsTXgZSVCvq9Ix3U3wvivqc=
|
||||
github.com/prometheus/alertmanager v0.28.0/go.mod h1:/okSnb2LlodbMlRoOWQEKtqI/coOo2NKZDm2Hu9QHLQ=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
@@ -611,10 +611,10 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/preferences
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.4
|
||||
github.com/grafana/grafana-app-sdk v0.48.5
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user