Skip to content

Commit a61dae5

Browse files
committed
feat: add volume serving plugin
1 parent d1e0808 commit a61dae5

16 files changed

Lines changed: 905 additions & 19 deletions

File tree

apps/dev-playground/client/src/appKitTypes.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import "@databricks/app-kit-ui/react";
33

44
declare module "@databricks/app-kit-ui/react" {
55
interface PluginRegistry {
6-
reconnect: {
6+
"reconnect": {
77
"/": {
88
message: string;
99
};
@@ -15,7 +15,7 @@ declare module "@databricks/app-kit-ui/react" {
1515
content: string;
1616
};
1717
}
18-
analytics: {
18+
"analytics": {
1919
"/users/me/query/:query_key": {
2020
chunk_index: number;
2121
row_offset: number;
@@ -32,6 +32,7 @@ declare module "@databricks/app-kit-ui/react" {
3232
}
3333

3434
interface QueryRegistry {
35+
3536
apps_list: {
3637
id: string;
3738
name: string;
@@ -61,5 +62,4 @@ declare module "@databricks/app-kit-ui/react" {
6162
total_cost_usd: number;
6263
}[];
6364
}
64-
6565
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { useState, useEffect, useCallback } from "react";
2+
3+
interface FileItem {
4+
name: string;
5+
path: string;
6+
isDirectory: boolean;
7+
size?: number;
8+
mimeType?: string | null;
9+
}
10+
11+
interface DirectoryListing {
12+
path: string;
13+
files: FileItem[];
14+
}
15+
16+
export function useDirectoryListing(
17+
initialPath: string = "/",
18+
onPathChange?: (path: string) => void,
19+
batchSize: number = 50,
20+
) {
21+
const [currentPath, setCurrentPath] = useState(initialPath);
22+
const [directoryListing, setDirectoryListing] =
23+
useState<DirectoryListing | null>(null);
24+
const [loading, setLoading] = useState(false);
25+
const [error, setError] = useState<string | null>(null);
26+
27+
const fetchPath = useCallback(
28+
async (path: string) => {
29+
setLoading(true);
30+
setError(null);
31+
setDirectoryListing(null);
32+
33+
// Batch-related variables accessible to both try and catch blocks
34+
const files: FileItem[] = [];
35+
let dirPath = path;
36+
let batchBuffer: FileItem[] = [];
37+
const effectiveBatchSize = Math.max(1, batchSize); // Clamp to minimum 1
38+
39+
// Flush function - centralized batch update logic
40+
const flushBatch = () => {
41+
if (batchBuffer.length > 0) {
42+
files.push(...batchBuffer);
43+
setDirectoryListing({ path: dirPath, files: [...files] });
44+
batchBuffer = [];
45+
}
46+
};
47+
48+
try {
49+
// Stream directory listing from NDJSON response
50+
const response = await fetch(`/api/volume-serving${path}`);
51+
52+
if (!response.ok) {
53+
throw new Error(`HTTP error! status: ${response.status}`);
54+
}
55+
56+
// Read the stream line by line
57+
const reader = response.body?.getReader();
58+
const decoder = new TextDecoder();
59+
let buffer = "";
60+
61+
if (reader) {
62+
while (true) {
63+
const { done, value } = await reader.read();
64+
65+
if (done) {
66+
flushBatch(); // Flush remaining files
67+
break;
68+
}
69+
70+
buffer += decoder.decode(value, { stream: true });
71+
const lines = buffer.split("\n");
72+
73+
// Process all complete lines
74+
for (let i = 0; i < lines.length - 1; i++) {
75+
const line = lines[i].trim();
76+
if (line) {
77+
const data = JSON.parse(line);
78+
79+
if (data.type === "metadata") {
80+
dirPath = data.path;
81+
} else if (data.type === "file") {
82+
const fileItem: FileItem = {
83+
name: data.name,
84+
path: data.path,
85+
isDirectory: data.isDirectory,
86+
size: data.size,
87+
mimeType: data.mimeType,
88+
};
89+
90+
batchBuffer.push(fileItem);
91+
92+
// Flush when batch is full
93+
if (batchBuffer.length >= effectiveBatchSize) {
94+
flushBatch();
95+
}
96+
}
97+
}
98+
}
99+
100+
// Keep the last incomplete line in the buffer
101+
buffer = lines[lines.length - 1];
102+
}
103+
}
104+
} catch (err) {
105+
flushBatch(); // Flush partial batch before error state
106+
setError(err instanceof Error ? err.message : "Failed to fetch path");
107+
} finally {
108+
setLoading(false);
109+
}
110+
},
111+
[batchSize],
112+
);
113+
114+
const navigateUp = () => {
115+
// Remove trailing slash if present
116+
const cleanPath =
117+
currentPath.endsWith("/") && currentPath !== "/"
118+
? currentPath.slice(0, -1)
119+
: currentPath;
120+
121+
// Get parent directory
122+
const parentPath =
123+
cleanPath.substring(0, cleanPath.lastIndexOf("/")) || "/";
124+
const normalizedParent = parentPath === "/" ? "/" : `${parentPath}/`;
125+
126+
setCurrentPath(normalizedParent);
127+
onPathChange?.(normalizedParent);
128+
fetchPath(normalizedParent);
129+
};
130+
131+
const handleNavigate = (file: FileItem) => {
132+
// Handle ".." navigation
133+
if (file.name === "..") {
134+
navigateUp();
135+
return;
136+
}
137+
138+
if (file.isDirectory) {
139+
// Navigate to directory
140+
const newPath = file.path;
141+
setCurrentPath(newPath);
142+
onPathChange?.(newPath);
143+
fetchPath(newPath);
144+
} else {
145+
// Open file in new window
146+
window.open(`/api/volume-serving${file.path}`, "_blank");
147+
}
148+
};
149+
150+
// Load directory when initialPath changes (including from URL)
151+
useEffect(() => {
152+
setCurrentPath(initialPath);
153+
fetchPath(initialPath);
154+
}, [initialPath, fetchPath]);
155+
156+
// Prepare files list with ".." entry if not in root, sorted with folders first
157+
const filesWithNavigation = directoryListing
158+
? [
159+
...(currentPath !== "/"
160+
? [
161+
{
162+
name: "..",
163+
path: "",
164+
isDirectory: true,
165+
size: undefined,
166+
mimeType: null,
167+
},
168+
]
169+
: []),
170+
// Sort: directories first, then by name
171+
...directoryListing.files.sort((a, b) => {
172+
if (a.isDirectory === b.isDirectory) {
173+
return a.name.localeCompare(b.name);
174+
}
175+
return a.isDirectory ? -1 : 1;
176+
}),
177+
]
178+
: [];
179+
180+
return {
181+
currentPath,
182+
directoryListing,
183+
loading,
184+
error,
185+
filesWithNavigation,
186+
fetchPath,
187+
navigateUp,
188+
handleNavigate,
189+
};
190+
}

apps/dev-playground/client/src/routeTree.gen.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@
99
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
1010

1111
import { Route as rootRouteImport } from "./routes/__root";
12+
import { Route as VolumeServingRouteRouteImport } from "./routes/volume-serving.route";
1213
import { Route as TelemetryRouteRouteImport } from "./routes/telemetry.route";
1314
import { Route as ReconnectRouteRouteImport } from "./routes/reconnect.route";
1415
import { Route as DataVisualizationRouteRouteImport } from "./routes/data-visualization.route";
1516
import { Route as AnalyticsRouteRouteImport } from "./routes/analytics.route";
1617
import { Route as IndexRouteImport } from "./routes/index";
1718

19+
const VolumeServingRouteRoute = VolumeServingRouteRouteImport.update({
20+
id: "/volume-serving",
21+
path: "/volume-serving",
22+
getParentRoute: () => rootRouteImport,
23+
} as any);
1824
const TelemetryRouteRoute = TelemetryRouteRouteImport.update({
1925
id: "/telemetry",
2026
path: "/telemetry",
@@ -47,13 +53,15 @@ export interface FileRoutesByFullPath {
4753
"/data-visualization": typeof DataVisualizationRouteRoute;
4854
"/reconnect": typeof ReconnectRouteRoute;
4955
"/telemetry": typeof TelemetryRouteRoute;
56+
"/volume-serving": typeof VolumeServingRouteRoute;
5057
}
5158
export interface FileRoutesByTo {
5259
"/": typeof IndexRoute;
5360
"/analytics": typeof AnalyticsRouteRoute;
5461
"/data-visualization": typeof DataVisualizationRouteRoute;
5562
"/reconnect": typeof ReconnectRouteRoute;
5663
"/telemetry": typeof TelemetryRouteRoute;
64+
"/volume-serving": typeof VolumeServingRouteRoute;
5765
}
5866
export interface FileRoutesById {
5967
__root__: typeof rootRouteImport;
@@ -62,6 +70,7 @@ export interface FileRoutesById {
6270
"/data-visualization": typeof DataVisualizationRouteRoute;
6371
"/reconnect": typeof ReconnectRouteRoute;
6472
"/telemetry": typeof TelemetryRouteRoute;
73+
"/volume-serving": typeof VolumeServingRouteRoute;
6574
}
6675
export interface FileRouteTypes {
6776
fileRoutesByFullPath: FileRoutesByFullPath;
@@ -70,16 +79,24 @@ export interface FileRouteTypes {
7079
| "/analytics"
7180
| "/data-visualization"
7281
| "/reconnect"
73-
| "/telemetry";
82+
| "/telemetry"
83+
| "/volume-serving";
7484
fileRoutesByTo: FileRoutesByTo;
75-
to: "/" | "/analytics" | "/data-visualization" | "/reconnect" | "/telemetry";
85+
to:
86+
| "/"
87+
| "/analytics"
88+
| "/data-visualization"
89+
| "/reconnect"
90+
| "/telemetry"
91+
| "/volume-serving";
7692
id:
7793
| "__root__"
7894
| "/"
7995
| "/analytics"
8096
| "/data-visualization"
8197
| "/reconnect"
82-
| "/telemetry";
98+
| "/telemetry"
99+
| "/volume-serving";
83100
fileRoutesById: FileRoutesById;
84101
}
85102
export interface RootRouteChildren {
@@ -88,10 +105,18 @@ export interface RootRouteChildren {
88105
DataVisualizationRouteRoute: typeof DataVisualizationRouteRoute;
89106
ReconnectRouteRoute: typeof ReconnectRouteRoute;
90107
TelemetryRouteRoute: typeof TelemetryRouteRoute;
108+
VolumeServingRouteRoute: typeof VolumeServingRouteRoute;
91109
}
92110

93111
declare module "@tanstack/react-router" {
94112
interface FileRoutesByPath {
113+
"/volume-serving": {
114+
id: "/volume-serving";
115+
path: "/volume-serving";
116+
fullPath: "/volume-serving";
117+
preLoaderRoute: typeof VolumeServingRouteRouteImport;
118+
parentRoute: typeof rootRouteImport;
119+
};
95120
"/telemetry": {
96121
id: "/telemetry";
97122
path: "/telemetry";
@@ -136,6 +161,7 @@ const rootRouteChildren: RootRouteChildren = {
136161
DataVisualizationRouteRoute: DataVisualizationRouteRoute,
137162
ReconnectRouteRoute: ReconnectRouteRoute,
138163
TelemetryRouteRoute: TelemetryRouteRoute,
164+
VolumeServingRouteRoute: VolumeServingRouteRoute,
139165
};
140166
export const routeTree = rootRouteImport
141167
._addFileChildren(rootRouteChildren)

apps/dev-playground/client/src/routes/__root.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ function RootComponent() {
5656
Telemetry
5757
</Button>
5858
</Link>
59+
<Link to="/volume-serving" className="no-underline">
60+
<Button
61+
variant="ghost"
62+
className="text-gray-700 hover:text-gray-900"
63+
>
64+
Volume Serving
65+
</Button>
66+
</Link>
5967
</div>
6068
</nav>
6169
</div>

apps/dev-playground/client/src/routes/index.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,24 @@ function IndexRoute() {
103103
</Button>
104104
</div>
105105
</Card>
106+
107+
<Card className="p-6 hover:shadow-lg transition-shadow cursor-pointer">
108+
<div className="flex flex-col h-full">
109+
<h3 className="text-2xl font-semibold text-gray-900 mb-3">
110+
Volume Serving
111+
</h3>
112+
<p className="text-gray-600 mb-6 flex-grow">
113+
Serve files and data directly from Databricks volumes with high
114+
performance and scalability.
115+
</p>
116+
<Button
117+
onClick={() => navigate({ to: "/volume-serving" })}
118+
className="w-full"
119+
>
120+
Explore Volume Serving
121+
</Button>
122+
</div>
123+
</Card>
106124
</div>
107125

108126
<div className="text-center pt-12 border-t border-gray-200">

apps/dev-playground/client/src/routes/telemetry.route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { createFileRoute, retainSearchParams } from "@tanstack/react-router";
2+
import { Activity, Loader2 } from "lucide-react";
3+
import { useState } from "react";
24
import { Button } from "@/components/ui/button";
35
import { Card } from "@/components/ui/card";
4-
import { useState } from "react";
5-
import { Activity, Loader2 } from "lucide-react";
66

77
export const Route = createFileRoute("/telemetry")({
88
component: TelemetryRoute,

0 commit comments

Comments
 (0)