Skip to content

Commit 1e058df

Browse files
committed
Project refinements
1 parent 8486a14 commit 1e058df

20 files changed

Lines changed: 1514 additions & 76 deletions

exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/MetadataResources.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,40 @@ public ColumnsResponse getColumns(
610610
throw new RuntimeException("Failed to fetch columns: " + e.getMessage(), e);
611611
}
612612

613+
// Dynamic-schema tables (e.g. Splunk, MongoDB, Elasticsearch) report
614+
// only "**" in INFORMATION_SCHEMA. For non-HTTP plugins, fall back to
615+
// SELECT * LIMIT 1 to discover the real columns at query time.
616+
// HTTP endpoints are skipped because they often require parameters
617+
// that a bare LIMIT 1 cannot provide.
618+
if (columns.size() == 1 && "**".equals(columns.get(0).name)) {
619+
if (isHttpPlugin(schema)) {
620+
// Keep the "**" entry so the frontend can show a helpful hint
621+
return new ColumnsResponse(columns);
622+
}
623+
columns.clear();
624+
try {
625+
String probeSql = String.format("SELECT * FROM %s.`%s` LIMIT 1",
626+
formatSchemaPath(schema), escapeBackticks(table));
627+
QueryResult probeResult = executeQuery(probeSql);
628+
List<String> columnNames = new ArrayList<>(probeResult.columns);
629+
for (int i = 0; i < columnNames.size(); i++) {
630+
String colName = columnNames.get(i);
631+
String dataType = "ANY";
632+
if (probeResult.metadata != null && i < probeResult.metadata.size()) {
633+
dataType = probeResult.metadata.get(i);
634+
} else if (!probeResult.rows.isEmpty()) {
635+
dataType = inferDataType(probeResult.rows.get(0).get(colName));
636+
}
637+
columns.add(new ColumnInfo(colName, dataType, true, schema, table));
638+
}
639+
} catch (Exception e) {
640+
logger.warn("Dynamic column probe failed for {}.{}: {}", schema, table, e.getMessage());
641+
// Probe failed — return the "**" marker so the frontend shows
642+
// the "schema determined at query time" hint instead of nothing
643+
columns.add(new ColumnInfo("**", "ANY", true, schema, table));
644+
}
645+
}
646+
613647
return new ColumnsResponse(columns);
614648
}
615649

@@ -940,6 +974,25 @@ private String escapeBackticks(String value) {
940974
return value.replace("`", "``");
941975
}
942976

