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;
|
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) */
|
/* DEPTH-SPECIFIC STYLES (via data-depth) */
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,8 @@ function isTreeSeparator(item: TreeItem): item is TreeSeparatorItem {
|
||||||
interface TreeNodeProps {
|
interface TreeNodeProps {
|
||||||
node: TreeNodeItem;
|
node: TreeNodeItem;
|
||||||
level: number;
|
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;
|
autoExpandActive: boolean;
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
onNodeClick?: (node: TreeNodeItem) => void;
|
onNodeClick?: (node: TreeNodeItem) => void;
|
||||||
|
|
@ -134,6 +136,7 @@ interface TreeNodeProps {
|
||||||
const TreeNode: React.FC<TreeNodeProps> = ({
|
const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
node,
|
node,
|
||||||
level,
|
level,
|
||||||
|
parentHasIcon = false,
|
||||||
autoExpandActive,
|
autoExpandActive,
|
||||||
currentPath,
|
currentPath,
|
||||||
onNodeClick,
|
onNodeClick,
|
||||||
|
|
@ -219,8 +222,9 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine if we should render as NavLink or button
|
// Unterknoten ohne Icon unter einem Knoten mit Icon: Text mit Eltern-Titel ausrichten (nicht mit Icon-Spalte)
|
||||||
const nodeClasses = `${styles.treeNode} ${isLeafActive ? styles.active : ''} ${isGroupActive ? styles.activeGroup : ''} ${node.disabled ? styles.disabled : ''} ${node.className || ''}`;
|
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 ? (
|
const nodeElement = node.path ? (
|
||||||
<NavLink
|
<NavLink
|
||||||
|
|
@ -258,6 +262,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
key={child.id || `${node.id}-child-${index}`}
|
key={child.id || `${node.id}-child-${index}`}
|
||||||
node={child}
|
node={child}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
|
parentHasIcon={!!node.icon}
|
||||||
autoExpandActive={autoExpandActive}
|
autoExpandActive={autoExpandActive}
|
||||||
currentPath={currentPath}
|
currentPath={currentPath}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
|
|
@ -304,6 +309,7 @@ const TreeSection: React.FC<TreeSectionProps> = ({
|
||||||
key={node.id || `section-${section.title}-${index}`}
|
key={node.id || `section-${section.title}-${index}`}
|
||||||
node={node}
|
node={node}
|
||||||
level={0}
|
level={0}
|
||||||
|
parentHasIcon={false}
|
||||||
autoExpandActive={autoExpandActive}
|
autoExpandActive={autoExpandActive}
|
||||||
currentPath={currentPath}
|
currentPath={currentPath}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
|
|
@ -355,6 +361,7 @@ export const TreeNavigation: React.FC<TreeNavigationProps> = ({
|
||||||
key={item.id || `node-${index}`}
|
key={item.id || `node-${index}`}
|
||||||
node={item}
|
node={item}
|
||||||
level={0}
|
level={0}
|
||||||
|
parentHasIcon={false}
|
||||||
autoExpandActive={autoExpandActive}
|
autoExpandActive={autoExpandActive}
|
||||||
currentPath={currentPath}
|
currentPath={currentPath}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ export interface Message {
|
||||||
role?: string;
|
role?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
sequenceNr?: number;
|
sequenceNr?: number;
|
||||||
|
/** ISO or number from API; workspace may use publishedAt only */
|
||||||
|
createdAt?: number;
|
||||||
publishedAt?: number;
|
publishedAt?: number;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
actionId?: string;
|
actionId?: string;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { useConnections, type Connection } from '../../hooks/useConnections';
|
import { useConnections, type Connection } from '../../hooks/useConnections';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
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';
|
import styles from '../admin/Admin.module.css';
|
||||||
|
|
||||||
export const ConnectionsPage: React.FC = () => {
|
export const ConnectionsPage: React.FC = () => {
|
||||||
|
|
@ -37,6 +38,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
const [editingConnection, setEditingConnection] = useState<Connection | null>(null);
|
const [editingConnection, setEditingConnection] = useState<Connection | null>(null);
|
||||||
const [deletingConnections, setDeletingConnections] = useState<Set<string>>(new Set());
|
const [deletingConnections, setDeletingConnections] = useState<Set<string>>(new Set());
|
||||||
const [refreshingConnections, setRefreshingConnections] = useState<Set<string>>(new Set());
|
const [refreshingConnections, setRefreshingConnections] = useState<Set<string>>(new Set());
|
||||||
|
const [adminConsentPending, setAdminConsentPending] = useState(false);
|
||||||
|
|
||||||
// Initial fetch
|
// Initial fetch
|
||||||
useEffect(() => {
|
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
|
// Form attributes for edit modal
|
||||||
const formAttributes = useMemo(() => {
|
const formAttributes = useMemo(() => {
|
||||||
const excludedFields = ['id', 'mandateId', 'userId', '_createdBy', '_createdAt', '_modifiedAt', 'connectedAt', 'lastChecked'];
|
const excludedFields = ['id', 'mandateId', 'userId', '_createdBy', '_createdAt', '_modifiedAt', 'connectedAt', 'lastChecked'];
|
||||||
|
|
@ -204,13 +224,21 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.adminPage}>
|
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||||
<div className={styles.pageHeader}>
|
<div className={styles.pageHeader}>
|
||||||
<div>
|
<div>
|
||||||
<h1 className={styles.pageTitle}>Verbindungen</h1>
|
<h1 className={styles.pageTitle}>Verbindungen</h1>
|
||||||
<p className={styles.pageSubtitle}>OAuth-Verbindungen verwalten</p>
|
<p className={styles.pageSubtitle}>OAuth-Verbindungen verwalten</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.headerActions}>
|
<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
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,7 @@ export const FilesPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.adminPage}>
|
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
|
|
|
||||||
|
|
@ -433,7 +433,7 @@ export const AutomationDefinitionsView: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.adminPage}>
|
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||||
<div className={styles.pageHeader}>
|
<div className={styles.pageHeader}>
|
||||||
<div>
|
<div>
|
||||||
<h1 className={styles.pageTitle}>Automatisierungen</h1>
|
<h1 className={styles.pageTitle}>Automatisierungen</h1>
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export const AutomationTemplatesView: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.adminPage}>
|
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||||
<div className={styles.pageHeader}>
|
<div className={styles.pageHeader}>
|
||||||
<div>
|
<div>
|
||||||
<h1 className={styles.pageTitle}>Automation-Vorlagen</h1>
|
<h1 className={styles.pageTitle}>Automation-Vorlagen</h1>
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,18 @@ export const ChatStream: React.FC<ChatStreamProps> = ({
|
||||||
<span>{msg.message}</span>
|
<span>{msg.message}</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="workspace-markdown">
|
<div className="workspace-markdown">
|
||||||
|
{msg.documentsLabel && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#666',
|
||||||
|
marginBottom: msg.message ? 8 : 0,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{msg.documentsLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{msg.message && (
|
{msg.message && (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import { startSseStream, SseEvent } from '../../../utils/sseClient';
|
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 {
|
export interface AgentProgress {
|
||||||
round: number;
|
round: number;
|
||||||
|
|
@ -173,13 +173,9 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
|
||||||
|
|
||||||
api.get(`/api/workspace/${instanceId}/workflows/${wfId}/messages`)
|
api.get(`/api/workspace/${instanceId}/workflows/${wfId}/messages`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const msgs = (res.data.messages || []).map((m: any) => ({
|
const msgs = (res.data.messages || [])
|
||||||
id: m.id || `loaded-${Math.random()}`,
|
.map((m: any) => _mapLoadedWorkspaceMessage(m, wfId))
|
||||||
workflowId: wfId,
|
.sort(_compareWorkspaceMessages);
|
||||||
role: m.role || 'assistant',
|
|
||||||
message: m.content || m.message || '',
|
|
||||||
publishedAt: m.createdAt || Date.now() / 1000,
|
|
||||||
}));
|
|
||||||
setMessages(msgs);
|
setMessages(msgs);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
@ -210,6 +206,13 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
message: prompt,
|
message: prompt,
|
||||||
publishedAt: Date.now() / 1000,
|
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),
|
onStreamEnd: () => setIsProcessing(false),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[instanceId, isProcessing, workflowId, refreshFiles],
|
[
|
||||||
|
instanceId,
|
||||||
|
isProcessing,
|
||||||
|
workflowId,
|
||||||
|
refreshFiles,
|
||||||
|
files,
|
||||||
|
dataSources,
|
||||||
|
featureDataSources,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const stopProcessing = useCallback(() => {
|
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
|
// Internal event handlers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue