fixed global RAG and admin consent msft
This commit is contained in:
parent
9b99020686
commit
131c4534b5
9 changed files with 172 additions and 16 deletions
|
|
@ -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) */
|
||||
/* ============================================ */
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue