-
Notifications
You must be signed in to change notification settings - Fork 107
Staging #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Staging #154
Changes from all commits
77520d7
0e9a975
89a7685
00f0ecd
46afa5a
b24d2b4
47a0216
4a035ef
5b8a1bf
6accd67
e0d292c
4921a0a
4deb0f1
9b91bf9
ffef1df
483b6be
e972860
b954137
7434dc2
dd79452
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -150,7 +150,6 @@ async def google_authorized(request: Request) -> RedirectResponse: | |||||||||||||||||||
| user_info = token.get("userinfo") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if user_info: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| user_data = { | ||||||||||||||||||||
| 'id': user_info.get('id') or user_info.get('sub'), | ||||||||||||||||||||
| 'email': user_info.get('email'), | ||||||||||||||||||||
|
|
@@ -163,7 +162,7 @@ async def google_authorized(request: Request) -> RedirectResponse: | |||||||||||||||||||
| if handler: | ||||||||||||||||||||
| api_token = secrets.token_urlsafe(32) # ~43 chars, hard to guess | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # call the registered handler (await if async) | ||||||||||||||||||||
| # Call the registered handler (await if async) | ||||||||||||||||||||
| await handler('google', user_data, api_token) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| redirect = RedirectResponse(url="/chat", status_code=302) | ||||||||||||||||||||
|
|
@@ -176,9 +175,16 @@ async def google_authorized(request: Request) -> RedirectResponse: | |||||||||||||||||||
|
|
||||||||||||||||||||
| return redirect | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Handler not set - log and raise error to prevent silent failure | ||||||||||||||||||||
| logging.error("Google OAuth callback handler not registered in app state") | ||||||||||||||||||||
| raise HTTPException(status_code=500, detail="Authentication handler not configured") | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+178
to
+181
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion HTTPException raised inside try is swallowed by the broad except below. Raising here gets caught by the later 🧰 Tools🪛 Ruff (0.12.2)180-180: Abstract (TRY301) 🤖 Prompt for AI Agents |
||||||||||||||||||||
| # If we reach here, user_info was falsy | ||||||||||||||||||||
| logging.warning("No user info received from Google OAuth") | ||||||||||||||||||||
| raise HTTPException(status_code=400, detail="Failed to get user info from Google") | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+183
to
185
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Same masking risk for the “no user info” branch. This 🧰 Tools🪛 Ruff (0.12.2)184-184: Abstract (TRY301) 🤖 Prompt for AI Agents |
||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||
| logging.error(f"Google OAuth authentication failed: {str(e)}") | ||||||||||||||||||||
| raise HTTPException(status_code=400, detail=f"Authentication failed: {str(e)}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
186
to
189
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use logging.exception, avoid f-strings in logs, and don’t blanket-convert all errors to 400.
Apply this diff: - except Exception as e:
- logging.error(f"Google OAuth authentication failed: {str(e)}")
- raise HTTPException(status_code=400, detail=f"Authentication failed: {str(e)}")
+ except HTTPException:
+ # Preserve earlier explicit HTTP errors (e.g., 400/500 above)
+ raise
+ except Exception as e:
+ logging.exception("Google OAuth authentication failed: %s", e)
+ raise HTTPException(status_code=500, detail="Authentication failed")📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.12.2)186-186: Do not catch blind exception: (BLE001) 187-187: Use Replace with (TRY400) 187-187: Use explicit conversion flag Replace with conversion flag (RUF010) 188-188: Within an (B904) 188-188: Use explicit conversion flag Replace with conversion flag (RUF010) 🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -232,21 +238,19 @@ async def github_authorized(request: Request) -> RedirectResponse: | |||||||||||||||||||
| break | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if user_info: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| user_data = { | ||||||||||||||||||||
| 'id': user_info.get('id'), | ||||||||||||||||||||
| 'email': user_info.get('email'), | ||||||||||||||||||||
| 'email': email, | ||||||||||||||||||||
| 'name': user_info.get('name'), | ||||||||||||||||||||
| 'picture': user_info.get('avatar_url'), | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Call the registered GitHub callback handler if it exists to store user data. | ||||||||||||||||||||
| handler = getattr(request.app.state, "callback_handler", None) | ||||||||||||||||||||
| if handler: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| api_token = secrets.token_urlsafe(32) # ~43 chars, hard to guess | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # call the registered handler (await if async) | ||||||||||||||||||||
| # Call the registered handler (await if async) | ||||||||||||||||||||
| await handler('github', user_data, api_token) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+253
to
255
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Mirror async/sync-safe handler invocation for GitHub. Same issue as Google flow: don’t assume the handler is async. Apply this diff: - # Call the registered handler (await if async)
- await handler('github', user_data, api_token)
+ # Call the registered handler (await if async)
+ if inspect.iscoroutinefunction(handler):
+ await handler('github', user_data, api_token)
+ else:
+ handler('github', user_data, api_token)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| redirect = RedirectResponse(url="/chat", status_code=302) | ||||||||||||||||||||
|
|
@@ -259,9 +263,16 @@ async def github_authorized(request: Request) -> RedirectResponse: | |||||||||||||||||||
|
|
||||||||||||||||||||
| return redirect | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Handler not set - log and raise error to prevent silent failure | ||||||||||||||||||||
| logging.error("GitHub OAuth callback handler not registered in app state") | ||||||||||||||||||||
| raise HTTPException(status_code=500, detail="Authentication handler not configured") | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+267
to
+269
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Same masking risk: handler-not-configured 500 gets turned into 400 by the blanket except. Address by preserving 🧰 Tools🪛 Ruff (0.12.2)268-268: Abstract (TRY301) 🤖 Prompt for AI Agents |
||||||||||||||||||||
| # If we reach here, user_info was falsy | ||||||||||||||||||||
| logging.warning("No user info received from GitHub OAuth") | ||||||||||||||||||||
| raise HTTPException(status_code=400, detail="Failed to get user info from Github") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||
| logging.error(f"GitHub OAuth authentication failed: {str(e)}") | ||||||||||||||||||||
| raise HTTPException(status_code=400, detail=f"Authentication failed: {str(e)}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+275
to
277
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use logging.exception and avoid swallowing prior HTTPExceptions; prefer 500 for unexpected errors. Same improvements as Google branch. Apply this diff: - except Exception as e:
- logging.error(f"GitHub OAuth authentication failed: {str(e)}")
- raise HTTPException(status_code=400, detail=f"Authentication failed: {str(e)}")
+ except HTTPException:
+ raise
+ except Exception as e:
+ logging.exception("GitHub OAuth authentication failed: %s", e)
+ raise HTTPException(status_code=500, detail="Authentication failed")📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.12.2)275-275: Use Replace with (TRY400) 275-275: Use explicit conversion flag Replace with conversion flag (RUF010) 276-276: Within an (B904) 276-276: Use explicit conversion flag Replace with conversion flag (RUF010) 🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||
| from fastapi import APIRouter, Request, HTTPException, UploadFile, File | ||||||
| from fastapi.responses import JSONResponse, StreamingResponse | ||||||
| from pydantic import BaseModel | ||||||
| from redis import ResponseError | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect ResponseError import. redis-py exposes ResponseError under redis.exceptions. -from redis import ResponseError
+from redis.exceptions import ResponseError📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| from api.agents import AnalysisAgent, RelevancyAgent, ResponseFormatterAgent | ||||||
| from api.auth.user_management import token_required | ||||||
|
|
@@ -84,10 +85,25 @@ def sanitize_query(query: str) -> str: | |||||
| return query.replace('\n', ' ').replace('\r', ' ')[:500] | ||||||
|
|
||||||
| def sanitize_log_input(value: str) -> str: | ||||||
| """Sanitize input for safe logging (remove newlines and carriage returns).""" | ||||||
| """ | ||||||
| Sanitize input for safe logging—remove newlines, | ||||||
| carriage returns, tabs, and wrap in repr(). | ||||||
| """ | ||||||
| if not isinstance(value, str): | ||||||
| return str(value) | ||||||
| return value.replace('\n', ' ').replace('\r', ' ') | ||||||
| value = str(value) | ||||||
|
|
||||||
| return value.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ') | ||||||
|
|
||||||
| def _graph_name(request: Request, graph_id:str) -> str: | ||||||
| if not graph_id or not isinstance(graph_id, str): | ||||||
| raise HTTPException(status_code=400, detail="Invalid graph_id") | ||||||
|
|
||||||
| graph_id = graph_id.strip()[:200] | ||||||
| if not graph_id: | ||||||
| raise HTTPException(status_code=400, | ||||||
| detail="Invalid graph_id, must be less than 200 characters.") | ||||||
|
|
||||||
| return f"{request.state.user_id}_{graph_id}" | ||||||
|
|
||||||
| @graphs_router.get("") | ||||||
| @token_required | ||||||
|
|
@@ -112,12 +128,7 @@ async def get_graph_data(request: Request, graph_id: str): | |||||
| Nodes contain a minimal set of properties (id, name, labels, props). | ||||||
| Edges contain source and target node names (or internal ids), type and props. | ||||||
| """ | ||||||
| if not graph_id or not isinstance(graph_id, str): | ||||||
| return JSONResponse(content={"error": "Invalid graph_id"}, status_code=400) | ||||||
|
|
||||||
| graph_id = graph_id.strip()[:200] | ||||||
| namespaced = f"{request.state.user_id}_{graph_id}" | ||||||
|
|
||||||
| namespaced = _graph_name(request, graph_id) | ||||||
| try: | ||||||
| graph = db.select_graph(namespaced) | ||||||
| except Exception as e: | ||||||
|
|
@@ -269,16 +280,7 @@ async def query_graph(request: Request, graph_id: str, chat_data: ChatRequest): | |||||
| """ | ||||||
| text2sql | ||||||
| """ | ||||||
| # Input validation | ||||||
| if not graph_id or not isinstance(graph_id, str): | ||||||
| raise HTTPException(status_code=400, detail="Invalid graph_id") | ||||||
|
|
||||||
| # Sanitize graph_id to prevent injection | ||||||
| graph_id = graph_id.strip()[:100] # Limit length and strip whitespace | ||||||
| if not graph_id: | ||||||
| raise HTTPException(status_code=400, detail="Invalid graph_id") | ||||||
|
|
||||||
| graph_id = f"{request.state.user_id}_{graph_id}" | ||||||
| graph_id = _graph_name(request, graph_id) | ||||||
|
|
||||||
| queries_history = chat_data.chat if hasattr(chat_data, 'chat') else None | ||||||
| result_history = chat_data.result if hasattr(chat_data, 'result') else None | ||||||
|
|
@@ -553,7 +555,8 @@ async def confirm_destructive_operation( | |||||
| """ | ||||||
| Handle user confirmation for destructive SQL operations | ||||||
| """ | ||||||
| graph_id = f"{request.state.user_id}_{graph_id.strip()}" | ||||||
|
|
||||||
| graph_id = _graph_name(request, graph_id) | ||||||
|
|
||||||
| if hasattr(confirm_data, 'confirmation'): | ||||||
| confirmation = confirm_data.confirmation.strip().upper() | ||||||
|
|
@@ -674,7 +677,7 @@ async def refresh_graph_schema(request: Request, graph_id: str): | |||||
| This endpoint allows users to manually trigger a schema refresh | ||||||
| if they suspect the graph is out of sync with the database. | ||||||
| """ | ||||||
| graph_id = f"{request.state.user_id}_{graph_id.strip()}" | ||||||
| graph_id = _graph_name(request, graph_id) | ||||||
|
|
||||||
| try: | ||||||
| # Get database connection details | ||||||
|
|
@@ -716,3 +719,27 @@ async def refresh_graph_schema(request: Request, graph_id: str): | |||||
| "success": False, | ||||||
| "error": "Error refreshing schema" | ||||||
| }, status_code=500) | ||||||
|
|
||||||
| @graphs_router.delete("/{graph_id}") | ||||||
| @token_required | ||||||
| async def delete_graph(request: Request, graph_id: str): | ||||||
| """Delete the specified graph (namespaced to the user). | ||||||
|
|
||||||
| This will attempt to delete the FalkorDB graph belonging to the | ||||||
| authenticated user. The graph id used by the client is stripped of | ||||||
| namespace and will be namespaced using the user's id from the request | ||||||
| state. | ||||||
| """ | ||||||
| namespaced = _graph_name(request, graph_id) | ||||||
|
|
||||||
| try: | ||||||
| # Select and delete the graph using the FalkorDB client API | ||||||
| graph = db.select_graph(namespaced) | ||||||
| await graph.delete() | ||||||
| return JSONResponse(content={"success": True, "graph": graph_id}) | ||||||
| except ResponseError: | ||||||
| return JSONResponse(content={"error": "Failed to delete graph, Graph not found"}, | ||||||
| status_code=404) | ||||||
| except Exception as e: | ||||||
| logging.exception("Failed to delete graph %s: %s", sanitize_log_input(namespaced), e) | ||||||
| return JSONResponse(content={"error": "Failed to delete graph"}, status_code=500) | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -331,3 +331,91 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: 16px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flex-shrink: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Graph custom dropdown (moved from chat_header.j2 inline styles) */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .graph-custom-dropdown { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| position: relative; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: inline-block; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 180px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| margin-left: 8px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .graph-selected { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| padding: 8px 14px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border-radius: 6px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: var(--falkor-quaternary); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: var(--text-primary); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cursor: pointer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border: 1px solid var(--border-color); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: flex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| align-items: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| justify-content: space-between; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 100%; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| min-width: 160px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| box-sizing: border-box; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| font-size: 14px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .graph-options { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| position: absolute; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| top: calc(100%); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| left: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| right: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: var(--falkor-secondary); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border: 1px solid var(--border-color); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border-radius: 6px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border-top-left-radius: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border-top-right-radius: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max-height: 260px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overflow: auto; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: none; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| z-index: 50; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+359
to
+374
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Graph options panel z-index is too low; may render under other layers. Align with .custom-dropdown (z-index: 1000) to avoid clipping. .graph-options {
position: absolute;
top: calc(100%);
left: 0;
right: 0;
background: var(--falkor-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
border-top-left-radius: 0;
border-top-right-radius: 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-height: 260px;
overflow: auto;
display: none;
- z-index: 50;
+ z-index: 1000;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .dropdown-option { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: flex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| align-items: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| justify-content: flex-start; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| padding: 8px 12px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gap: 8px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: var(--text-primary); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cursor: pointer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .dropdown-option:hover { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: var(--bg-tertiary); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .dropdown-option span { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overflow: hidden; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text-overflow: ellipsis; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| white-space: nowrap; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .dropdown-option .delete-btn { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: transparent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border: none; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: #ff6b6b; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cursor: pointer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 28px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: 28px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: flex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| align-items: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| justify-content: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| margin-left: auto; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .dropdown-option:hover .delete-btn { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .dropdown-option .delete-btn svg { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 16px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: 16px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+376
to
+417
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Selector collision with global .dropdown-option. Scope styles to graph menu. Prevent unintended overrides of other dropdowns. -.dropdown-option {
+.graph-options .dropdown-option {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 8px 12px;
gap: 8px;
color: var(--text-primary);
cursor: pointer;
}
-.dropdown-option:hover {
+.graph-options .dropdown-option:hover {
background: var(--bg-tertiary);
}
-.dropdown-option span {
+.graph-options .dropdown-option span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
-.dropdown-option .delete-btn {
+.graph-options .dropdown-option .delete-btn {
background: transparent;
border: none;
color: #ff6b6b;
opacity: 0;
cursor: pointer;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
}
-.dropdown-option:hover .delete-btn {
+.graph-options .dropdown-option:hover .delete-btn {
opacity: 1;
}
-.dropdown-option .delete-btn svg {
+.graph-options .dropdown-option .delete-btn svg {
width: 16px;
height: 16px;
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .graph-options.open { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: block; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,9 +3,16 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| <img src="/static/icons/queryweaver.webp" alt="Chat Logo" class="logo"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1>Natural Language to SQL Generator</h1> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="button-container"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <select title="Select Database" id="graph-select"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <option value="">Loading...</option> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </select> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Custom dropdown to show per-item delete action on hover --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="graph-custom-dropdown" class="graph-custom-dropdown"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="graph-selected" class="graph-selected dropdown-selected" title="Select Database"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="dropdown-text">Select Database</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="dropdown-arrow">▼</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="graph-options" class="graph-options dropdown-options" aria-hidden="true"></div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add proper a11y semantics and a real selected value (not display text). The custom dropdown lacks keyboard support and ARIA roles, and the selected value is inferred from visible text, which will break when the placeholder (“Select Database”) is present. Store the selected value in a data attribute and apply combobox/listbox semantics. Apply: - <div id="graph-custom-dropdown" class="graph-custom-dropdown">
- <div id="graph-selected" class="graph-selected dropdown-selected" title="Select Database">
- <span class="dropdown-text">Select Database</span>
- <span class="dropdown-arrow">▼</span>
- </div>
- <div id="graph-options" class="graph-options dropdown-options" aria-hidden="true"></div>
- </div>
+ <div id="graph-custom-dropdown" class="graph-custom-dropdown">
+ <div
+ id="graph-selected"
+ class="graph-selected dropdown-selected"
+ title="Select Database"
+ role="combobox"
+ aria-haspopup="listbox"
+ aria-expanded="false"
+ aria-controls="graph-options"
+ tabindex="0"
+ data-value=""
+ >
+ <span class="dropdown-text">Select Database</span>
+ <span class="dropdown-arrow" aria-hidden="true">▼</span>
+ </div>
+ <div id="graph-options" class="graph-options dropdown-options" role="listbox" aria-hidden="true"></div>
+ </div>Follow-up: ensure the TS toggles aria-expanded/aria-hidden when opening/closing, and that getSelectedGraph() reads graph-selected.dataset.value instead of text. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- styles for the graph dropdown moved to app/public/css/menu.css --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="vertical-separator"></div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input title="Upload Schema" id="schema-upload" type="file" accept=".json" style="display: none;" tabindex="-1" disabled/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label for="schema-upload" id="custom-file-upload"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Don’t unconditionally await the callback handler (support sync and async).
await handler(...)will raise if the handler is synchronous. Detect coroutine functions and call accordingly.Apply this diff:
Add the missing import near the top-level imports:
🤖 Prompt for AI Agents