commit
2ade186821
2 changed files with 195 additions and 49 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue