diff --git a/.github/workflows/admin-sourcemaps.yml b/.github/workflows/admin-sourcemaps.yml index 175f7f8794..fd367ce27c 100644 --- a/.github/workflows/admin-sourcemaps.yml +++ b/.github/workflows/admin-sourcemaps.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3 name: Checkout code - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.8 - uses: actions/setup-node@v3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5ea93fcfd..5ba8499a13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,10 +37,10 @@ jobs: with: app_id: ${{ vars.SENTRY_INTERNAL_APP_ID }} private_key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: cache-epoch-1|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} @@ -76,7 +76,7 @@ jobs: # If working tree is dirty, commit and update if we have a token - name: Apply any pre-commit fixed files if: steps.token.outcome == 'success' && github.ref != 'refs/heads/master' && always() - uses: getsentry/action-github-commit@v1.0.0 + uses: getsentry/action-github-commit@v2.1.0 with: github-token: ${{ steps.token.outputs.token }} @@ -98,7 +98,7 @@ jobs: steps: - uses: actions/checkout@v3 name: Checkout code - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies @@ -116,7 +116,7 @@ jobs: steps: - uses: actions/checkout@v3 name: Checkout code - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies @@ -153,7 +153,7 @@ jobs: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - name: Build snuba docker image for CI - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 if: github.repository_owner == 'getsentry' with: context: . diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 27c89e764b..5ac403a5c5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -37,7 +37,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: config-file: ./.github/codeql/codeql-config.yml languages: ${{ matrix.language }} @@ -49,7 +49,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -63,4 +63,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/ddl-changes.yml b/.github/workflows/ddl-changes.yml index 8e57a2a52b..182bfd6c06 100644 --- a/.github/workflows/ddl-changes.yml +++ b/.github/workflows/ddl-changes.yml @@ -20,7 +20,7 @@ jobs: with: clean: false fetch-depth: 200 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index 4bc10e9b9f..08d187ab14 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.8' - name: Generate config schema docs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index beb953664e..c5985e40fe 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.8' - name: Generate config schema docs @@ -22,7 +22,7 @@ jobs: - name: Build docs run: | make snubadocs - - uses: peaceiris/actions-gh-pages@v3.7.3 + - uses: peaceiris/actions-gh-pages@v4.0.0 name: Publish to GitHub Pages with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..547d950c82 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-python.black-formatter", + "ms-python.mypy-type-checker", + "ms-python.flake8" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index a2eb678fc1..17f7235bdf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,21 +26,25 @@ }, "[javascript]": { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "editor.tabSize": 2 }, "[javascriptreact]": { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "editor.tabSize": 2 }, "[typescript]": { "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2 }, "[typescriptreact]": { "editor.formatOnSave": true, - "editor.defaultFormatter": "vscode.typescript-language-features" + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.tabSize": 2 }, "[less]": { @@ -56,11 +60,7 @@ "editor.formatOnSave": true }, - "python.linting.mypyEnabled": true, - "python.linting.flake8Enabled": true, - "python.formatting.provider": "black", - // https://github.com/DonJayamanne/pythonVSCode/issues/992 - "python.pythonPath": ".venv/bin/python", + "python.defaultInterpreterPath": ".venv/bin/python", // test discovery is sluggish and the UI around running // tests is often in your way and misclicked "python.testing.pytestEnabled": true, diff --git a/Dockerfile b/Dockerfile index 0fe9af0e03..7260e6f749 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG PYTHON_VERSION=3.11.8 -FROM python:${PYTHON_VERSION}-slim-bookworm as build_base +FROM python:${PYTHON_VERSION}-slim-bookworm AS build_base WORKDIR /usr/src/snuba ENV PIP_NO_CACHE_DIR=off \ @@ -150,7 +150,7 @@ EXPOSE 1218 1219 ENTRYPOINT [ "./docker_entrypoint.sh" ] CMD [ "api" ] -FROM application_base as application +FROM application_base AS application USER 0 RUN set -ex; \ apt-get purge -y --auto-remove $(cat /tmp/build-deps.txt); \ diff --git a/requirements-test.txt b/requirements-test.txt index e6def11b02..5ed8f57f6f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -7,7 +7,7 @@ mypy==1.1.1 types-python-dateutil==2.8.19 types-python-jose==3.3.0 types-pyyaml==6.0.11 -types-requests==2.28.10 +types-requests==2.32.0.20240907 types-setuptools==65.3.0 types-simplejson==3.17.7 types-google-cloud-ndb==2.2.0 diff --git a/requirements.txt b/requirements.txt index a80d4bc316..2caeb65219 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ clickhouse-driver==0.2.6 confluent-kafka==2.3.0 datadog==0.21.0 devservices==0.0.4 -flake8==5.0.4 +flake8==7.0.0 Flask==2.2.5 google-cloud-storage==2.18.0 googleapis-common-protos==1.63.2 @@ -28,9 +28,9 @@ python-dateutil==2.8.2 python-rapidjson==1.8 redis==4.5.4 sentry-arroyo==2.17.6 -sentry-kafka-schemas==0.1.115 +sentry-kafka-schemas==0.1.117 sentry-redis-tools==0.3.0 -sentry-relay==0.8.44 +sentry-relay==0.9.2 sentry-sdk==2.8.0 simplejson==3.17.6 snuba-sdk==3.0.39 @@ -38,7 +38,7 @@ structlog==22.3.0 structlog-sentry==2.0.0 sql-metadata==2.11.0 typing-extensions==4.8.0 -urllib3==1.26.12 +urllib3==2.2.3 pyuwsgi==2.0.23 Werkzeug==3.0.5 PyYAML==6.0 @@ -46,4 +46,4 @@ sqlparse==0.5.0 google-api-python-client==2.88.0 sentry-usage-accountant==0.0.10 freezegun==1.2.2 -sentry-protos==0.1.32 +sentry-protos==0.1.34 diff --git a/rust_snuba/src/mutations/factory.rs b/rust_snuba/src/mutations/factory.rs index 266e2c2f93..3b2c88ac73 100644 --- a/rust_snuba/src/mutations/factory.rs +++ b/rust_snuba/src/mutations/factory.rs @@ -102,7 +102,7 @@ impl ProcessingStrategyFactory for MutConsumerStrategyFactory { ); let mut synchronizer = Synchronizer { - min_delay: TimeDelta::hours(48), + min_delay: TimeDelta::hours(1), }; let next_step = RunTask::new(move |m| synchronizer.process_message(m), next_step); diff --git a/rust_snuba/src/processors/eap_spans.rs b/rust_snuba/src/processors/eap_spans.rs index e388b1905b..3733f28335 100644 --- a/rust_snuba/src/processors/eap_spans.rs +++ b/rust_snuba/src/processors/eap_spans.rs @@ -29,9 +29,16 @@ pub fn process_message( _metadata: KafkaMessageMetadata, config: &ProcessorConfig, ) -> anyhow::Result { + if let Some(headers) = payload.headers() { + if let Some(ingest_in_eap) = headers.get("ingest_in_eap") { + if ingest_in_eap == b"false" { + return Ok(InsertBatch::skip()); + } + } + } + let payload_bytes = payload.payload().context("Expected payload")?; let msg: FromSpanMessage = serde_json::from_slice(payload_bytes)?; - let origin_timestamp = DateTime::from_timestamp(msg.received as i64, 0); let mut span: EAPSpan = msg.try_into()?; diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__snuba-spans-.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__snuba-spans-.snap deleted file mode 100644 index cc1e1d0d1c..0000000000 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__snuba-spans-.snap +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: src/processors/mod.rs -expression: diff ---- -[ - Change { - path: "", - change: PropertyAdd { - lhs_additional_properties: true, - added: "data", - }, - }, - Change { - path: "", - change: RequiredAdd { - property: "end_timestamp_precise", - }, - }, - Change { - path: "", - change: RequiredAdd { - property: "organization_id", - }, - }, - Change { - path: "", - change: RequiredAdd { - property: "start_timestamp_precise", - }, - }, -] diff --git a/snuba/admin/clickhouse/common.py b/snuba/admin/clickhouse/common.py index 17b3029dac..ba9bc4c61f 100644 --- a/snuba/admin/clickhouse/common.py +++ b/snuba/admin/clickhouse/common.py @@ -58,7 +58,12 @@ def _validate_node( if not is_valid_node(clickhouse_host, clickhouse_port, cluster, storage_name): raise InvalidNodeError( f"host {clickhouse_host} and port {clickhouse_port} are not valid", - extra_data={"host": clickhouse_host, "port": clickhouse_port}, + extra_data={ + "host": clickhouse_host, + "port": clickhouse_port, + "query_host": cluster.get_query_node().host_name, + "query_port": cluster.get_query_node().port, + }, ) diff --git a/snuba/admin/clickhouse/profile_events.py b/snuba/admin/clickhouse/profile_events.py new file mode 100644 index 0000000000..44533cf54f --- /dev/null +++ b/snuba/admin/clickhouse/profile_events.py @@ -0,0 +1,98 @@ +import json +import socket +import time +from typing import Dict, List, cast + +import structlog +from flask import g + +from snuba.admin.clickhouse.system_queries import run_system_query_on_host_with_sql +from snuba.admin.clickhouse.tracing import QueryTraceData, TraceOutput +from snuba.utils.constants import ( + PROFILE_EVENTS_MAX_ATTEMPTS, + PROFILE_EVENTS_MAX_WAIT_SECONDS, +) + +logger = structlog.get_logger().bind(module=__name__) + + +def gather_profile_events(query_trace: TraceOutput, storage: str) -> None: + """ + Gathers profile events for each query trace and updates the query_trace object with results. + Uses exponential backoff when polling for results. + + Args: + query_trace: TraceOutput object to update with profile events + storage: Storage identifier + """ + profile_events_raw_sql = "SELECT ProfileEvents FROM system.query_log WHERE query_id = '{}' AND type = 'QueryFinish'" + + for query_trace_data in parse_trace_for_query_ids(query_trace): + sql = profile_events_raw_sql.format(query_trace_data.query_id) + logger.info( + "Gathering profile event using host: {}, port = {}, storage = {}, sql = {}, g.user = {}".format( + query_trace_data.host, query_trace_data.port, storage, sql, g.user + ) + ) + + system_query_result = None + attempt = 0 + wait_time = 1 + while attempt < PROFILE_EVENTS_MAX_ATTEMPTS: + system_query_result = run_system_query_on_host_with_sql( + query_trace_data.host, + int(query_trace_data.port), + storage, + sql, + False, + g.user, + ) + + if system_query_result.results: + break + + wait_time = min(wait_time * 2, PROFILE_EVENTS_MAX_WAIT_SECONDS) + time.sleep(wait_time) + attempt += 1 + + if system_query_result is not None and len(system_query_result.results) > 0: + query_trace.profile_events_meta.append(system_query_result.meta) + query_trace.profile_events_profile = cast( + Dict[str, int], system_query_result.profile + ) + columns = system_query_result.meta + if columns: + res = {} + res["column_names"] = [name for name, _ in columns] + res["rows"] = [] + for query_result in system_query_result.results: + if query_result[0]: + res["rows"].append(json.dumps(query_result[0])) + query_trace.profile_events_results[query_trace_data.node_name] = res + + +def hostname_resolves(hostname: str) -> bool: + try: + socket.gethostbyname(hostname) + except socket.error: + return False + else: + return True + + +def parse_trace_for_query_ids(trace_output: TraceOutput) -> List[QueryTraceData]: + summarized_trace_output = trace_output.summarized_trace_output + node_name_to_query_id = { + node_name: query_summary.query_id + for node_name, query_summary in summarized_trace_output.query_summaries.items() + } + logger.info("node to query id mapping: {}".format(node_name_to_query_id)) + return [ + QueryTraceData( + host=node_name if hostname_resolves(node_name) else "127.0.0.1", + port=9000, + query_id=query_id, + node_name=node_name, + ) + for node_name, query_id in node_name_to_query_id.items() + ] diff --git a/snuba/admin/clickhouse/system_queries.py b/snuba/admin/clickhouse/system_queries.py index 9d7754e5fe..2048696a92 100644 --- a/snuba/admin/clickhouse/system_queries.py +++ b/snuba/admin/clickhouse/system_queries.py @@ -49,23 +49,6 @@ def _run_sql_query_on_host( return query_result -SYSTEM_QUERY_RE = re.compile( - r""" - ^ # Start - (SELECT|select) - \s - (?P[\w\s\',()*+\-\/:]+|\*) - \s - (FROM|from) - \s - system.[a-z_]+ - (?P\s[\w\s,=()*+<>'%"\-\/:]+)? - ;? # Optional semicolon - $ # End - """, - re.VERBOSE, -) - DESCRIBE_QUERY_RE = re.compile( r""" ^ # Start @@ -129,13 +112,60 @@ def _run_sql_query_on_host( ) -def is_query_select(sql_query: str) -> bool: +def is_query_using_only_system_tables( + clickhouse_host: str, + clickhouse_port: int, + storage_name: str, + sql_query: str, +) -> bool: """ - Simple validation to ensure query is a select command + Run the EXPLAIN QUERY TREE on the given sql_query and check that the only tables + in the query are system tables. """ - sql_query = " ".join(sql_query.split()) - match = SYSTEM_QUERY_RE.match(sql_query) - return True if match else False + sql_query = sql_query.strip().rstrip(";") if sql_query.endswith(";") else sql_query + explain_query_tree_query = ( + f"EXPLAIN QUERY TREE {sql_query} SETTINGS allow_experimental_analyzer = 1" + ) + explain_query_tree_result = _run_sql_query_on_host( + clickhouse_host, clickhouse_port, storage_name, explain_query_tree_query, False + ) + + for line in explain_query_tree_result.results: + line = line[0].strip() + # We don't allow table functions for now as the clickhouse analyzer isn't good enough yet to resolve those tables + if line.startswith("TABLE_FUNCTION"): + return False + if line.startswith("TABLE"): + match = re.search(r"table_name:\s*(\S+)", line, re.IGNORECASE) + if match: + table_name = match.group(1) + if not table_name.startswith("system."): + return False + + return True + + +def is_valid_system_query( + clickhouse_host: str, clickhouse_port: int, storage_name: str, sql_query: str +) -> bool: + """ + Validation based on Query Tree and AST to ensure the query is a valid select query. + """ + explain_ast_query = f"EXPLAIN AST {sql_query}" + disallowed_ast_nodes = ["AlterQuery", "AlterCommand", "DropQuery", "InsertQuery"] + explain_ast_result = _run_sql_query_on_host( + clickhouse_host, clickhouse_port, storage_name, explain_ast_query, False + ) + + for node in disallowed_ast_nodes: + if any( + line[0].lstrip().startswith(node) for line in explain_ast_result.results + ): + return False + + return is_query_using_only_system_tables( + clickhouse_host, clickhouse_port, storage_name, sql_query + ) def is_query_show(sql_query: str) -> bool: @@ -183,6 +213,33 @@ def is_query_alter(sql_query: str) -> bool: return True if match else False +def validate_query( + clickhouse_host: str, + clickhouse_port: int, + storage_name: str, + system_query_sql: str, + sudo_mode: bool, +) -> None: + if is_query_describe(system_query_sql) or is_query_show(system_query_sql): + return + + if sudo_mode and ( + is_system_command(system_query_sql) + or is_query_alter(system_query_sql) + or is_query_optimize(system_query_sql) + ): + return + + if is_valid_system_query( + clickhouse_host, clickhouse_port, storage_name, system_query_sql + ): + if sudo_mode: + raise InvalidCustomQuery("Query is valid but sudo is not allowed") + return + + raise InvalidCustomQuery("Query is invalid") + + def run_system_query_on_host_with_sql( clickhouse_host: str, clickhouse_port: int, @@ -200,20 +257,9 @@ def run_system_query_on_host_with_sql( if not can_sudo: raise UnauthorizedForSudo() - if is_query_select(system_query_sql): - validate_system_query(system_query_sql) - elif is_query_describe(system_query_sql): - pass - elif is_query_show(system_query_sql): - pass - elif sudo_mode and ( - is_system_command(system_query_sql) - or is_query_alter(system_query_sql) - or is_query_optimize(system_query_sql) - ): - pass - else: - raise InvalidCustomQuery("Query is invalid") + validate_query( + clickhouse_host, clickhouse_port, storage_name, system_query_sql, sudo_mode + ) try: return _run_sql_query_on_host( @@ -240,36 +286,3 @@ def run_system_query_on_host_with_sql( }, notify=sudo_mode, ) - - -def validate_system_query(sql_query: str) -> None: - """ - Simple validation to ensure query only attempts to access system tables and not - any others. Will be replaced by AST parser eventually. - - Raises InvalidCustomQuery if query is invalid or not allowed. - """ - sql_query = " ".join(sql_query.split()) - - disallowed_keywords = ["select", "insert", "join"] - - match = SYSTEM_QUERY_RE.match(sql_query) - - if match is None: - raise InvalidCustomQuery("Query is invalid") - - select_statement = match.group("select_statement") - - # Extremely quick and dirty way of ensuring there is not a nested select, insert or a join - for kw in disallowed_keywords: - if kw in select_statement.lower(): - raise InvalidCustomQuery(f"{kw} is not allowed here") - - extra = match.group("extra") - - # Unfortunately "extra" is pretty permissive right now, just ensure - # there is no attempt to do a select, insert or join in there - if extra is not None: - for kw in disallowed_keywords: - if kw in extra.lower(): - raise InvalidCustomQuery(f"{kw} is not allowed here") diff --git a/snuba/admin/package.json b/snuba/admin/package.json index 27675c97d0..add393e490 100644 --- a/snuba/admin/package.json +++ b/snuba/admin/package.json @@ -56,11 +56,11 @@ "bootstrap": "^5.2.3", "jest": "^29.4.3", "jest-environment-jsdom": "^29.5.0", - "react-bootstrap": "^2.7.4", + "react-bootstrap": "^2.10.5", "resize-observer-polyfill": "^1.5.1", - "ts-jest": "^29.1.2", + "ts-jest": "^29.2.5", "use-resize-observer": "^9.1.0", - "webpack": "^5.95.0", + "webpack": "^5.96.1", "webpack-cli": "^4.10.0" }, "volta": { diff --git a/snuba/admin/static/api_client.tsx b/snuba/admin/static/api_client.tsx index fc31ec58a8..ecc5148d8c 100644 --- a/snuba/admin/static/api_client.tsx +++ b/snuba/admin/static/api_client.tsx @@ -72,7 +72,7 @@ interface Client { executeTracingQuery: (req: TracingRequest) => Promise; getKafkaData: () => Promise; getRpcEndpoints: () => Promise>; - executeRpcEndpoint: (endpointName: string, version: string, requestBody: any) => Promise; + executeRpcEndpoint: (endpointName: string, version: string, requestBody: any, signal?: AbortSignal) => Promise; getPredefinedQuerylogOptions: () => Promise<[PredefinedQuery]>; getQuerylogSchema: () => Promise; executeQuerylogQuery: (req: QuerylogRequest) => Promise; @@ -114,6 +114,7 @@ interface Client { runJob(job_id: string): Promise; getJobLogs(job_id: string): Promise; getClickhouseSystemSettings: (host: string, port: number, storage: string) => Promise; + summarizeTraceWithProfile: (traceLogs: string, spanType: string, signal?: AbortSignal) => Promise; } function Client(): Client { @@ -335,7 +336,7 @@ function Client(): Client { }).then((resp) => resp.json()) as Promise>; }, - executeRpcEndpoint: async (endpointName: string, version: string, requestBody: any) => { + executeRpcEndpoint: async (endpointName: string, version: string, requestBody: any, signal?: AbortSignal) => { try { const url = `${baseUrl}rpc_execute/${endpointName}/${version}`; const response = await fetch(url, { @@ -345,6 +346,7 @@ function Client(): Client { Accept: "application/json" }, body: JSON.stringify(requestBody), + signal, }); if (!response.ok) { const errorData = await response.json(); @@ -594,6 +596,24 @@ function Client(): Client { } }); }, + summarizeTraceWithProfile: (traceLogs: string, storage: string, signal?: AbortSignal) => { + const url = baseUrl + "rpc_summarize_trace_with_profile"; + return fetch(url, { + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify({ + trace_logs: traceLogs, + storage: storage + }), + signal, + }).then((resp) => { + if (resp.ok) { + return resp.json(); + } else { + return resp.json().then(Promise.reject.bind(Promise)); + } + }); + }, }; } diff --git a/snuba/admin/static/rpc_endpoints/endpoint_selector.tsx b/snuba/admin/static/rpc_endpoints/endpoint_selector.tsx new file mode 100644 index 0000000000..1d823567f4 --- /dev/null +++ b/snuba/admin/static/rpc_endpoints/endpoint_selector.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Select } from '@mantine/core'; +import { EndpointSelectorProps } from 'SnubaAdmin/rpc_endpoints/types'; + +export const EndpointSelector = ({ + endpoints, + selectedEndpoint, + handleEndpointSelect +}: EndpointSelectorProps) => ( + <> +

RPC Endpoints

+ ({ value: endpoint.name, label: `${endpoint.name} (${endpoint.version})` }))} - value={selectedEndpoint} - onChange={handleEndpointSelect} - style={{ width: '100%', marginBottom: '1rem' }} - /> - - setAccordionOpened(value === 'example')} - > - - Example Request Payload - - -
-                                {JSON.stringify(
-                                    selectedEndpoint && selectedVersion
-                                        ? exampleRequestTemplates[selectedEndpoint]?.[selectedVersion] || exampleRequestTemplates.default
-                                        : exampleRequestTemplates.default,
-                                    null,
-                                    2
-                                )}
-                            
-
- -
-
-
- -