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;
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 {

View file

@ -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 {