Skip to content

Commit 5ddffda

Browse files
feat: databricks sample app
0 parents  commit 5ddffda

22 files changed

Lines changed: 4044 additions & 0 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
__pycache__
3+
.DS_Store
4+
.vscode
5+
.databricks

app.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
command: ["uvicorn", "backend.main:app"]

backend/__init__.py

Whitespace-only changes.

backend/main.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import os
2+
import logging
3+
from databricks import sql
4+
from fastapi import FastAPI, HTTPException, Request
5+
from fastapi.staticfiles import StaticFiles
6+
from fastapi.responses import FileResponse
7+
8+
host = ""
9+
http_path = ""
10+
access_token = ""
11+
12+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
13+
logger = logging.getLogger(__name__)
14+
15+
app = FastAPI(title="Simple FastAPI + React App")
16+
17+
def get_databricks_connection():
18+
"""Create and return a Databricks SQL connection"""
19+
if not access_token:
20+
raise ValueError("DATABRICKS_TOKEN environment variable not set")
21+
22+
return sql.connect(
23+
server_hostname=host,
24+
http_path=http_path,
25+
access_token=access_token
26+
)
27+
28+
@app.get("/api/health")
29+
async def health_check():
30+
logger.info("Health check at /api/health")
31+
return {"status": "healthy"}
32+
33+
@app.get("/api/classification")
34+
async def get_classification():
35+
connection = get_databricks_connection()
36+
cursor = connection.cursor()
37+
cursor.execute('SELECT resource_category, resource_type, total_count FROM cq_catalog.cloudquery.cloud_assets_counts')
38+
result = cursor.fetchall()
39+
cursor.close()
40+
connection.close()
41+
42+
data = {}
43+
for row in result:
44+
resource_category = row[0]
45+
resource_type = row[1]
46+
total_count = row[2]
47+
48+
if resource_category not in data:
49+
data[resource_category] = {
50+
"resource_category": resource_category,
51+
"total_count": 0,
52+
"types": []
53+
}
54+
55+
# Add the type to the category
56+
data[resource_category]["types"].append({
57+
"resource_type": resource_type,
58+
"total_count": total_count
59+
})
60+
61+
data[resource_category]["total_count"] += total_count
62+
63+
64+
return {
65+
"data": list(data.values()),
66+
"title": "Resource Classification in Databricks"
67+
}
68+
69+
@app.get("/api/data")
70+
async def get_data(request: Request):
71+
connection = get_databricks_connection()
72+
cursor = connection.cursor()
73+
74+
filter_param = request.query_params.get("filter", "")
75+
sorting_param = request.query_params.get("sorting", "")
76+
page = int(request.query_params.get("page", 0))
77+
page_size = int(request.query_params.get("pageSize", 25))
78+
offset = page * page_size
79+
80+
def compose_where_clause(filter_array):
81+
if not filter_array:
82+
return ""
83+
84+
conditions = []
85+
for filter_item in filter_array:
86+
field = filter_item.get("field", "")
87+
operator = filter_item.get("operator", "")
88+
value = filter_item.get("value", "")
89+
90+
if not field or not operator or value == "":
91+
continue
92+
93+
# Handle different operators
94+
if operator == "equals":
95+
conditions.append(f"`{field}` = '{value}'")
96+
elif operator == "doesNotEqual":
97+
conditions.append(f"`{field}` != '{value}'")
98+
elif operator == "contains":
99+
conditions.append(f"`{field}` LIKE '%{value}%'")
100+
# Add more operators as needed
101+
102+
if conditions:
103+
return f"WHERE {' AND '.join(conditions)}"
104+
return ""
105+
106+
def compose_order_by_clause(sort_array):
107+
if not sort_array:
108+
return ""
109+
110+
order_clauses = []
111+
for sort_item in sort_array:
112+
field = sort_item.get("field", "")
113+
sort_direction = sort_item.get("sort", "").upper()
114+
115+
if not field or sort_direction not in ["ASC", "DESC"]:
116+
continue
117+
118+
order_clauses.append(f"`{field}` {sort_direction}")
119+
120+
if order_clauses:
121+
return f"ORDER BY {', '.join(order_clauses)}"
122+
return ""
123+
124+
# Parse filter parameter (assuming it's JSON string)
125+
try:
126+
import json
127+
filter_array = json.loads(filter_param) if filter_param else []
128+
where_clause = compose_where_clause(filter_array)
129+
except (json.JSONDecodeError, TypeError):
130+
# Fallback to empty filter if parsing fails
131+
where_clause = ""
132+
133+
# Parse sort parameter (assuming it's JSON string)
134+
try:
135+
import json
136+
sort_array = json.loads(sorting_param) if sorting_param else []
137+
order_by_clause = compose_order_by_clause(sort_array)
138+
except (json.JSONDecodeError, TypeError):
139+
# Fallback to empty sort if parsing fails
140+
order_by_clause = ""
141+
142+
# Get total count first with filter
143+
count_query = f'SELECT COUNT(*) as total FROM cq_catalog.cloudquery.cloud_assets {where_clause}'
144+
cursor.execute(count_query)
145+
total_count = cursor.fetchone()[0]
146+
147+
# Get paginated data with filter and sorting
148+
data_query = f'SELECT * FROM cq_catalog.cloudquery.cloud_assets {where_clause} {order_by_clause} LIMIT {page_size} OFFSET {offset}'
149+
cursor.execute(data_query)
150+
result = cursor.fetchall()
151+
152+
# Get column names
153+
columns = [desc[0] for desc in cursor.description]
154+
155+
# Convert to list of dictionaries for JSON serialization
156+
data = []
157+
for row in result:
158+
row_dict = {}
159+
for i, value in enumerate(row):
160+
# Convert non-serializable types to strings
161+
if value is not None:
162+
try:
163+
# Handle different data types more comprehensively
164+
if hasattr(value, '__dict__'):
165+
row_dict[columns[i]] = str(value)
166+
elif isinstance(value, (dict, list, tuple)):
167+
row_dict[columns[i]] = str(value)
168+
elif isinstance(value, (int, float, str, bool)):
169+
row_dict[columns[i]] = value
170+
else:
171+
# For any other type, convert to string
172+
row_dict[columns[i]] = str(value)
173+
except (TypeError, ValueError, AttributeError):
174+
row_dict[columns[i]] = str(value)
175+
else:
176+
row_dict[columns[i]] = None
177+
data.append(row_dict)
178+
179+
cursor.close()
180+
connection.close()
181+
182+
return {
183+
"data": data,
184+
"rowCount": total_count,
185+
"title": "cloud_assets from Databricks"
186+
}
187+
188+
189+
# --- Static Files Setup ---
190+
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
191+
os.makedirs(static_dir, exist_ok=True)
192+
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")
193+
194+
# --- Catch-all for React Routes ---
195+
@app.get("/{full_path:path}")
196+
async def serve_react(full_path: str):
197+
index_html = os.path.join(static_dir, "index.html")
198+
if os.path.exists(index_html):
199+
logger.info(f"Serving React frontend for path: /{full_path}")
200+
return FileResponse(index_html)
201+
logger.error("Frontend not built. index.html missing.")
202+
raise HTTPException(
203+
status_code=404,
204+
detail="Frontend not built. Please run 'npm run build' first."
205+
)

