This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris-tools.git
The following commit(s) were added to refs/heads/main by this push:
new 126959c Fix Some Generic Tables API Issues (#112)
126959c is described below
commit 126959c6064973afddc30ddc43cac88ca3051902
Author: Adam Christian
<[email protected]>
AuthorDate: Sat Dec 20 01:33:14 2025 -0500
Fix Some Generic Tables API Issues (#112)
---
console/docker/nginx.conf | 19 ++++
console/src/api/catalog/tables.ts | 39 +++++++
console/src/pages/NamespaceDetails.tsx | 8 --
console/src/pages/TableDetails.tsx | 202 +++++++++++++++++++++++++++------
console/vite.config.ts | 21 ++++
5 files changed, 246 insertions(+), 43 deletions(-)
diff --git a/console/docker/nginx.conf b/console/docker/nginx.conf
index aeaca69..ff9da4a 100644
--- a/console/docker/nginx.conf
+++ b/console/docker/nginx.conf
@@ -50,6 +50,25 @@ server {
proxy_read_timeout 60s;
}
+ # Proxy Polaris-specific API requests to Polaris backend
+ # Rewrite /polaris/v1/* to /api/catalog/polaris/v1/*
+ location /polaris/ {
+ rewrite ^/polaris/(.*)$ /api/catalog/polaris/$1 break;
+ proxy_pass ${VITE_POLARIS_API_URL};
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Disable buffering for streaming responses
+ proxy_buffering off;
+
+ # Timeouts
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 60s;
+ proxy_read_timeout 60s;
+ }
+
# Serve static files
location / {
try_files $uri $uri/ /index.html;
diff --git a/console/src/api/catalog/tables.ts
b/console/src/api/catalog/tables.ts
index 06a2c72..5a22e2f 100644
--- a/console/src/api/catalog/tables.ts
+++ b/console/src/api/catalog/tables.ts
@@ -194,6 +194,26 @@ export const tablesApi = {
}))
},
+ /**
+ * Get generic table details.
+ * @param prefix - The catalog name
+ * @param namespace - Namespace array (e.g., ["accounting", "tax"])
+ * @param tableName - Table name
+ */
+ getGeneric: async (
+ prefix: string,
+ namespace: string[],
+ tableName: string
+ ): Promise<unknown> => {
+ const namespaceStr = encodeNamespace(namespace)
+ const response = await apiClient
+ .getPolarisClient()
+ .get<unknown>(
+
`/${encodeURIComponent(prefix)}/namespaces/${encodeURIComponent(namespaceStr)}/generic-tables/${encodeURIComponent(tableName)}`
+ )
+ return response.data
+ },
+
/**
* Create a generic table.
* @param prefix - The catalog name
@@ -214,5 +234,24 @@ export const tablesApi = {
)
return response.data
},
+
+ /**
+ * Delete a generic table.
+ * @param prefix - The catalog name
+ * @param namespace - Namespace array
+ * @param tableName - Table name
+ */
+ deleteGeneric: async (
+ prefix: string,
+ namespace: string[],
+ tableName: string
+ ): Promise<void> => {
+ const namespaceStr = encodeNamespace(namespace)
+ await apiClient
+ .getPolarisClient()
+ .delete(
+
`/${encodeURIComponent(prefix)}/namespaces/${encodeURIComponent(namespaceStr)}/generic-tables/${encodeURIComponent(tableName)}`
+ )
+ },
}
diff --git a/console/src/pages/NamespaceDetails.tsx
b/console/src/pages/NamespaceDetails.tsx
index 66e87ed..1bc08db 100644
--- a/console/src/pages/NamespaceDetails.tsx
+++ b/console/src/pages/NamespaceDetails.tsx
@@ -445,7 +445,6 @@ export function NamespaceDetails() {
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
- <TableHead>Created</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
@@ -467,13 +466,6 @@ export function NamespaceDetails() {
{table.type || "Generic"}
</span>
</TableCell>
- <TableCell>
- <span className="text-muted-foreground text-sm">
- {table.createTime
- ? formatDistanceToNow(new Date(table.createTime),
{ addSuffix: true })
- : "Unknown"}
- </span>
- </TableCell>
<TableCell>
<Button
variant="ghost"
diff --git a/console/src/pages/TableDetails.tsx
b/console/src/pages/TableDetails.tsx
index 0f04002..cd02d14 100644
--- a/console/src/pages/TableDetails.tsx
+++ b/console/src/pages/TableDetails.tsx
@@ -19,7 +19,7 @@
import { useState } from "react"
import { useParams, useNavigate, Link } from "react-router-dom"
-import { useQuery } from "@tanstack/react-query"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { ArrowLeft, RefreshCw, Table as TableIcon, Pencil, Trash2 } from
"lucide-react"
import { tablesApi } from "@/api/catalog/tables"
import { catalogsApi } from "@/api/management/catalogs"
@@ -32,6 +32,7 @@ import { MetadataViewer } from
"@/components/table/MetadataViewer"
import { RenameTableModal } from "@/components/forms/RenameTableModal"
import { EditTablePropertiesModal } from
"@/components/forms/EditTablePropertiesModal"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader,
DialogTitle } from "@/components/ui/dialog"
+import { toast } from "sonner"
export function TableDetails() {
const { catalogName, namespace: namespaceParam, tableName } = useParams<{
@@ -41,6 +42,7 @@ export function TableDetails() {
}>()
const navigate = useNavigate()
+ const queryClient = useQueryClient()
const namespaceArray = namespaceParam?.split(".") || []
@@ -58,7 +60,20 @@ export function TableDetails() {
const tableQuery = useQuery({
queryKey: ["table", catalogName, namespaceArray.join("."), tableName],
- queryFn: () => tablesApi.get(catalogName!, namespaceArray, tableName!),
+ queryFn: async () => {
+ // Try to fetch as an Iceberg table first
+ try {
+ return await tablesApi.get(catalogName!, namespaceArray, tableName!)
+ } catch (icebergError) {
+ // If that fails, try to fetch as a generic table
+ try {
+ return await tablesApi.getGeneric(catalogName!, namespaceArray,
tableName!)
+ } catch (genericError) {
+ // If both fail, throw the original Iceberg error
+ throw icebergError
+ }
+ }
+ },
enabled: !!catalogName && namespaceArray.length > 0 && !!tableName,
})
@@ -75,7 +90,51 @@ export function TableDetails() {
const refreshDisabled = tableQuery.isFetching || namespaceQuery.isFetching
|| catalogQuery.isFetching
const tableData = tableQuery.data
- const currentSchema = tableData?.metadata.schemas.find(
+
+ // Check if this is a generic table (has 'table' property) or Iceberg table
(has 'metadata' property)
+ const isGenericTable = tableData && 'table' in tableData
+ const genericTableData = isGenericTable ? (tableData as any).table : null
+
+ // Delete mutations
+ const deleteIcebergMutation = useMutation({
+ mutationFn: () => tablesApi.delete(catalogName!, namespaceArray,
tableName!),
+ onSuccess: () => {
+ toast.success("Table deleted successfully")
+ queryClient.invalidateQueries({ queryKey: ["tables", catalogName,
namespaceArray] })
+ queryClient.invalidateQueries({ queryKey: ["namespace", catalogName,
namespaceArray] })
+
navigate(`/catalogs/${encodeURIComponent(catalogName!)}/namespaces/${encodeURIComponent(namespaceParam!)}`)
+ },
+ onError: (error: Error) => {
+ toast.error("Failed to delete table", {
+ description: error.message || "An error occurred",
+ })
+ },
+ })
+
+ const deleteGenericMutation = useMutation({
+ mutationFn: () => tablesApi.deleteGeneric(catalogName!, namespaceArray,
tableName!),
+ onSuccess: () => {
+ toast.success("Generic table deleted successfully")
+ queryClient.invalidateQueries({ queryKey: ["generic-tables",
catalogName, namespaceArray] })
+ queryClient.invalidateQueries({ queryKey: ["namespace", catalogName,
namespaceArray] })
+
navigate(`/catalogs/${encodeURIComponent(catalogName!)}/namespaces/${encodeURIComponent(namespaceParam!)}`)
+ },
+ onError: (error: Error) => {
+ toast.error("Failed to delete generic table", {
+ description: error.message || "An error occurred",
+ })
+ },
+ })
+
+ const handleDelete = () => {
+ if (isGenericTable) {
+ deleteGenericMutation.mutate()
+ } else {
+ deleteIcebergMutation.mutate()
+ }
+ }
+
+ const currentSchema = !isGenericTable && tableData?.metadata?.schemas?.find(
(s) => s["schema-id"] === tableData.metadata["current-schema-id"]
)
@@ -132,12 +191,16 @@ export function TableDetails() {
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
- <Button variant="outline" onClick={() => setRenameOpen(true)}
disabled={!tableData}>
- <Pencil className="mr-2 h-4 w-4" /> Rename
- </Button>
- <Button variant="outline" onClick={() => setEditPropsOpen(true)}
disabled={!tableData}>
- Edit Properties
- </Button>
+ {!isGenericTable && (
+ <>
+ <Button variant="outline" onClick={() => setRenameOpen(true)}
disabled={!tableData}>
+ <Pencil className="mr-2 h-4 w-4" /> Rename
+ </Button>
+ <Button variant="outline" onClick={() => setEditPropsOpen(true)}
disabled={!tableData}>
+ Edit Properties
+ </Button>
+ </>
+ )}
<Button variant="destructive" onClick={() => setDeleteOpen(true)}
disabled={!tableData}>
<Trash2 className="mr-2 h-4 w-4" /> Delete
</Button>
@@ -150,9 +213,68 @@ export function TableDetails() {
<div className="text-red-600">Error loading table:
{tableQuery.error.message}</div>
) : !tableData ? (
<div>Table not found</div>
+ ) : isGenericTable ? (
+ <>
+ {/* Generic Table Information */}
+ <Card>
+ <CardHeader>
+ <CardTitle>Generic Table Information</CardTitle>
+ <CardDescription>Metadata for this {genericTableData.format}
table</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
+ <div>
+ <label className="text-sm font-medium
text-muted-foreground">Name</label>
+ <p className="mt-1 text-sm
font-mono">{genericTableData.name}</p>
+ </div>
+ <div>
+ <label className="text-sm font-medium
text-muted-foreground">Format</label>
+ <p className="mt-1 text-sm">{genericTableData.format}</p>
+ </div>
+ {genericTableData["base-location"] && (
+ <div className="md:col-span-2">
+ <label className="text-sm font-medium
text-muted-foreground">Base Location</label>
+ <p className="mt-1 text-sm font-mono
break-all">{genericTableData["base-location"]}</p>
+ </div>
+ )}
+ {genericTableData.doc && (
+ <div className="md:col-span-2">
+ <label className="text-sm font-medium
text-muted-foreground">Description</label>
+ <p className="mt-1 text-sm">{genericTableData.doc}</p>
+ </div>
+ )}
+ </div>
+ {genericTableData.properties &&
Object.keys(genericTableData.properties).length > 0 && (
+ <div>
+ <label className="text-sm font-medium
text-muted-foreground">Properties</label>
+ <div className="mt-2 border rounded-md overflow-hidden">
+ <table className="w-full text-sm">
+ <thead className="bg-muted/50">
+ <tr>
+ <th className="px-3 py-2 text-left
font-medium">Key</th>
+ <th className="px-3 py-2 text-left
font-medium">Value</th>
+ </tr>
+ </thead>
+ <tbody className="divide-y">
+
{Object.entries(genericTableData.properties).map(([key, value]) => (
+ <tr key={key} className="hover:bg-muted/50">
+ <td className="px-3 py-2 font-mono
text-xs">{key}</td>
+ <td className="px-3 py-2 text-xs
break-all">{value}</td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ </>
) : (
<>
- {/* Table Information */}
+ {/* Iceberg Table Information */}
<Card>
<CardHeader>
<CardTitle>Table Information</CardTitle>
@@ -193,8 +315,12 @@ export function TableDetails() {
/>
</CardContent>
</Card>
+ </>
+ )}
- {/* Modals */}
+ {/* Modals - shown for Iceberg tables only */}
+ {tableData && !isGenericTable && (
+ <>
<RenameTableModal
open={renameOpen}
onOpenChange={setRenameOpen}
@@ -216,32 +342,38 @@ export function TableDetails() {
tableName={tableName}
properties={tableData.metadata.properties}
/>
-
- {/* Delete Confirmation */}
- <Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Delete table</DialogTitle>
- <DialogDescription>
- Are you sure you want to delete "{tableName}"? This action
cannot be undone.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter>
- <Button variant="outline" onClick={() =>
setDeleteOpen(false)}>Cancel</Button>
- <Button
- variant="destructive"
- onClick={async () => {
- await tablesApi.delete(catalogName, namespaceArray,
tableName)
-
navigate(`/catalogs/${encodeURIComponent(catalogName)}/namespaces/${encodeURIComponent(namespaceParam)}`)
- }}
- >
- Delete
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
</>
)}
+
+ {/* Delete Confirmation - shown for both Iceberg and Generic tables */}
+ {tableData && (
+ <Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Delete {isGenericTable ? "generic table" :
"table"}</DialogTitle>
+ <DialogDescription>
+ Are you sure you want to delete "{tableName}"? This action
cannot be undone.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => setDeleteOpen(false)}
+ disabled={deleteIcebergMutation.isPending ||
deleteGenericMutation.isPending}
+ >
+ Cancel
+ </Button>
+ <Button
+ variant="destructive"
+ onClick={handleDelete}
+ disabled={deleteIcebergMutation.isPending ||
deleteGenericMutation.isPending}
+ >
+ {(deleteIcebergMutation.isPending ||
deleteGenericMutation.isPending) ? "Deleting..." : "Delete"}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )}
</div>
)
}
diff --git a/console/vite.config.ts b/console/vite.config.ts
index 6e546eb..11a3248 100644
--- a/console/vite.config.ts
+++ b/console/vite.config.ts
@@ -51,6 +51,27 @@ export default defineConfig({
}
},
},
+ '/polaris': {
+ target: process.env.VITE_POLARIS_API_URL || 'http://localhost:8181',
+ changeOrigin: true,
+ secure: false,
+ rewrite: (path) => path.replace(/^\/polaris/, '/api/catalog/polaris'),
+ configure: (proxy) => {
+ // Only log in development mode
+ if (process.env.NODE_ENV === 'development') {
+ proxy.on('error', (err) => {
+ console.error('Proxy error:', err);
+ });
+ proxy.on('proxyReq', (proxyReq, req) => {
+ const target = process.env.VITE_POLARIS_API_URL ||
'http://localhost:8181';
+ console.log('📤 Proxying:', req.method, req.url, '→', target +
proxyReq.path);
+ });
+ proxy.on('proxyRes', (proxyRes, req) => {
+ console.log('Received Response from the Target:',
proxyRes.statusCode, req.url);
+ });
+ }
+ },
+ },
},
},
})