fixed rendering issues
This commit is contained in:
parent
131c4534b5
commit
196f76f95e
2 changed files with 195 additions and 49 deletions
|
|
@ -27,12 +27,15 @@ interface TreeNode {
|
|||
connectionId: string;
|
||||
service?: string;
|
||||
path?: string;
|
||||
/** Breadcrumb for tooltips and persisted displayPath (service + folder segments) */
|
||||
displayPath?: string;
|
||||
authority?: string;
|
||||
}
|
||||
|
||||
interface FeatureConnectionNode {
|
||||
featureInstanceId: string;
|
||||
featureCode: string;
|
||||
mandateId?: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
tableCount: number;
|
||||
|
|
@ -41,6 +44,13 @@ interface FeatureConnectionNode {
|
|||
tables: FeatureTableNode[] | null;
|
||||
}
|
||||
|
||||
interface MandateGroupNode {
|
||||
mandateId: string;
|
||||
mandateLabel: string;
|
||||
expanded: boolean;
|
||||
featureConnections: FeatureConnectionNode[];
|
||||
}
|
||||
|
||||
interface FeatureTableNode {
|
||||
objectKey: string;
|
||||
tableName: string;
|
||||
|
|
@ -102,6 +112,53 @@ function _getSourceIcon(sourceType: string): string {
|
|||
return map[sourceType] || '\uD83D\uDCC1';
|
||||
}
|
||||
|
||||
function _mapFeatureTreeUpdate(
|
||||
prev: MandateGroupNode[],
|
||||
featureInstanceId: string,
|
||||
updater: (n: FeatureConnectionNode) => FeatureConnectionNode,
|
||||
): MandateGroupNode[] {
|
||||
return prev.map(g => ({
|
||||
...g,
|
||||
featureConnections: g.featureConnections.map(n =>
|
||||
n.featureInstanceId === featureInstanceId ? updater(n) : n
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
function _findFeatureInstanceMeta(
|
||||
groups: MandateGroupNode[],
|
||||
featureInstanceId: string,
|
||||
): { mandateLabel: string; instanceLabel: string } | null {
|
||||
for (const g of groups) {
|
||||
const fc = g.featureConnections.find(f => f.featureInstanceId === featureInstanceId);
|
||||
if (fc) return { mandateLabel: g.mandateLabel, instanceLabel: fc.label };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _personalDataSourceHoverTitle(connLabel: string, ds: DataSource): string {
|
||||
const pathPart = (ds.displayPath && ds.displayPath.trim()) || ds.label || ds.path || '';
|
||||
return pathPart ? `${connLabel} / ${pathPart}` : connLabel;
|
||||
}
|
||||
|
||||
function _featureDataSourceHoverTitle(
|
||||
meta: { mandateLabel: string; instanceLabel: string } | null,
|
||||
fds: FeatureDataSource,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
if (meta) {
|
||||
parts.push(meta.mandateLabel, meta.instanceLabel);
|
||||
}
|
||||
const labelPart = fds.label && fds.tableName && fds.label !== fds.tableName
|
||||
? `${fds.label} (${fds.tableName})`
|
||||
: (fds.label || fds.tableName);
|
||||
parts.push(labelPart);
|
||||
if (fds.objectKey && fds.objectKey !== labelPart && !labelPart.includes(fds.objectKey)) {
|
||||
parts.push(fds.objectKey);
|
||||
}
|
||||
return parts.join(' / ');
|
||||
}
|
||||
|
||||
/* ─── Component ─────────────────────────────────────────────────────── */
|
||||
|
||||
export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
||||
|
|
@ -114,7 +171,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
const [tree, setTree] = useState<TreeNode[]>([]);
|
||||
const [loadingRoot, setLoadingRoot] = useState(false);
|
||||
const [addingPath, setAddingPath] = useState<string | null>(null);
|
||||
const [featureTree, setFeatureTree] = useState<FeatureConnectionNode[]>([]);
|
||||
const [featureTree, setFeatureTree] = useState<MandateGroupNode[]>([]);
|
||||
const [loadingFeatures, setLoadingFeatures] = useState(false);
|
||||
const [addingFeatureKey, setAddingFeatureKey] = useState<string | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
|
@ -174,7 +231,13 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
if (node.type === 'connection') {
|
||||
children = await _loadServices(instanceId, node.connectionId);
|
||||
} else if (node.type === 'service' || node.type === 'folder') {
|
||||
children = await _browseService(instanceId, node.connectionId, node.service!, node.path || '/');
|
||||
children = await _browseService(
|
||||
instanceId,
|
||||
node.connectionId,
|
||||
node.service!,
|
||||
node.path || '/',
|
||||
node.displayPath || node.label,
|
||||
);
|
||||
}
|
||||
|
||||
if (mountedRef.current) {
|
||||
|
|
@ -205,6 +268,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
sourceType: sourceTypeMap[node.service] || node.service,
|
||||
path: node.path || '/',
|
||||
label: node.label,
|
||||
displayPath: node.displayPath || node.label,
|
||||
});
|
||||
onRefresh();
|
||||
} catch (err) {
|
||||
|
|
@ -238,16 +302,22 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
api.get(`/api/workspace/${instanceId}/feature-connections`)
|
||||
.then(res => {
|
||||
if (!mountedRef.current) return;
|
||||
const conns = res.data.featureConnections || [];
|
||||
setFeatureTree(conns.map((c: any) => ({
|
||||
featureInstanceId: c.featureInstanceId,
|
||||
featureCode: c.featureCode,
|
||||
label: c.label,
|
||||
icon: c.icon || '\uD83D\uDDC3\uFE0F',
|
||||
tableCount: c.tableCount || 0,
|
||||
expanded: false,
|
||||
loading: false,
|
||||
tables: null,
|
||||
const groups = res.data.featureConnectionsByMandate || [];
|
||||
setFeatureTree(groups.map((g: any) => ({
|
||||
mandateId: g.mandateId,
|
||||
mandateLabel: g.mandateLabel || g.mandateId,
|
||||
expanded: true,
|
||||
featureConnections: (g.featureConnections || []).map((c: any) => ({
|
||||
featureInstanceId: c.featureInstanceId,
|
||||
featureCode: c.featureCode,
|
||||
mandateId: c.mandateId,
|
||||
label: c.label,
|
||||
icon: c.icon || '\uD83D\uDDC3\uFE0F',
|
||||
tableCount: c.tableCount || 0,
|
||||
expanded: false,
|
||||
loading: false,
|
||||
tables: null,
|
||||
})),
|
||||
})));
|
||||
})
|
||||
.catch(() => { if (mountedRef.current) setFeatureTree([]); })
|
||||
|
|
@ -256,25 +326,28 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
|
||||
useEffect(() => { _loadFeatureConnections(); }, [_loadFeatureConnections]);
|
||||
|
||||
/* ── Feature Connections: Toggle mandate group ── */
|
||||
const _toggleMandateGroup = useCallback((mandateId: string) => {
|
||||
setFeatureTree(prev => prev.map(g =>
|
||||
g.mandateId === mandateId ? { ...g, expanded: !g.expanded } : g
|
||||
));
|
||||
}, []);
|
||||
|
||||
/* ── Feature Connections: Toggle expand ── */
|
||||
const _toggleFeatureNode = useCallback(async (node: FeatureConnectionNode) => {
|
||||
if (node.expanded) {
|
||||
setFeatureTree(prev => prev.map(n =>
|
||||
n.featureInstanceId === node.featureInstanceId ? { ...n, expanded: false } : n
|
||||
));
|
||||
setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({ ...n, expanded: false })));
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.tables !== null) {
|
||||
setFeatureTree(prev => prev.map(n =>
|
||||
n.featureInstanceId === node.featureInstanceId ? { ...n, expanded: true } : n
|
||||
));
|
||||
setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({ ...n, expanded: true })));
|
||||
return;
|
||||
}
|
||||
|
||||
setFeatureTree(prev => prev.map(n =>
|
||||
n.featureInstanceId === node.featureInstanceId ? { ...n, loading: true, expanded: true } : n
|
||||
));
|
||||
setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
|
||||
...n, loading: true, expanded: true,
|
||||
})));
|
||||
|
||||
try {
|
||||
const res = await api.get(`/api/workspace/${instanceId}/feature-connections/${node.featureInstanceId}/tables`);
|
||||
|
|
@ -285,15 +358,15 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
fields: t.fields || [],
|
||||
}));
|
||||
if (mountedRef.current) {
|
||||
setFeatureTree(prev => prev.map(n =>
|
||||
n.featureInstanceId === node.featureInstanceId ? { ...n, loading: false, tables } : n
|
||||
));
|
||||
setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
|
||||
...n, loading: false, tables,
|
||||
})));
|
||||
}
|
||||
} catch {
|
||||
if (mountedRef.current) {
|
||||
setFeatureTree(prev => prev.map(n =>
|
||||
n.featureInstanceId === node.featureInstanceId ? { ...n, loading: false, tables: [] } : n
|
||||
));
|
||||
setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
|
||||
...n, loading: false, tables: [],
|
||||
})));
|
||||
}
|
||||
}
|
||||
}, [instanceId]);
|
||||
|
|
@ -341,7 +414,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
{dataSources.length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
||||
Active Sources
|
||||
Active Personal Sources
|
||||
</div>
|
||||
{dataSources.map(ds => {
|
||||
const connColor = _getSourceColor(ds.sourceType);
|
||||
|
|
@ -355,7 +428,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
background: `${connColor}18`,
|
||||
borderLeft: `3px solid ${connColor}`,
|
||||
fontSize: 12,
|
||||
}} title={`${connLabel} – ${ds.path || ds.label}`}>
|
||||
}} title={_personalDataSourceHoverTitle(connLabel, ds)}>
|
||||
<span style={{ fontSize: 12, flexShrink: 0 }}>{_getSourceIcon(ds.sourceType)}</span>
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{connLabel} – {folder}
|
||||
|
|
@ -423,7 +496,8 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
Active Feature Sources
|
||||
</div>
|
||||
{featureDataSources.map(fds => {
|
||||
const fdsConnLabel = featureTree.find(n => n.featureInstanceId === fds.featureInstanceId)?.label || fds.label;
|
||||
const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId);
|
||||
const fdsConnLabel = meta?.instanceLabel || fds.tableName;
|
||||
return (
|
||||
<div key={fds.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
|
|
@ -431,7 +505,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
background: '#7b1fa218',
|
||||
borderLeft: '3px solid #7b1fa2',
|
||||
fontSize: 12,
|
||||
}} title={`${fdsConnLabel} - ${fds.tableName}`}>
|
||||
}} title={_featureDataSourceHoverTitle(meta, fds)}>
|
||||
<span style={{ fontSize: 12, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}>
|
||||
{getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
|
||||
</span>
|
||||
|
|
@ -477,11 +551,12 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{featureTree.map(fNode => (
|
||||
<_FeatureNodeView
|
||||
key={fNode.featureInstanceId}
|
||||
node={fNode}
|
||||
onToggle={_toggleFeatureNode}
|
||||
{featureTree.map(g => (
|
||||
<_MandateGroupView
|
||||
key={g.mandateId}
|
||||
group={g}
|
||||
onToggleGroup={_toggleMandateGroup}
|
||||
onToggleFeature={_toggleFeatureNode}
|
||||
onAddTable={_addFeatureTable}
|
||||
isTableAdded={_isFeatureTableAdded}
|
||||
addingKey={addingFeatureKey}
|
||||
|
|
@ -596,6 +671,63 @@ const _TreeNodeView: React.FC<TreeNodeViewProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
/* ─── MandateGroupView (mandate + feature instances) ───────────────── */
|
||||
|
||||
interface MandateGroupViewProps {
|
||||
group: MandateGroupNode;
|
||||
onToggleGroup: (mandateId: string) => void;
|
||||
onToggleFeature: (node: FeatureConnectionNode) => void;
|
||||
onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void;
|
||||
isTableAdded: (featureInstanceId: string, tableName: string) => boolean;
|
||||
addingKey: string | null;
|
||||
}
|
||||
|
||||
const _MandateGroupView: React.FC<MandateGroupViewProps> = ({
|
||||
group, onToggleGroup, onToggleFeature, onAddTable, isTableAdded, addingKey,
|
||||
}) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const chevron = group.expanded ? '\u25BE' : '\u25B8';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onClick={() => onToggleGroup(group.mandateId)}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 4,
|
||||
paddingLeft: 4, paddingRight: 4, paddingTop: 3, paddingBottom: 3,
|
||||
cursor: 'pointer', borderRadius: 3,
|
||||
background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent',
|
||||
transition: 'background 0.1s', userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0 }}>
|
||||
{chevron}
|
||||
</span>
|
||||
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12, fontWeight: 700, color: '#555' }}>
|
||||
{group.mandateLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{group.expanded && (
|
||||
<div style={{ paddingLeft: 10 }}>
|
||||
{group.featureConnections.map(fNode => (
|
||||
<_FeatureNodeView
|
||||
key={fNode.featureInstanceId}
|
||||
node={fNode}
|
||||
onToggle={onToggleFeature}
|
||||
onAddTable={onAddTable}
|
||||
isTableAdded={isTableAdded}
|
||||
addingKey={addingKey}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ─── FeatureNodeView (feature instance + tables) ─────────────────── */
|
||||
|
||||
interface FeatureNodeViewProps {
|
||||
|
|
@ -748,28 +880,40 @@ async function _loadServices(instanceId: string, connectionId: string): Promise<
|
|||
connectionId,
|
||||
service: s.service,
|
||||
path: '/',
|
||||
displayPath: s.label || s.service,
|
||||
}));
|
||||
}
|
||||
|
||||
async function _browseService(
|
||||
instanceId: string, connectionId: string, service: string, path: string,
|
||||
instanceId: string,
|
||||
connectionId: string,
|
||||
service: string,
|
||||
path: string,
|
||||
parentDisplayPath: string | undefined,
|
||||
): Promise<TreeNode[]> {
|
||||
const res = await api.get(`/api/workspace/${instanceId}/connections/${connectionId}/browse`, {
|
||||
params: { service, path },
|
||||
});
|
||||
const items = res.data.items || [];
|
||||
return items.map((entry: any, idx: number) => ({
|
||||
key: `item-${connectionId}-${service}-${entry.path || idx}`,
|
||||
label: entry.name,
|
||||
icon: entry.isFolder ? '\uD83D\uDCC1' : _fileIcon(entry.name),
|
||||
type: entry.isFolder ? 'folder' as const : 'file' as const,
|
||||
expanded: false,
|
||||
loading: false,
|
||||
children: entry.isFolder ? null : [],
|
||||
connectionId,
|
||||
service,
|
||||
path: entry.path,
|
||||
}));
|
||||
return items.map((entry: any, idx: number) => {
|
||||
const seg = entry.name || '';
|
||||
const displayPath = parentDisplayPath
|
||||
? `${parentDisplayPath} / ${seg}`
|
||||
: seg;
|
||||
return {
|
||||
key: `item-${connectionId}-${service}-${entry.path || idx}`,
|
||||
label: entry.name,
|
||||
icon: entry.isFolder ? '\uD83D\uDCC1' : _fileIcon(entry.name),
|
||||
type: entry.isFolder ? 'folder' as const : 'file' as const,
|
||||
expanded: false,
|
||||
loading: false,
|
||||
children: entry.isFolder ? null : [],
|
||||
connectionId,
|
||||
service,
|
||||
path: entry.path,
|
||||
displayPath,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function _fileIcon(name: string): string {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ export interface DataSource {
|
|||
sourceType: string;
|
||||
path: string;
|
||||
label: string;
|
||||
/** Human-readable full path (service + folders); used for tooltips */
|
||||
displayPath?: string;
|
||||
}
|
||||
|
||||
export interface FeatureDataSource {
|
||||
|
|
|
|||
Loading…
Reference in a new issue