fixed global RAG and admin consent msft

This commit is contained in:
ValueOn AG 2026-03-22 01:20:40 +01:00
parent 9b99020686
commit 131c4534b5
9 changed files with 172 additions and 16 deletions

View file

@ -103,6 +103,14 @@
pointer-events: none;
}
/*
* Label-only row under a parent that shows an icon: indent so text lines up with the
* parent's title, not with the icon column (see .nodeIcon width + .treeNode gap).
*/
.treeNodeAlignWithParentTitle {
padding-left: calc(0.5rem + 0.875rem + 0.375rem);
}
/* ============================================ */
/* DEPTH-SPECIFIC STYLES (via data-depth) */
/* ============================================ */

View file

@ -125,6 +125,8 @@ function isTreeSeparator(item: TreeItem): item is TreeSeparatorItem {
interface TreeNodeProps {
node: TreeNodeItem;
level: number;
/** True when the parent row shows an icon — used to align label-only children with the parent's title text. */
parentHasIcon?: boolean;
autoExpandActive: boolean;
currentPath: string;
onNodeClick?: (node: TreeNodeItem) => void;
@ -134,6 +136,7 @@ interface TreeNodeProps {
const TreeNode: React.FC<TreeNodeProps> = ({
node,
level,
parentHasIcon = false,
autoExpandActive,
currentPath,
onNodeClick,
@ -219,8 +222,9 @@ const TreeNode: React.FC<TreeNodeProps> = ({
</>
);
// Determine if we should render as NavLink or button
const nodeClasses = `${styles.treeNode} ${isLeafActive ? styles.active : ''} ${isGroupActive ? styles.activeGroup : ''} ${node.disabled ? styles.disabled : ''} ${node.className || ''}`;
// Unterknoten ohne Icon unter einem Knoten mit Icon: Text mit Eltern-Titel ausrichten (nicht mit Icon-Spalte)
const alignLabelWithParentTitle = parentHasIcon && !node.icon;
const nodeClasses = `${styles.treeNode} ${isLeafActive ? styles.active : ''} ${isGroupActive ? styles.activeGroup : ''} ${node.disabled ? styles.disabled : ''} ${alignLabelWithParentTitle ? styles.treeNodeAlignWithParentTitle : ''} ${node.className || ''}`;
const nodeElement = node.path ? (
<NavLink
@ -258,6 +262,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
key={child.id || `${node.id}-child-${index}`}
node={child}
level={level + 1}
parentHasIcon={!!node.icon}
autoExpandActive={autoExpandActive}
currentPath={currentPath}
onNodeClick={onNodeClick}
@ -304,6 +309,7 @@ const TreeSection: React.FC<TreeSectionProps> = ({
key={node.id || `section-${section.title}-${index}`}
node={node}
level={0}
parentHasIcon={false}
autoExpandActive={autoExpandActive}
currentPath={currentPath}
onNodeClick={onNodeClick}
@ -355,6 +361,7 @@ export const TreeNavigation: React.FC<TreeNavigationProps> = ({
key={item.id || `node-${index}`}
node={item}
level={0}
parentHasIcon={false}
autoExpandActive={autoExpandActive}
currentPath={currentPath}
onNodeClick={onNodeClick}

View file

@ -32,6 +32,8 @@ export interface Message {
role?: string;
status?: string;
sequenceNr?: number;
/** ISO or number from API; workspace may use publishedAt only */
createdAt?: number;
publishedAt?: number;
success?: boolean;
actionId?: string;

View file

@ -9,7 +9,8 @@ import React, { useState, useMemo, useEffect } from 'react';
import { useConnections, type Connection } from '../../hooks/useConnections';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaPlug, FaGoogle, FaMicrosoft, FaLink, FaRedo } from 'react-icons/fa';
import { FaSync, FaPlug, FaGoogle, FaMicrosoft, FaLink, FaRedo, FaShieldAlt } from 'react-icons/fa';
import { getApiBaseUrl } from '../../../config/config';
import styles from '../admin/Admin.module.css';
export const ConnectionsPage: React.FC = () => {
@ -37,6 +38,7 @@ export const ConnectionsPage: React.FC = () => {
const [editingConnection, setEditingConnection] = useState<Connection | null>(null);
const [deletingConnections, setDeletingConnections] = useState<Set<string>>(new Set());
const [refreshingConnections, setRefreshingConnections] = useState<Set<string>>(new Set());
const [adminConsentPending, setAdminConsentPending] = useState(false);
// Initial fetch
useEffect(() => {
@ -182,6 +184,24 @@ export const ConnectionsPage: React.FC = () => {
}
};
// Open Microsoft Admin Consent flow in a popup
const handleAdminConsent = () => {
setAdminConsentPending(true);
const consentUrl = `${getApiBaseUrl()}/api/msft/adminconsent`;
const popup = window.open(consentUrl, 'msft-admin-consent', 'width=600,height=700,scrollbars=yes,resizable=yes');
if (!popup) {
setAdminConsentPending(false);
return;
}
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
setAdminConsentPending(false);
refetch();
}
}, 1000);
};
// Form attributes for edit modal
const formAttributes = useMemo(() => {
const excludedFields = ['id', 'mandateId', 'userId', '_createdBy', '_createdAt', '_modifiedAt', 'connectedAt', 'lastChecked'];
@ -204,13 +224,21 @@ export const ConnectionsPage: React.FC = () => {
}
return (
<div className={styles.adminPage}>
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Verbindungen</h1>
<p className={styles.pageSubtitle}>OAuth-Verbindungen verwalten</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={handleAdminConsent}
disabled={adminConsentPending}
title="Microsoft Admin Consent — erteilt der App die nötigen Berechtigungen für den gesamten Tenant"
>
<FaShieldAlt /> Admin Consent
</button>
<button
className={styles.secondaryButton}
onClick={() => refetch()}

View file

@ -307,7 +307,7 @@ export const FilesPage: React.FC = () => {
}
return (
<div className={styles.adminPage}>
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<input
ref={fileInputRef}
type="file"

View file

@ -433,7 +433,7 @@ export const AutomationDefinitionsView: React.FC = () => {
}
return (
<div className={styles.adminPage}>
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Automatisierungen</h1>

View file

@ -123,7 +123,7 @@ export const AutomationTemplatesView: React.FC = () => {
}
return (
<div className={styles.adminPage}>
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Automation-Vorlagen</h1>

View file

@ -74,6 +74,18 @@ export const ChatStream: React.FC<ChatStreamProps> = ({
<span>{msg.message}</span>
) : (
<div className="workspace-markdown">
{msg.documentsLabel && (
<div
style={{
fontSize: 12,
color: '#666',
marginBottom: msg.message ? 8 : 0,
fontStyle: 'italic',
}}
>
{msg.documentsLabel}
</div>
)}
{msg.message && (
<ReactMarkdown
remarkPlugins={[remarkGfm]}

View file

@ -9,7 +9,7 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import api from '../../../api';
import { startSseStream, SseEvent } from '../../../utils/sseClient';
import type { Message } from '../../../components/UiComponents/Messages/MessagesTypes';
import type { Message, MessageDocument } from '../../../components/UiComponents/Messages/MessagesTypes';
export interface AgentProgress {
round: number;
@ -173,13 +173,9 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
api.get(`/api/workspace/${instanceId}/workflows/${wfId}/messages`)
.then(res => {
const msgs = (res.data.messages || []).map((m: any) => ({
id: m.id || `loaded-${Math.random()}`,
workflowId: wfId,
role: m.role || 'assistant',
message: m.content || m.message || '',
publishedAt: m.createdAt || Date.now() / 1000,
}));
const msgs = (res.data.messages || [])
.map((m: any) => _mapLoadedWorkspaceMessage(m, wfId))
.sort(_compareWorkspaceMessages);
setMessages(msgs);
})
.catch(() => {});
@ -210,6 +206,13 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
role: 'user',
message: prompt,
publishedAt: Date.now() / 1000,
documents: _documentsFromFileIds(files, fileIds),
documentsLabel: _attachmentLabelFromContext(
dataSourceIds,
featureDataSourceIds,
dataSources,
featureDataSources,
),
},
]);
@ -393,7 +396,15 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
onStreamEnd: () => setIsProcessing(false),
});
},
[instanceId, isProcessing, workflowId, refreshFiles],
[
instanceId,
isProcessing,
workflowId,
refreshFiles,
files,
dataSources,
featureDataSources,
],
);
const stopProcessing = useCallback(() => {
@ -468,6 +479,94 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
};
}
// ---------------------------------------------------------------------------
// Internal: loaded message mapping & attachment display
// ---------------------------------------------------------------------------
function _mapLoadedWorkspaceMessage(m: Record<string, unknown>, wfId: string): Message {
const publishedAt =
(typeof m.publishedAt === 'number' ? m.publishedAt : undefined) ??
(typeof m.createdAt === 'number' ? m.createdAt : undefined) ??
Date.now() / 1000;
const docsRaw = Array.isArray(m.documents) ? m.documents : [];
const documents: MessageDocument[] = docsRaw.map((d: any) => ({
id: String(d.id || `doc-${d.fileId}`),
messageId: String(d.messageId || ''),
fileId: String(d.fileId || ''),
fileName: String(d.fileName || ''),
mimeType: String(d.mimeType || 'application/octet-stream'),
fileSize: Number(d.fileSize || 0),
roundNumber: Number(d.roundNumber ?? 0),
taskNumber: Number(d.taskNumber ?? 0),
actionNumber: Number(d.actionNumber ?? 0),
actionId: String(d.actionId || ''),
}));
return {
id: String(m.id || `loaded-${Math.random()}`),
workflowId: wfId,
role: String(m.role || 'assistant'),
message: String(m.content ?? m.message ?? ''),
publishedAt,
sequenceNr: typeof m.sequenceNr === 'number' ? m.sequenceNr : undefined,
documents: documents.length ? documents : undefined,
documentsLabel: typeof m.documentsLabel === 'string' ? m.documentsLabel : undefined,
};
}
function _compareWorkspaceMessages(a: Message, b: Message): number {
const ta = (a.publishedAt || 0) - (b.publishedAt || 0);
if (ta !== 0) return ta;
const sa = (a.sequenceNr ?? 0) - (b.sequenceNr ?? 0);
if (sa !== 0) return sa;
return String(a.id).localeCompare(String(b.id));
}
function _documentsFromFileIds(files: WorkspaceFile[], fileIds: string[]): MessageDocument[] | undefined {
const out: MessageDocument[] = [];
for (const fid of fileIds) {
const f = files.find(x => x.id === fid);
if (f) {
out.push({
id: `local-${fid}-${Date.now()}`,
messageId: '',
fileId: f.id,
fileName: f.fileName,
mimeType: f.mimeType,
fileSize: f.fileSize,
roundNumber: 0,
taskNumber: 0,
actionNumber: 0,
actionId: '',
});
}
}
return out.length ? out : undefined;
}
function _attachmentLabelFromContext(
dataSourceIds: string[],
featureDataSourceIds: string[],
dataSources: DataSource[],
featureDataSources: FeatureDataSource[],
): string | undefined {
const parts: string[] = [];
const dsLabels = dataSourceIds
.map(id => {
const ds = dataSources.find(d => d.id === id);
return ds?.label || ds?.path;
})
.filter((x): x is string => Boolean(x));
if (dsLabels.length) parts.push(`Datenquellen: ${dsLabels.join(', ')}`);
const fdsLabels = featureDataSourceIds
.map(id => {
const fds = featureDataSources.find(x => x.id === id);
return fds ? `${fds.tableName} (${fds.label})` : '';
})
.filter(Boolean);
if (fdsLabels.length) parts.push(`Feature-Daten: ${fdsLabels.join(', ')}`);
return parts.length ? parts.join(' | ') : undefined;
}
// ---------------------------------------------------------------------------
// Internal event handlers
// ---------------------------------------------------------------------------