977+
/**
978+
* Check whether a schema belongs to an HTTP storage plugin.
979+
* Uses the config class name to avoid a compile-time dependency on the
980+
* contrib HTTP module.
981+
*/
982+
private boolean isHttpPlugin(String schema) {
983+
try {
984+
String pluginName = schema.contains(".") ? schema.split("\\.", 2)[0] : schema;
985+
StoragePlugin plugin = storageRegistry.getPlugin(pluginName);
986+
if (plugin != null) {
987+
String configClass = plugin.getConfig().getClass().getSimpleName();
988+
return configClass.contains("HttpStoragePlugin");
989+
}
990+
} catch (Exception e) {
991+
logger.debug("Could not determine plugin type for schema {}: {}", schema, e.getMessage());
992+
}
993+
return false;
994+
}
995+
943996
/**
944997
* Format a compound schema name for SQL queries.
945998
* Plugin name stays unquoted; workspace parts are individually backtick-quoted.

exec/java-exec/src/main/resources/webapp/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import { Routes, Route, Navigate } from 'react-router-dom';
1919
import { Layout } from 'antd';
2020
import Navbar from './components/common/Navbar';
21+
import CommandPalette from './components/common/CommandPalette';
2122
import ProjectsPage from './pages/ProjectsPage';
2223
import ProjectDetailPage from './pages/ProjectDetailPage';
2324
import DataSourcesPage from './pages/DataSourcesPage';
@@ -45,6 +46,7 @@ const { Content } = Layout;
4546
function App() {
4647
return (
4748
<Layout className="sqllab-container">
49+
<CommandPalette />
4850
<Navbar />
4951
<Content style={{ flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
5052
<Routes>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
import { useState, useMemo } from 'react';
19+
import { Modal, Input, List, Typography, Space, message } from 'antd';
20+
import { SearchOutlined, FolderOutlined } from '@ant-design/icons';
21+
import { useQuery, useQueryClient } from '@tanstack/react-query';
22+
import { getProjects, addSavedQuery, addVisualization, addDashboard } from '../../api/projects';
23+
24+
const { Text } = Typography;
25+
26+
interface AddToProjectModalProps {
27+
open: boolean;
28+
onClose: () => void;
29+
itemId: string;
30+
itemType: 'savedQuery' | 'visualization' | 'dashboard';
31+
}
32+
33+
const typeLabels = { savedQuery: 'query', visualization: 'visualization', dashboard: 'dashboard' };
34+
35+
export default function AddToProjectModal({ open, onClose, itemId, itemType }: AddToProjectModalProps) {
36+
const [search, setSearch] = useState('');
37+
const [loading, setLoading] = useState<string | null>(null);
38+
const queryClient = useQueryClient();
39+
40+
const { data: projects } = useQuery({
41+
queryKey: ['projects'],
42+
queryFn: getProjects,
43+
enabled: open,
44+
});
45+
46+
const filtered = useMemo(() => {
47+
if (!projects) {
48+
return [];
49+
}
50+
if (!search) {
51+
return projects;
52+
}
53+
const lower = search.toLowerCase();
54+
return projects.filter(p => p.name.toLowerCase().includes(lower));
55+
}, [projects, search]);
56+
57+
const handleAdd = async (projectId: string) => {
58+
setLoading(projectId);
59+
try {
60+
if (itemType === 'savedQuery') {
61+
await addSavedQuery(projectId, itemId);
62+
} else if (itemType === 'visualization') {
63+
await addVisualization(projectId, itemId);
64+
} else {
65+
await addDashboard(projectId, itemId);
66+
}
67+
message.success(`Added ${typeLabels[itemType]} to project`);
68+
queryClient.invalidateQueries({ queryKey: ['project', projectId] });
69+
onClose();
70+
} catch (err) {
71+
message.error(`Failed: ${(err as Error).message}`);
72+
} finally {
73+
setLoading(null);
74+
}
75+
};
76+
77+
return (
78+
<Modal title="Add to Project" open={open} onCancel={onClose} footer={null} width={480}>
79+
<Input
80+
placeholder="Search projects..."
81+
prefix={<SearchOutlined />}
82+
value={search}
83+
onChange={e => setSearch(e.target.value)}
84+
allowClear
85+
style={{ marginBottom: 12 }}
86+
/>
87+
<List
88+
dataSource={filtered}
89+
style={{ maxHeight: 320, overflow: 'auto' }}
90+
loading={!projects}
91+
locale={{ emptyText: 'No projects found' }}
92+
renderItem={project => (
93+
<List.Item
94+
style={{ cursor: 'pointer', padding: '8px 12px' }}
95+
onClick={() => !loading && handleAdd(project.id)}
96+
>
97+
<Space>
98+
<FolderOutlined />
99+
<div>
100+
<Text strong>{project.name}</Text>
101+
{project.description && (
102+
<Text type="secondary" style={{ display: 'block', fontSize: 12 }} ellipsis>{project.description}</Text>
103+
)}
104+
</div>
105+
</Space>
106+
{loading === project.id && <Text type="secondary">Adding...</Text>}
107+
</List.Item>
108+
)}
109+
/>
110+
</Modal>
111+
);
112+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
import { Button, Space, Typography, Popconfirm } from 'antd';
19+
import { DeleteOutlined, FolderOutlined, CloseOutlined } from '@ant-design/icons';
20+
21+
const { Text } = Typography;
22+
23+
interface BulkActionBarProps {
24+
selectedCount: number;
25+
onAddToProject?: () => void;
26+
onDelete?: () => void;
27+
onClear: () => void;
28+
}
29+
30+
export default function BulkActionBar({ selectedCount, onAddToProject, onDelete, onClear }: BulkActionBarProps) {
31+
if (selectedCount === 0) {
32+
return null;
33+
}
34+
35+
return (
36+
<div style={{
37+
display: 'flex',
38+
alignItems: 'center',
39+
justifyContent: 'space-between',
40+
padding: '8px 16px',
41+
background: 'var(--color-bg-elevated)',
42+
border: '1px solid var(--color-primary)',
43+
borderRadius: 8,
44+
marginBottom: 12,
45+
}}>
46+
<Text strong>{selectedCount} selected</Text>
47+
<Space>
48+
{onAddToProject && (
49+
<Button size="small" icon={<FolderOutlined />} onClick={onAddToProject}>
50+
Add to Project
51+
</Button>
52+
)}
53+
{onDelete && (
54+
<Popconfirm
55+
title={`Delete ${selectedCount} item${selectedCount > 1 ? 's' : ''}?`}
56+
description="This action cannot be undone."
57+
onConfirm={onDelete}
58+
okText="Delete"
59+
cancelText="Cancel"
60+
okButtonProps={{ danger: true }}
61+
>
62+
<Button size="small" danger icon={<DeleteOutlined />}>
63+
Delete
64+
</Button>
65+
</Popconfirm>
66+
)}
67+
<Button size="small" type="text" icon={<CloseOutlined />} onClick={onClear}>
68+
Clear
69+
</Button>
70+
</Space>
71+
</div>
72+
);
73+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
import { useState, useMemo } from 'react';
19+
import { Modal, Input, List, Typography, Space, message } from 'antd';
20+
import { SearchOutlined, FolderOutlined } from '@ant-design/icons';
21+
import { useQuery, useQueryClient } from '@tanstack/react-query';
22+
import { getProjects, addSavedQuery, addVisualization, addDashboard } from '../../api/projects';
23+
24+
const { Text } = Typography;
25+
26+
interface Props {
27+
open: boolean;
28+
onClose: () => void;
29+
itemIds: string[];
30+
itemType: 'savedQuery' | 'visualization' | 'dashboard';
31+
}
32+
33+
export default function BulkAddToProjectModal({ open, onClose, itemIds, itemType }: Props) {
34+
const [search, setSearch] = useState('');
35+
const [loading, setLoading] = useState(false);
36+
const queryClient = useQueryClient();
37+
38+
const { data: projects } = useQuery({
39+
queryKey: ['projects'],
40+
queryFn: getProjects,
41+
enabled: open,
42+
});
43+
44+
const filtered = useMemo(() => {
45+
if (!projects) {
46+
return [];
47+
}
48+
if (!search) {
49+
return projects;
50+
}
51+
const lower = search.toLowerCase();
52+
return projects.filter(p => p.name.toLowerCase().includes(lower));
53+
}, [projects, search]);
54+
55+
const handleAdd = async (projectId: string) => {
56+
setLoading(true);
57+
try {
58+
const addFn = itemType === 'savedQuery' ? addSavedQuery
59+
: itemType === 'visualization' ? addVisualization
60+
: addDashboard;
61+
for (const id of itemIds) {
62+
await addFn(projectId, id);
63+
}
64+
message.success(`Added ${itemIds.length} item${itemIds.length > 1 ? 's' : ''} to project`);
65+
queryClient.invalidateQueries({ queryKey: ['project', projectId] });
66+
onClose();
67+
} catch (err) {
68+
message.error(`Failed: ${(err as Error).message}`);
69+
} finally {
70+
setLoading(false);
71+
}
72+
};
73+
74+
return (
75+
<Modal
76+
title={`Add ${itemIds.length} item${itemIds.length > 1 ? 's' : ''} to Project`}
77+
open={open}
78+
onCancel={onClose}
79+
footer={null}
80+
width={480}
81+
>
82+
<Input
83+
placeholder="Search projects..."
84+
prefix={<SearchOutlined />}
85+
value={search}
86+
onChange={e => setSearch(e.target.value)}
87+
allowClear
88+
style={{ marginBottom: 12 }}
89+
/>
90+
<List
91+
dataSource={filtered}
92+
loading={loading || !projects}
93+
style={{ maxHeight: 320, overflow: 'auto' }}
94+
renderItem={project => (
95+
<List.Item
96+
style={{ cursor: loading ? 'wait' : 'pointer', padding: '8px 12px' }}
97+
onClick={() => !loading && handleAdd(project.id)}
98+
>
99+
<Space>
100+
<FolderOutlined />
101+
<Text strong>{project.name}</Text>
102+
</Space>
103+
</List.Item>
104+
)}
105+
/>
106+
</Modal>
107+
);
108+
}

0 commit comments

Comments
 (0)