frontend/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>React + FastAPI App</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>

frontend/src/App.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { useEffect } from "react";
2+
import CssBaseline from "@mui/material/CssBaseline";
3+
import { createTheme, ThemeProvider } from "@mui/material/styles";
4+
import Box from "@mui/material/Box";
5+
import Stack from "@mui/material/Stack";
6+
import { createThemeOptions } from "@cloudquery/cloud-ui";
7+
import { MuiChipsInput } from "mui-chips-input";
8+
import { SidePanel } from "./components/SidePanel";
9+
import { InventoryTable } from "./components/InventoryTable";
10+
import { useDatabricks } from "./hooks/useDatabricks";
11+
import { useChips } from "./hooks/useChips";
12+
13+
const cloudUITheme = createThemeOptions();
14+
const theme = createTheme(cloudUITheme);
15+
16+
function App() {
17+
const { chips, updateChipsFromFilterModel } = useChips();
18+
const {
19+
panelData,
20+
tableData,
21+
loading,
22+
columns,
23+
filterModel,
24+
paginationModel,
25+
sortModel,
26+
setFilterModel,
27+
setPaginationModel,
28+
setSortModel,
29+
selectedCategory,
30+
selectedType,
31+
rowCount,
32+
} = useDatabricks();
33+
34+
const handleManualClauseChange = (newChips: string[]) => {
35+
setFilterModel({
36+
items: newChips.map((chip) => {
37+
const [field, operator, value] = chip.split(" ");
38+
return { field, operator, value };
39+
}),
40+
});
41+
};
42+
43+
const handlePanelItemClick = (category?: string, type?: string) => {
44+
setPaginationModel({ page: 0, pageSize: 25 });
45+
setFilterModel({
46+
items: [
47+
{
48+
field: "resource_category",
49+
operator: "equals",
50+
value: category,
51+
},
52+
{ field: "resource_type", operator: "equals", value: type },
53+
],
54+
});
55+
};
56+
57+
const handleFilterModelChange = (model: any) => {
58+
setFilterModel(model);
59+
};
60+
61+
useEffect(() => {
62+
updateChipsFromFilterModel(filterModel);
63+
}, [filterModel]);
64+
65+
return (
66+
<ThemeProvider theme={theme}>
67+
<CssBaseline />
68+
<Stack direction={"row"}>
69+
<Box p={2} sx={{ width: "275px" }}>
70+
<SidePanel
71+
items={panelData?.data || []}
72+
onItemClick={handlePanelItemClick}
73+
selectedItem={{
74+
category: selectedCategory,
75+
type: selectedType,
76+
}}
77+
/>
78+
</Box>
79+
<Box p={2} sx={{ textAlign: "center", width: "80vw" }}>
80+
<MuiChipsInput
81+
value={chips}
82+
onChange={handleManualClauseChange}
83+
fullWidth
84+
/>
85+
<InventoryTable
86+
rows={tableData?.data || []}
87+
columns={columns}
88+
loading={loading}
89+
filterModel={filterModel}
90+
paginationModel={paginationModel}
91+
sortModel={sortModel}
92+
rowCount={rowCount}
93+
onFilterModelChange={handleFilterModelChange}
94+
onPaginationModelChange={setPaginationModel}
95+
onSortModelChange={setSortModel}
96+
/>
97+
</Box>
98+
</Stack>
99+
</ThemeProvider>
100+
);
101+
}
102+
103+
export default App;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use client';
2+
3+
import { useState, useRef } from 'react';
4+
5+
import Chip from '@mui/material/Chip';
6+
import Tooltip from '@mui/material/Tooltip';
7+
8+
interface ChipWithTooltipProps {
9+
[key: string]: any;
10+
label: string;
11+
onClick?: () => void;
12+
}
13+
14+
export const ChipWithTooltip = ({ label, onClick, ...props }: ChipWithTooltipProps) => {
15+
const [isClipped, setIsClipped] = useState(false);
16+
const textRef = useRef<HTMLSpanElement>(null);
17+
18+
const handleMouseEnter = () => {
19+
if (textRef.current) {
20+
const { clientWidth, scrollWidth } = textRef.current;
21+
setIsClipped(scrollWidth > clientWidth);
22+
}
23+
};
24+
25+
return (
26+
<Tooltip disableHoverListener={!isClipped} title={label}>
27+
<span>
28+
<Chip
29+
label={
30+
<span
31+
ref={textRef}
32+
style={{
33+
display: 'block',
34+
overflow: 'hidden',
35+
padding: 0,
36+
textAlign: 'left',
37+
textOverflow: 'ellipsis',
38+
whiteSpace: 'nowrap',
39+
width: '100%',
40+
}}
41+
>
42+
{label}
43+
</span>
44+
}
45+
onClick={onClick}
46+
onMouseEnter={handleMouseEnter}
47+
sx={{ ...props?.sx, padding: 0 }}
48+
{...props}
49+
/>
50+
</span>
51+
</Tooltip>
52+
);
53+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.MuiDataGrid-filterFormLogicOperatorInput {
2+
display: none !important;
3+
}

0 commit comments

Comments
 (0)