Merge pull request #22 from valueonag/int

fixed rendering issues
This commit is contained in:
Patrick Motsch 2026-03-22 11:11:45 +01:00 committed by GitHub
commit 2ade186821
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 195 additions and 49 deletions

View file

@ -27,12 +27,15 @@ interface TreeNode {
connectionId: string; connectionId: string;
service?: string; service?: string;
path?: string; path?: string;
/** Breadcrumb for tooltips and persisted displayPath (service + folder segments) */
displayPath?: string;
authority?: string; authority?: string;
} }
interface FeatureConnectionNode { interface FeatureConnectionNode {
featureInstanceId: string; featureInstanceId: string;
featureCode: string; featureCode: string;
mandateId?: string;
label: string; label: string;
icon: string; icon: string;
tableCount: number; tableCount: number;
@ -41,6 +44,13 @@ interface FeatureConnectionNode {
tables: FeatureTableNode[] | null; tables: FeatureTableNode[] | null;
} }
interface MandateGroupNode {
mandateId: string;
mandateLabel: string;
expanded: boolean;
featureConnections: FeatureConnectionNode[];
}
interface FeatureTableNode { interface FeatureTableNode {
objectKey: string; objectKey: string;
tableName: string; tableName: string;
@ -102,6 +112,53 @@ function _getSourceIcon(sourceType: string): string {
return map[sourceType] || '\uD83D\uDCC1'; 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 ─────────────────────────────────────────────────────── */ /* ─── Component ─────────────────────────────────────────────────────── */
export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
@ -114,7 +171,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
const [tree, setTree] = useState<TreeNode[]>([]); const [tree, setTree] = useState<TreeNode[]>([]);
const [loadingRoot, setLoadingRoot] = useState(false); const [loadingRoot, setLoadingRoot] = useState(false);
const [addingPath, setAddingPath] = useState<string | null>(null); const [addingPath, setAddingPath] = useState<string | null>(null);
const [featureTree, setFeatureTree] = useState<FeatureConnectionNode[]>([]); const [featureTree, setFeatureTree] = useState<MandateGroupNode[]>([]);
const [loadingFeatures, setLoadingFeatures] = useState(false); const [loadingFeatures, setLoadingFeatures] = useState(false);
const [addingFeatureKey, setAddingFeatureKey] = useState<string | null>(null); const [addingFeatureKey, setAddingFeatureKey] = useState<string | null>(null);
const mountedRef = useRef(true); const mountedRef = useRef(true);
@ -174,7 +231,13 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
if (node.type === 'connection') { if (node.type === 'connection') {
children = await _loadServices(instanceId, node.connectionId); children = await _loadServices(instanceId, node.connectionId);
} else if (node.type === 'service' || node.type === 'folder') { } 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) { if (mountedRef.current) {
@ -205,6 +268,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
sourceType: sourceTypeMap[node.service] || node.service, sourceType: sourceTypeMap[node.service] || node.service,
path: node.path || '/', path: node.path || '/',
label: node.label, label: node.label,
displayPath: node.displayPath || node.label,
}); });
onRefresh(); onRefresh();
} catch (err) { } catch (err) {
@ -238,16 +302,22 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
api.get(`/api/workspace/${instanceId}/feature-connections`) api.get(`/api/workspace/${instanceId}/feature-connections`)
.then(res => { .then(res => {
if (!mountedRef.current) return; if (!mountedRef.current) return;
const conns = res.data.featureConnections || []; const groups = res.data.featureConnectionsByMandate || [];
setFeatureTree(conns.map((c: any) => ({ setFeatureTree(groups.map((g: any) => ({
mandateId: g.mandateId,
mandateLabel: g.mandateLabel || g.mandateId,
expanded: true,
featureConnections: (g.featureConnections || []).map((c: any) => ({
featureInstanceId: c.featureInstanceId, featureInstanceId: c.featureInstanceId,
featureCode: c.featureCode, featureCode: c.featureCode,
mandateId: c.mandateId,
label: c.label, label: c.label,
icon: c.icon || '\uD83D\uDDC3\uFE0F', icon: c.icon || '\uD83D\uDDC3\uFE0F',
tableCount: c.tableCount || 0, tableCount: c.tableCount || 0,
expanded: false, expanded: false,
loading: false, loading: false,
tables: null, tables: null,
})),
}))); })));
}) })
.catch(() => { if (mountedRef.current) setFeatureTree([]); }) .catch(() => { if (mountedRef.current) setFeatureTree([]); })
@ -256,25 +326,28 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
useEffect(() => { _loadFeatureConnections(); }, [_loadFeatureConnections]); 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 ── */ /* ── Feature Connections: Toggle expand ── */
const _toggleFeatureNode = useCallback(async (node: FeatureConnectionNode) => { const _toggleFeatureNode = useCallback(async (node: FeatureConnectionNode) => {
if (node.expanded) { if (node.expanded) {
setFeatureTree(prev => prev.map(n => setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({ ...n, expanded: false })));
n.featureInstanceId === node.featureInstanceId ? { ...n, expanded: false } : n
));
return; return;
} }
if (node.tables !== null) { if (node.tables !== null) {
setFeatureTree(prev => prev.map(n => setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({ ...n, expanded: true })));
n.featureInstanceId === node.featureInstanceId ? { ...n, expanded: true } : n
));
return; return;
} }
setFeatureTree(prev => prev.map(n => setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
n.featureInstanceId === node.featureInstanceId ? { ...n, loading: true, expanded: true } : n ...n, loading: true, expanded: true,
)); })));
try { try {
const res = await api.get(`/api/workspace/${instanceId}/feature-connections/${node.featureInstanceId}/tables`); 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 || [], fields: t.fields || [],
})); }));
if (mountedRef.current) { if (mountedRef.current) {
setFeatureTree(prev => prev.map(n => setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
n.featureInstanceId === node.featureInstanceId ? { ...n, loading: false, tables } : n ...n, loading: false, tables,
)); })));
} }
} catch { } catch {
if (mountedRef.current) { if (mountedRef.current) {
setFeatureTree(prev => prev.map(n => setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
n.featureInstanceId === node.featureInstanceId ? { ...n, loading: false, tables: [] } : n ...n, loading: false, tables: [],
)); })));
} }
} }
}, [instanceId]); }, [instanceId]);
@ -341,7 +414,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
{dataSources.length > 0 && ( {dataSources.length > 0 && (
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}> <div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
Active Sources Active Personal Sources
</div> </div>
{dataSources.map(ds => { {dataSources.map(ds => {
const connColor = _getSourceColor(ds.sourceType); const connColor = _getSourceColor(ds.sourceType);
@ -355,7 +428,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
background: `${connColor}18`, background: `${connColor}18`,
borderLeft: `3px solid ${connColor}`, borderLeft: `3px solid ${connColor}`,
fontSize: 12, fontSize: 12,
}} title={`${connLabel} ${ds.path || ds.label}`}> }} title={_personalDataSourceHoverTitle(connLabel, ds)}>
<span style={{ fontSize: 12, flexShrink: 0 }}>{_getSourceIcon(ds.sourceType)}</span> <span style={{ fontSize: 12, flexShrink: 0 }}>{_getSourceIcon(ds.sourceType)}</span>
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{connLabel} {folder} {connLabel} {folder}
@ -423,7 +496,8 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
Active Feature Sources Active Feature Sources
</div> </div>
{featureDataSources.map(fds => { {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 ( return (
<div key={fds.id} style={{ <div key={fds.id} style={{
display: 'flex', alignItems: 'center', gap: 6, display: 'flex', alignItems: 'center', gap: 6,
@ -431,7 +505,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
background: '#7b1fa218', background: '#7b1fa218',
borderLeft: '3px solid #7b1fa2', borderLeft: '3px solid #7b1fa2',
fontSize: 12, fontSize: 12,
}} title={`${fdsConnLabel} - ${fds.tableName}`}> }} title={_featureDataSourceHoverTitle(meta, fds)}>
<span style={{ fontSize: 12, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}> <span style={{ fontSize: 12, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}>
{getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'} {getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
</span> </span>
@ -477,11 +551,12 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
</div> </div>
)} )}
{featureTree.map(fNode => ( {featureTree.map(g => (
<_FeatureNodeView <_MandateGroupView
key={fNode.featureInstanceId} key={g.mandateId}
node={fNode} group={g}
onToggle={_toggleFeatureNode} onToggleGroup={_toggleMandateGroup}
onToggleFeature={_toggleFeatureNode}
onAddTable={_addFeatureTable} onAddTable={_addFeatureTable}
isTableAdded={_isFeatureTableAdded} isTableAdded={_isFeatureTableAdded}
addingKey={addingFeatureKey} 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) ─────────────────── */ /* ─── FeatureNodeView (feature instance + tables) ─────────────────── */
interface FeatureNodeViewProps { interface FeatureNodeViewProps {
@ -748,17 +880,27 @@ async function _loadServices(instanceId: string, connectionId: string): Promise<
connectionId, connectionId,
service: s.service, service: s.service,
path: '/', path: '/',
displayPath: s.label || s.service,
})); }));
} }
async function _browseService( async function _browseService(
instanceId: string, connectionId: string, service: string, path: string, instanceId: string,
connectionId: string,
service: string,
path: string,
parentDisplayPath: string | undefined,
): Promise<TreeNode[]> { ): Promise<TreeNode[]> {
const res = await api.get(`/api/workspace/${instanceId}/connections/${connectionId}/browse`, { const res = await api.get(`/api/workspace/${instanceId}/connections/${connectionId}/browse`, {
params: { service, path }, params: { service, path },
}); });
const items = res.data.items || []; const items = res.data.items || [];
return items.map((entry: any, idx: number) => ({ 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}`, key: `item-${connectionId}-${service}-${entry.path || idx}`,
label: entry.name, label: entry.name,
icon: entry.isFolder ? '\uD83D\uDCC1' : _fileIcon(entry.name), icon: entry.isFolder ? '\uD83D\uDCC1' : _fileIcon(entry.name),
@ -769,7 +911,9 @@ async function _browseService(
connectionId, connectionId,
service, service,
path: entry.path, path: entry.path,
})); displayPath,
};
});
} }
function _fileIcon(name: string): string { function _fileIcon(name: string): string {

View file

@ -54,6 +54,8 @@ export interface DataSource {
sourceType: string; sourceType: string;
path: string; path: string;
label: string; label: string;
/** Human-readable full path (service + folders); used for tooltips */
displayPath?: string;
} }
export interface FeatureDataSource { export interface FeatureDataSource {