mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 11:10:49 +08:00
refactor(ModelMappingDiagram): restructure component to utilize new modular columns and improve type definitions
This commit is contained in:
@@ -1,23 +1,24 @@
|
|||||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, type DragEvent, type MouseEvent as ReactMouseEvent } from 'react';
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, type DragEvent, type MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { OAuthModelAliasEntry } from '@/types';
|
import type { OAuthModelAliasEntry } from '@/types';
|
||||||
import { useThemeStore } from '@/stores';
|
import { useThemeStore } from '@/stores';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { AliasColumn, ProviderColumn, SourceColumn } from './ModelMappingDiagramColumns';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { DiagramContextMenu } from './ModelMappingDiagramContextMenu';
|
||||||
import { Button } from '@/components/ui/Button';
|
import {
|
||||||
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
AddAliasModal,
|
||||||
import { IconTrash2 } from '@/components/ui/icons';
|
RenameAliasModal,
|
||||||
|
SettingsAliasModal,
|
||||||
|
SettingsSourceModal
|
||||||
|
} from './ModelMappingDiagramModals';
|
||||||
|
import type {
|
||||||
|
AliasNode,
|
||||||
|
AuthFileModelItem,
|
||||||
|
ContextMenuState,
|
||||||
|
DiagramLine,
|
||||||
|
SourceNode
|
||||||
|
} from './ModelMappingDiagramTypes';
|
||||||
import styles from './ModelMappingDiagram.module.scss';
|
import styles from './ModelMappingDiagram.module.scss';
|
||||||
|
|
||||||
// Type definition for models from API
|
|
||||||
export interface AuthFileModelItem {
|
|
||||||
id: string;
|
|
||||||
display_name?: string;
|
|
||||||
type?: string;
|
|
||||||
owned_by?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelMappingDiagramProps {
|
export interface ModelMappingDiagramProps {
|
||||||
modelAlias: Record<string, OAuthModelAliasEntry[]>;
|
modelAlias: Record<string, OAuthModelAliasEntry[]>;
|
||||||
allProviderModels?: Record<string, AuthFileModelItem[]>;
|
allProviderModels?: Record<string, AuthFileModelItem[]>;
|
||||||
@@ -31,7 +32,6 @@ export interface ModelMappingDiagramProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to generate consistent colors
|
|
||||||
const PROVIDER_COLORS = [
|
const PROVIDER_COLORS = [
|
||||||
'#3b82f6', '#10b981', '#f59e0b', '#ef4444',
|
'#3b82f6', '#10b981', '#f59e0b', '#ef4444',
|
||||||
'#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'
|
'#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'
|
||||||
@@ -42,26 +42,6 @@ function getProviderColor(provider: string): string {
|
|||||||
return PROVIDER_COLORS[hash % PROVIDER_COLORS.length];
|
return PROVIDER_COLORS[hash % PROVIDER_COLORS.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourceNode {
|
|
||||||
id: string; // unique: provider::name
|
|
||||||
provider: string;
|
|
||||||
name: string;
|
|
||||||
aliases: { alias: string; fork: boolean }[]; // all aliases this source maps to
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AliasNode {
|
|
||||||
id: string; // alias
|
|
||||||
alias: string;
|
|
||||||
sources: SourceNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextMenuState {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
type: 'alias' | 'background' | 'provider' | 'source';
|
|
||||||
data?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelMappingDiagramRef {
|
export interface ModelMappingDiagramRef {
|
||||||
collapseAll: () => void;
|
collapseAll: () => void;
|
||||||
refreshLayout: () => void;
|
refreshLayout: () => void;
|
||||||
@@ -84,7 +64,7 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
|
|||||||
const isDark = resolvedTheme === 'dark';
|
const isDark = resolvedTheme === 'dark';
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [lines, setLines] = useState<{ path: string; color: string; id: string }[]>([]);
|
const [lines, setLines] = useState<DiagramLine[]>([]);
|
||||||
const [draggedSource, setDraggedSource] = useState<SourceNode | null>(null);
|
const [draggedSource, setDraggedSource] = useState<SourceNode | null>(null);
|
||||||
const [draggedAlias, setDraggedAlias] = useState<string | null>(null);
|
const [draggedAlias, setDraggedAlias] = useState<string | null>(null);
|
||||||
const [dropTargetAlias, setDropTargetAlias] = useState<string | null>(null);
|
const [dropTargetAlias, setDropTargetAlias] = useState<string | null>(null);
|
||||||
@@ -100,18 +80,6 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
|
|||||||
const [addAliasError, setAddAliasError] = useState('');
|
const [addAliasError, setAddAliasError] = useState('');
|
||||||
const [settingsAlias, setSettingsAlias] = useState<string | null>(null);
|
const [settingsAlias, setSettingsAlias] = useState<string | null>(null);
|
||||||
const [settingsSourceId, setSettingsSourceId] = useState<string | null>(null);
|
const [settingsSourceId, setSettingsSourceId] = useState<string | null>(null);
|
||||||
const contextMenuRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
// Close context menu on click outside
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClick = (event: globalThis.MouseEvent) => {
|
|
||||||
if (!contextMenuRef.current?.contains(event.target as Node)) {
|
|
||||||
setContextMenu(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('mousedown', handleClick);
|
|
||||||
return () => document.removeEventListener('mousedown', handleClick);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Parse data: each source model (provider+name) and each alias is distinct by id; 1 source -> many aliases.
|
// Parse data: each source model (provider+name) and each alias is distinct by id; 1 source -> many aliases.
|
||||||
const { aliasNodes, providerNodes } = useMemo(() => {
|
const { aliasNodes, providerNodes } = useMemo(() => {
|
||||||
@@ -458,77 +426,6 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAliasMenu = () => {
|
|
||||||
const aliasData = contextMenu?.data;
|
|
||||||
if (contextMenu?.type !== 'alias' || aliasData == null) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.menuItem} onClick={() => handleRenameClick(aliasData)}>
|
|
||||||
<span>{t('oauth_model_alias.diagram_rename')}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={styles.menuItem}
|
|
||||||
onClick={() => {
|
|
||||||
closeContextMenu();
|
|
||||||
setSettingsAlias(aliasData);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{t('oauth_model_alias.diagram_settings')}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.menuDivider} />
|
|
||||||
<div className={`${styles.menuItem} ${styles.danger}`} onClick={() => handleDeleteClick(aliasData)}>
|
|
||||||
<span>{t('oauth_model_alias.diagram_delete_alias')}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderProviderMenu = () => {
|
|
||||||
const provider = contextMenu?.data;
|
|
||||||
if (contextMenu?.type !== 'provider' || !provider) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={styles.menuItem}
|
|
||||||
onClick={() => {
|
|
||||||
closeContextMenu();
|
|
||||||
onEditProvider?.(provider);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{t('common.edit')}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.menuDivider} />
|
|
||||||
<div
|
|
||||||
className={`${styles.menuItem} ${styles.danger}`}
|
|
||||||
onClick={() => {
|
|
||||||
closeContextMenu();
|
|
||||||
onDeleteProvider?.(provider);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{t('oauth_model_alias.delete')}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSourceMenu = () => {
|
|
||||||
const sourceId = contextMenu?.data;
|
|
||||||
const source = resolveSourceById(sourceId ?? null);
|
|
||||||
if (contextMenu?.type !== 'source' || !source) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={styles.menuItem}
|
|
||||||
onClick={() => {
|
|
||||||
closeContextMenu();
|
|
||||||
setSettingsSourceId(source.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{t('oauth_model_alias.diagram_settings')}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -551,356 +448,120 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
|
|||||||
))}
|
))}
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* Column 1: Providers (mindmap root branch) */}
|
<ProviderColumn
|
||||||
<div
|
providerNodes={providerNodes}
|
||||||
className={`${styles.column} ${styles.providers}`}
|
collapsedProviders={collapsedProviders}
|
||||||
onContextMenu={(e) => {
|
getProviderColor={getProviderColor}
|
||||||
e.preventDefault();
|
providerRefs={providerRefs}
|
||||||
e.stopPropagation();
|
onToggleCollapse={toggleProviderCollapse}
|
||||||
handleContextMenu(e, 'background');
|
onContextMenu={(e, type, data) => handleContextMenu(e, type, data)}
|
||||||
|
label={t('oauth_model_alias.diagram_providers')}
|
||||||
|
expandLabel={t('oauth_model_alias.diagram_expand')}
|
||||||
|
collapseLabel={t('oauth_model_alias.diagram_collapse')}
|
||||||
|
/>
|
||||||
|
<SourceColumn
|
||||||
|
providerNodes={providerNodes}
|
||||||
|
collapsedProviders={collapsedProviders}
|
||||||
|
sourceRefs={sourceRefs}
|
||||||
|
getProviderColor={getProviderColor}
|
||||||
|
draggedSource={draggedSource}
|
||||||
|
dropTargetSource={dropTargetSource}
|
||||||
|
draggable={!!onUpdate}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={() => {
|
||||||
|
setDraggedSource(null);
|
||||||
|
setDropTargetAlias(null);
|
||||||
}}
|
}}
|
||||||
>
|
onDragOver={handleDragOverSource}
|
||||||
<div className={styles.columnHeader}>{t('oauth_model_alias.diagram_providers')}</div>
|
onDragLeave={handleDragLeaveSource}
|
||||||
{providerNodes.map(({ provider, sources }) => {
|
onDrop={handleDropOnSource}
|
||||||
const collapsed = collapsedProviders.has(provider);
|
onContextMenu={(e, type, data) => handleContextMenu(e, type, data)}
|
||||||
return (
|
label={t('oauth_model_alias.diagram_source_models')}
|
||||||
<div
|
/>
|
||||||
key={provider}
|
<AliasColumn
|
||||||
ref={(el) => {
|
aliasNodes={aliasNodes}
|
||||||
if (el) providerRefs.current.set(provider, el);
|
aliasRefs={aliasRefs}
|
||||||
else providerRefs.current.delete(provider);
|
dropTargetAlias={dropTargetAlias}
|
||||||
}}
|
draggedAlias={draggedAlias}
|
||||||
className={`${styles.item} ${styles.providerItem}`}
|
draggable={!!onUpdate}
|
||||||
style={{ borderLeftColor: getProviderColor(provider) }}
|
onDragStart={handleDragStartAlias}
|
||||||
onContextMenu={(e) => {
|
onDragEnd={() => {
|
||||||
e.preventDefault();
|
setDraggedAlias(null);
|
||||||
e.stopPropagation();
|
setDropTargetSource(null);
|
||||||
handleContextMenu(e, 'provider', provider);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.collapseBtn}
|
|
||||||
onClick={() => toggleProviderCollapse(provider)}
|
|
||||||
aria-label={collapsed ? t('oauth_model_alias.diagram_expand') : t('oauth_model_alias.diagram_collapse')}
|
|
||||||
title={collapsed ? t('oauth_model_alias.diagram_expand') : t('oauth_model_alias.diagram_collapse')}
|
|
||||||
>
|
|
||||||
<span className={collapsed ? styles.chevronRight : styles.chevronDown} />
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
className={styles.providerLabel}
|
|
||||||
style={{ color: getProviderColor(provider) }}
|
|
||||||
>
|
|
||||||
{provider}
|
|
||||||
</span>
|
|
||||||
<span className={styles.itemCount}>{sources.length}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Column 2: Source Models (children per provider; hidden when provider collapsed) */}
|
|
||||||
<div
|
|
||||||
className={`${styles.column} ${styles.sources}`}
|
|
||||||
onContextMenu={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleContextMenu(e, 'background');
|
|
||||||
}}
|
}}
|
||||||
>
|
onDragOver={handleDragOver}
|
||||||
<div className={styles.columnHeader}>{t('oauth_model_alias.diagram_source_models')}</div>
|
onDragLeave={handleDragLeave}
|
||||||
{providerNodes.flatMap(({ provider, sources }) => {
|
onDrop={handleDrop}
|
||||||
if (collapsedProviders.has(provider)) return [];
|
onContextMenu={(e, type, data) => handleContextMenu(e, type, data)}
|
||||||
return sources.map((source) => (
|
label={t('oauth_model_alias.diagram_aliases')}
|
||||||
<div
|
/>
|
||||||
key={source.id}
|
|
||||||
ref={(el) => {
|
|
||||||
if (el) sourceRefs.current.set(source.id, el);
|
|
||||||
else sourceRefs.current.delete(source.id);
|
|
||||||
}}
|
|
||||||
className={`${styles.item} ${styles.sourceItem} ${
|
|
||||||
draggedSource?.id === source.id ? styles.dragging : ''
|
|
||||||
} ${dropTargetSource === source.id ? styles.dropTarget : ''}`}
|
|
||||||
draggable={!!onUpdate}
|
|
||||||
onDragStart={(e) => handleDragStart(e, source)}
|
|
||||||
onDragEnd={() => {
|
|
||||||
setDraggedSource(null);
|
|
||||||
setDropTargetAlias(null);
|
|
||||||
}}
|
|
||||||
onDragOver={(e) => handleDragOverSource(e, source)}
|
|
||||||
onDragLeave={handleDragLeaveSource}
|
|
||||||
onDrop={(e) => handleDropOnSource(e, source)}
|
|
||||||
onContextMenu={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleContextMenu(e, 'source', source.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className={styles.itemName} title={source.name}>
|
|
||||||
{source.name}
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
className={styles.dot}
|
|
||||||
style={{
|
|
||||||
background: getProviderColor(source.provider),
|
|
||||||
opacity: source.aliases.length > 0 ? 1 : 0.3
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<DiagramContextMenu
|
||||||
className={`${styles.column} ${styles.aliases}`}
|
contextMenu={contextMenu}
|
||||||
onContextMenu={(e) => {
|
t={t}
|
||||||
e.preventDefault();
|
onRequestClose={() => setContextMenu(null)}
|
||||||
e.stopPropagation();
|
onAddAlias={handleAddAlias}
|
||||||
handleContextMenu(e, 'background');
|
onRenameAlias={handleRenameClick}
|
||||||
|
onOpenAliasSettings={(alias) => {
|
||||||
|
setContextMenu(null);
|
||||||
|
setSettingsAlias(alias);
|
||||||
}}
|
}}
|
||||||
>
|
onDeleteAlias={handleDeleteClick}
|
||||||
<div className={styles.columnHeader}>{t('oauth_model_alias.diagram_aliases')}</div>
|
onEditProvider={(provider) => {
|
||||||
{aliasNodes.map((node) => (
|
setContextMenu(null);
|
||||||
<div
|
onEditProvider?.(provider);
|
||||||
key={node.id}
|
}}
|
||||||
ref={(el) => {
|
onDeleteProvider={(provider) => {
|
||||||
if (el) aliasRefs.current.set(node.id, el);
|
setContextMenu(null);
|
||||||
else aliasRefs.current.delete(node.id);
|
onDeleteProvider?.(provider);
|
||||||
}}
|
}}
|
||||||
className={`${styles.item} ${styles.aliasItem} ${
|
onOpenSourceSettings={(sourceId) => {
|
||||||
dropTargetAlias === node.alias ? styles.dropTarget : ''
|
setContextMenu(null);
|
||||||
} ${draggedAlias === node.alias ? styles.dragging : ''}`}
|
setSettingsSourceId(sourceId);
|
||||||
draggable={!!onUpdate}
|
}}
|
||||||
onDragStart={(e) => handleDragStartAlias(e, node.alias)}
|
/>
|
||||||
onDragEnd={() => {
|
|
||||||
setDraggedAlias(null);
|
|
||||||
setDropTargetSource(null);
|
|
||||||
}}
|
|
||||||
onDragOver={(e) => handleDragOver(e, node.alias)}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={(e) => handleDrop(e, node.alias)}
|
|
||||||
onContextMenu={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleContextMenu(e, 'alias', node.alias);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={`${styles.dot} ${styles.dotLeft}`} />
|
|
||||||
<span className={styles.itemName} title={node.alias}>
|
|
||||||
{node.alias}
|
|
||||||
</span>
|
|
||||||
<span className={styles.itemCount}>{node.sources.length}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{contextMenu &&
|
<RenameAliasModal
|
||||||
createPortal(
|
|
||||||
<div
|
|
||||||
ref={contextMenuRef}
|
|
||||||
className={styles.contextMenu}
|
|
||||||
style={{ top: contextMenu.y, left: contextMenu.x }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{contextMenu.type === 'background' && (
|
|
||||||
<div className={styles.menuItem} onClick={handleAddAlias}>
|
|
||||||
<span>{t('oauth_model_alias.diagram_add_alias')}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{contextMenu.type === 'alias' && renderAliasMenu()}
|
|
||||||
{contextMenu.type === 'provider' && renderProviderMenu()}
|
|
||||||
{contextMenu.type === 'source' && renderSourceMenu()}
|
|
||||||
</div>,
|
|
||||||
document.body
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={!!renameState}
|
open={!!renameState}
|
||||||
|
t={t}
|
||||||
|
value={renameValue}
|
||||||
|
error={renameError}
|
||||||
|
onChange={(value) => {
|
||||||
|
setRenameValue(value);
|
||||||
|
setRenameError('');
|
||||||
|
}}
|
||||||
onClose={() => setRenameState(null)}
|
onClose={() => setRenameState(null)}
|
||||||
title={t('oauth_model_alias.diagram_rename_alias_title')}
|
onSubmit={handleRenameSubmit}
|
||||||
width={400}
|
/>
|
||||||
footer={
|
<AddAliasModal
|
||||||
<>
|
|
||||||
<Button variant="secondary" onClick={() => setRenameState(null)}>
|
|
||||||
{t('common.cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleRenameSubmit}>
|
|
||||||
{t('oauth_model_alias.diagram_rename_btn')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
label={t('oauth_model_alias.diagram_rename_alias_label')}
|
|
||||||
value={renameValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
setRenameValue(e.target.value);
|
|
||||||
setRenameError('');
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') handleRenameSubmit();
|
|
||||||
}}
|
|
||||||
error={renameError}
|
|
||||||
placeholder={t('oauth_model_alias.diagram_rename_placeholder')}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={addAliasOpen}
|
open={addAliasOpen}
|
||||||
|
t={t}
|
||||||
|
value={addAliasValue}
|
||||||
|
error={addAliasError}
|
||||||
|
onChange={(value) => {
|
||||||
|
setAddAliasValue(value);
|
||||||
|
setAddAliasError('');
|
||||||
|
}}
|
||||||
onClose={() => setAddAliasOpen(false)}
|
onClose={() => setAddAliasOpen(false)}
|
||||||
title={t('oauth_model_alias.diagram_add_alias_title')}
|
onSubmit={handleAddAliasSubmit}
|
||||||
width={400}
|
/>
|
||||||
footer={
|
<SettingsAliasModal
|
||||||
<>
|
|
||||||
<Button variant="secondary" onClick={() => setAddAliasOpen(false)}>
|
|
||||||
{t('common.cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleAddAliasSubmit}>
|
|
||||||
{t('oauth_model_alias.diagram_add_btn')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
label={t('oauth_model_alias.diagram_add_alias_label')}
|
|
||||||
value={addAliasValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
setAddAliasValue(e.target.value);
|
|
||||||
setAddAliasError('');
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') handleAddAliasSubmit();
|
|
||||||
}}
|
|
||||||
error={addAliasError}
|
|
||||||
placeholder={t('oauth_model_alias.diagram_add_placeholder')}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={Boolean(settingsAlias)}
|
open={Boolean(settingsAlias)}
|
||||||
|
t={t}
|
||||||
|
alias={settingsAlias}
|
||||||
|
aliasNodes={aliasNodes}
|
||||||
onClose={() => setSettingsAlias(null)}
|
onClose={() => setSettingsAlias(null)}
|
||||||
title={t('oauth_model_alias.diagram_settings_title', { alias: settingsAlias ?? '' })}
|
onToggleFork={handleToggleFork}
|
||||||
width={720}
|
onUnlink={handleUnlinkSource}
|
||||||
footer={
|
/>
|
||||||
<Button variant="secondary" onClick={() => setSettingsAlias(null)}>
|
<SettingsSourceModal
|
||||||
{t('common.close')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{settingsAlias ? (
|
|
||||||
(() => {
|
|
||||||
const node = aliasNodes.find((n) => n.alias === settingsAlias);
|
|
||||||
if (!node || node.sources.length === 0) {
|
|
||||||
return <div className={styles.settingsEmpty}>{t('oauth_model_alias.diagram_settings_empty')}</div>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={styles.settingsList}>
|
|
||||||
{node.sources.map((source) => {
|
|
||||||
const entry = source.aliases.find((item) => item.alias === settingsAlias);
|
|
||||||
const forkEnabled = entry?.fork === true;
|
|
||||||
return (
|
|
||||||
<div key={source.id} className={styles.settingsRow}>
|
|
||||||
<div className={styles.settingsNames}>
|
|
||||||
<span className={styles.settingsSource}>{source.name}</span>
|
|
||||||
<span className={styles.settingsArrow}>→</span>
|
|
||||||
<span className={styles.settingsAlias}>{settingsAlias}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.settingsActions}>
|
|
||||||
<span className={styles.settingsLabel}>
|
|
||||||
{t('oauth_model_alias.alias_fork_label')}
|
|
||||||
</span>
|
|
||||||
<ToggleSwitch
|
|
||||||
checked={forkEnabled}
|
|
||||||
onChange={(value) => handleToggleFork(source.provider, source.name, settingsAlias, value)}
|
|
||||||
ariaLabel={t('oauth_model_alias.alias_fork_label')}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.settingsDelete}
|
|
||||||
onClick={() => handleUnlinkSource(source.provider, source.name, settingsAlias)}
|
|
||||||
aria-label={t('oauth_model_alias.diagram_delete_link', {
|
|
||||||
provider: source.provider,
|
|
||||||
name: source.name
|
|
||||||
})}
|
|
||||||
title={t('oauth_model_alias.diagram_delete_link', {
|
|
||||||
provider: source.provider,
|
|
||||||
name: source.name
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<IconTrash2 size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
) : null}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={Boolean(settingsSourceId)}
|
open={Boolean(settingsSourceId)}
|
||||||
|
t={t}
|
||||||
|
source={resolveSourceById(settingsSourceId)}
|
||||||
onClose={() => setSettingsSourceId(null)}
|
onClose={() => setSettingsSourceId(null)}
|
||||||
title={t('oauth_model_alias.diagram_settings_source_title')}
|
onToggleFork={handleToggleFork}
|
||||||
width={720}
|
onUnlink={handleUnlinkSource}
|
||||||
footer={
|
/>
|
||||||
<Button variant="secondary" onClick={() => setSettingsSourceId(null)}>
|
|
||||||
{t('common.close')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{settingsSourceId ? (
|
|
||||||
(() => {
|
|
||||||
const source = resolveSourceById(settingsSourceId);
|
|
||||||
if (!source || source.aliases.length === 0) {
|
|
||||||
return <div className={styles.settingsEmpty}>{t('oauth_model_alias.diagram_settings_empty')}</div>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={styles.settingsList}>
|
|
||||||
{source.aliases.map((entry) => (
|
|
||||||
<div key={`${source.id}-${entry.alias}`} className={styles.settingsRow}>
|
|
||||||
<div className={styles.settingsNames}>
|
|
||||||
<span className={styles.settingsSource}>{source.name}</span>
|
|
||||||
<span className={styles.settingsArrow}>→</span>
|
|
||||||
<span className={styles.settingsAlias}>{entry.alias}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.settingsActions}>
|
|
||||||
<span className={styles.settingsLabel}>
|
|
||||||
{t('oauth_model_alias.alias_fork_label')}
|
|
||||||
</span>
|
|
||||||
<ToggleSwitch
|
|
||||||
checked={entry.fork === true}
|
|
||||||
onChange={(value) =>
|
|
||||||
handleToggleFork(source.provider, source.name, entry.alias, value)
|
|
||||||
}
|
|
||||||
ariaLabel={t('oauth_model_alias.alias_fork_label')}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.settingsDelete}
|
|
||||||
onClick={() => handleUnlinkSource(source.provider, source.name, entry.alias)}
|
|
||||||
aria-label={t('oauth_model_alias.diagram_delete_link', {
|
|
||||||
provider: source.provider,
|
|
||||||
name: source.name
|
|
||||||
})}
|
|
||||||
title={t('oauth_model_alias.diagram_delete_link', {
|
|
||||||
provider: source.provider,
|
|
||||||
name: source.name
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<IconTrash2 size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
) : null}
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
229
src/components/modelAlias/ModelMappingDiagramColumns.tsx
Normal file
229
src/components/modelAlias/ModelMappingDiagramColumns.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import type { DragEvent, MouseEvent as ReactMouseEvent, RefObject } from 'react';
|
||||||
|
import type { AliasNode, ProviderNode, SourceNode } from './ModelMappingDiagramTypes';
|
||||||
|
import styles from './ModelMappingDiagram.module.scss';
|
||||||
|
|
||||||
|
interface ProviderColumnProps {
|
||||||
|
providerNodes: ProviderNode[];
|
||||||
|
collapsedProviders: Set<string>;
|
||||||
|
getProviderColor: (provider: string) => string;
|
||||||
|
providerRefs: RefObject<Map<string, HTMLDivElement>>;
|
||||||
|
onToggleCollapse: (provider: string) => void;
|
||||||
|
onContextMenu: (e: ReactMouseEvent, type: 'provider' | 'background', data?: string) => void;
|
||||||
|
label: string;
|
||||||
|
expandLabel: string;
|
||||||
|
collapseLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProviderColumn({
|
||||||
|
providerNodes,
|
||||||
|
collapsedProviders,
|
||||||
|
getProviderColor,
|
||||||
|
providerRefs,
|
||||||
|
onToggleCollapse,
|
||||||
|
onContextMenu,
|
||||||
|
label,
|
||||||
|
expandLabel,
|
||||||
|
collapseLabel
|
||||||
|
}: ProviderColumnProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.column} ${styles.providers}`}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onContextMenu(e, 'background');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.columnHeader}>{label}</div>
|
||||||
|
{providerNodes.map(({ provider, sources }) => {
|
||||||
|
const collapsed = collapsedProviders.has(provider);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={provider}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) providerRefs.current?.set(provider, el);
|
||||||
|
else providerRefs.current?.delete(provider);
|
||||||
|
}}
|
||||||
|
className={`${styles.item} ${styles.providerItem}`}
|
||||||
|
style={{ borderLeftColor: getProviderColor(provider) }}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onContextMenu(e, 'provider', provider);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.collapseBtn}
|
||||||
|
onClick={() => onToggleCollapse(provider)}
|
||||||
|
aria-label={collapsed ? expandLabel : collapseLabel}
|
||||||
|
title={collapsed ? expandLabel : collapseLabel}
|
||||||
|
>
|
||||||
|
<span className={collapsed ? styles.chevronRight : styles.chevronDown} />
|
||||||
|
</button>
|
||||||
|
<span className={styles.providerLabel} style={{ color: getProviderColor(provider) }}>
|
||||||
|
{provider}
|
||||||
|
</span>
|
||||||
|
<span className={styles.itemCount}>{sources.length}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SourceColumnProps {
|
||||||
|
providerNodes: ProviderNode[];
|
||||||
|
collapsedProviders: Set<string>;
|
||||||
|
sourceRefs: RefObject<Map<string, HTMLDivElement>>;
|
||||||
|
getProviderColor: (provider: string) => string;
|
||||||
|
draggedSource: SourceNode | null;
|
||||||
|
dropTargetSource: string | null;
|
||||||
|
draggable: boolean;
|
||||||
|
onDragStart: (e: DragEvent, source: SourceNode) => void;
|
||||||
|
onDragEnd: () => void;
|
||||||
|
onDragOver: (e: DragEvent, source: SourceNode) => void;
|
||||||
|
onDragLeave: () => void;
|
||||||
|
onDrop: (e: DragEvent, source: SourceNode) => void;
|
||||||
|
onContextMenu: (e: ReactMouseEvent, type: 'source' | 'background', data?: string) => void;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SourceColumn({
|
||||||
|
providerNodes,
|
||||||
|
collapsedProviders,
|
||||||
|
sourceRefs,
|
||||||
|
getProviderColor,
|
||||||
|
draggedSource,
|
||||||
|
dropTargetSource,
|
||||||
|
draggable,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
onDragOver,
|
||||||
|
onDragLeave,
|
||||||
|
onDrop,
|
||||||
|
onContextMenu,
|
||||||
|
label
|
||||||
|
}: SourceColumnProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.column} ${styles.sources}`}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onContextMenu(e, 'background');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.columnHeader}>{label}</div>
|
||||||
|
{providerNodes.flatMap(({ provider, sources }) => {
|
||||||
|
if (collapsedProviders.has(provider)) return [];
|
||||||
|
return sources.map((source) => (
|
||||||
|
<div
|
||||||
|
key={source.id}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) sourceRefs.current?.set(source.id, el);
|
||||||
|
else sourceRefs.current?.delete(source.id);
|
||||||
|
}}
|
||||||
|
className={`${styles.item} ${styles.sourceItem} ${
|
||||||
|
draggedSource?.id === source.id ? styles.dragging : ''
|
||||||
|
} ${dropTargetSource === source.id ? styles.dropTarget : ''}`}
|
||||||
|
draggable={draggable}
|
||||||
|
onDragStart={(e) => onDragStart(e, source)}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragOver={(e) => onDragOver(e, source)}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
|
onDrop={(e) => onDrop(e, source)}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onContextMenu(e, 'source', source.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.itemName} title={source.name}>
|
||||||
|
{source.name}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={styles.dot}
|
||||||
|
style={{
|
||||||
|
background: getProviderColor(source.provider),
|
||||||
|
opacity: source.aliases.length > 0 ? 1 : 0.3
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AliasColumnProps {
|
||||||
|
aliasNodes: AliasNode[];
|
||||||
|
aliasRefs: RefObject<Map<string, HTMLDivElement>>;
|
||||||
|
dropTargetAlias: string | null;
|
||||||
|
draggedAlias: string | null;
|
||||||
|
draggable: boolean;
|
||||||
|
onDragStart: (e: DragEvent, alias: string) => void;
|
||||||
|
onDragEnd: () => void;
|
||||||
|
onDragOver: (e: DragEvent, alias: string) => void;
|
||||||
|
onDragLeave: () => void;
|
||||||
|
onDrop: (e: DragEvent, alias: string) => void;
|
||||||
|
onContextMenu: (e: ReactMouseEvent, type: 'alias' | 'background', data?: string) => void;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AliasColumn({
|
||||||
|
aliasNodes,
|
||||||
|
aliasRefs,
|
||||||
|
dropTargetAlias,
|
||||||
|
draggedAlias,
|
||||||
|
draggable,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
onDragOver,
|
||||||
|
onDragLeave,
|
||||||
|
onDrop,
|
||||||
|
onContextMenu,
|
||||||
|
label
|
||||||
|
}: AliasColumnProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.column} ${styles.aliases}`}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onContextMenu(e, 'background');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.columnHeader}>{label}</div>
|
||||||
|
{aliasNodes.map((node) => (
|
||||||
|
<div
|
||||||
|
key={node.id}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) aliasRefs.current?.set(node.id, el);
|
||||||
|
else aliasRefs.current?.delete(node.id);
|
||||||
|
}}
|
||||||
|
className={`${styles.item} ${styles.aliasItem} ${
|
||||||
|
dropTargetAlias === node.alias ? styles.dropTarget : ''
|
||||||
|
} ${draggedAlias === node.alias ? styles.dragging : ''}`}
|
||||||
|
draggable={draggable}
|
||||||
|
onDragStart={(e) => onDragStart(e, node.alias)}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragOver={(e) => onDragOver(e, node.alias)}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
|
onDrop={(e) => onDrop(e, node.alias)}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onContextMenu(e, 'alias', node.alias);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={`${styles.dot} ${styles.dotLeft}`} />
|
||||||
|
<span className={styles.itemName} title={node.alias}>
|
||||||
|
{node.alias}
|
||||||
|
</span>
|
||||||
|
<span className={styles.itemCount}>{node.sources.length}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
src/components/modelAlias/ModelMappingDiagramContextMenu.tsx
Normal file
111
src/components/modelAlias/ModelMappingDiagramContextMenu.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import type { TFunction } from 'i18next';
|
||||||
|
import type { ContextMenuState } from './ModelMappingDiagramTypes';
|
||||||
|
import styles from './ModelMappingDiagram.module.scss';
|
||||||
|
|
||||||
|
interface DiagramContextMenuProps {
|
||||||
|
contextMenu: ContextMenuState | null;
|
||||||
|
t: TFunction;
|
||||||
|
onRequestClose: () => void;
|
||||||
|
onAddAlias: () => void;
|
||||||
|
onRenameAlias: (alias: string) => void;
|
||||||
|
onOpenAliasSettings: (alias: string) => void;
|
||||||
|
onDeleteAlias: (alias: string) => void;
|
||||||
|
onEditProvider: (provider: string) => void;
|
||||||
|
onDeleteProvider: (provider: string) => void;
|
||||||
|
onOpenSourceSettings: (sourceId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiagramContextMenu({
|
||||||
|
contextMenu,
|
||||||
|
t,
|
||||||
|
onRequestClose,
|
||||||
|
onAddAlias,
|
||||||
|
onRenameAlias,
|
||||||
|
onOpenAliasSettings,
|
||||||
|
onDeleteAlias,
|
||||||
|
onEditProvider,
|
||||||
|
onDeleteProvider,
|
||||||
|
onOpenSourceSettings
|
||||||
|
}: DiagramContextMenuProps) {
|
||||||
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!contextMenu) return;
|
||||||
|
const handleClick = (event: globalThis.MouseEvent) => {
|
||||||
|
if (!menuRef.current?.contains(event.target as Node)) {
|
||||||
|
onRequestClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClick);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClick);
|
||||||
|
}, [contextMenu, onRequestClose]);
|
||||||
|
|
||||||
|
if (!contextMenu) return null;
|
||||||
|
|
||||||
|
const { type, data } = contextMenu;
|
||||||
|
|
||||||
|
const renderBackground = () => (
|
||||||
|
<div className={styles.menuItem} onClick={onAddAlias}>
|
||||||
|
<span>{t('oauth_model_alias.diagram_add_alias')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderAlias = () => {
|
||||||
|
if (!data) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.menuItem} onClick={() => onRenameAlias(data)}>
|
||||||
|
<span>{t('oauth_model_alias.diagram_rename')}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.menuItem} onClick={() => onOpenAliasSettings(data)}>
|
||||||
|
<span>{t('oauth_model_alias.diagram_settings')}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.menuDivider} />
|
||||||
|
<div className={`${styles.menuItem} ${styles.danger}`} onClick={() => onDeleteAlias(data)}>
|
||||||
|
<span>{t('oauth_model_alias.diagram_delete_alias')}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderProvider = () => {
|
||||||
|
if (!data) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.menuItem} onClick={() => onEditProvider(data)}>
|
||||||
|
<span>{t('common.edit')}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.menuDivider} />
|
||||||
|
<div className={`${styles.menuItem} ${styles.danger}`} onClick={() => onDeleteProvider(data)}>
|
||||||
|
<span>{t('oauth_model_alias.delete')}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSource = () => {
|
||||||
|
if (!data) return null;
|
||||||
|
return (
|
||||||
|
<div className={styles.menuItem} onClick={() => onOpenSourceSettings(data)}>
|
||||||
|
<span>{t('oauth_model_alias.diagram_settings')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className={styles.contextMenu}
|
||||||
|
style={{ top: contextMenu.y, left: contextMenu.x }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{type === 'background' && renderBackground()}
|
||||||
|
{type === 'alias' && renderAlias()}
|
||||||
|
{type === 'provider' && renderProvider()}
|
||||||
|
{type === 'source' && renderSource()}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
267
src/components/modelAlias/ModelMappingDiagramModals.tsx
Normal file
267
src/components/modelAlias/ModelMappingDiagramModals.tsx
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import type { KeyboardEvent } from 'react';
|
||||||
|
import type { TFunction } from 'i18next';
|
||||||
|
import { Modal } from '@/components/ui/Modal';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||||
|
import { IconTrash2 } from '@/components/ui/icons';
|
||||||
|
import type { AliasNode, SourceNode } from './ModelMappingDiagramTypes';
|
||||||
|
import styles from './ModelMappingDiagram.module.scss';
|
||||||
|
|
||||||
|
interface RenameAliasModalProps {
|
||||||
|
open: boolean;
|
||||||
|
t: TFunction;
|
||||||
|
value: string;
|
||||||
|
error: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RenameAliasModal({
|
||||||
|
open,
|
||||||
|
t,
|
||||||
|
value,
|
||||||
|
error,
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
onSubmit
|
||||||
|
}: RenameAliasModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t('oauth_model_alias.diagram_rename_alias_title')}
|
||||||
|
width={400}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onSubmit}>{t('oauth_model_alias.diagram_rename_btn')}</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label={t('oauth_model_alias.diagram_rename_alias_label')}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') onSubmit();
|
||||||
|
}}
|
||||||
|
error={error}
|
||||||
|
placeholder={t('oauth_model_alias.diagram_rename_placeholder')}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddAliasModalProps {
|
||||||
|
open: boolean;
|
||||||
|
t: TFunction;
|
||||||
|
value: string;
|
||||||
|
error: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddAliasModal({
|
||||||
|
open,
|
||||||
|
t,
|
||||||
|
value,
|
||||||
|
error,
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
onSubmit
|
||||||
|
}: AddAliasModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t('oauth_model_alias.diagram_add_alias_title')}
|
||||||
|
width={400}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onSubmit}>{t('oauth_model_alias.diagram_add_btn')}</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label={t('oauth_model_alias.diagram_add_alias_label')}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') onSubmit();
|
||||||
|
}}
|
||||||
|
error={error}
|
||||||
|
placeholder={t('oauth_model_alias.diagram_add_placeholder')}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsAliasModalProps {
|
||||||
|
open: boolean;
|
||||||
|
t: TFunction;
|
||||||
|
alias: string | null;
|
||||||
|
aliasNodes: AliasNode[];
|
||||||
|
onClose: () => void;
|
||||||
|
onToggleFork: (provider: string, sourceModel: string, alias: string, fork: boolean) => void;
|
||||||
|
onUnlink: (provider: string, sourceModel: string, alias: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsAliasModal({
|
||||||
|
open,
|
||||||
|
t,
|
||||||
|
alias,
|
||||||
|
aliasNodes,
|
||||||
|
onClose,
|
||||||
|
onToggleFork,
|
||||||
|
onUnlink
|
||||||
|
}: SettingsAliasModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t('oauth_model_alias.diagram_settings_title', { alias: alias ?? '' })}
|
||||||
|
width={720}
|
||||||
|
footer={
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
{t('common.close')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{alias ? (
|
||||||
|
(() => {
|
||||||
|
const node = aliasNodes.find((n) => n.alias === alias);
|
||||||
|
if (!node || node.sources.length === 0) {
|
||||||
|
return <div className={styles.settingsEmpty}>{t('oauth_model_alias.diagram_settings_empty')}</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.settingsList}>
|
||||||
|
{node.sources.map((source) => {
|
||||||
|
const entry = source.aliases.find((item) => item.alias === alias);
|
||||||
|
const forkEnabled = entry?.fork === true;
|
||||||
|
return (
|
||||||
|
<div key={source.id} className={styles.settingsRow}>
|
||||||
|
<div className={styles.settingsNames}>
|
||||||
|
<span className={styles.settingsSource}>{source.name}</span>
|
||||||
|
<span className={styles.settingsArrow}>→</span>
|
||||||
|
<span className={styles.settingsAlias}>{alias}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.settingsActions}>
|
||||||
|
<span className={styles.settingsLabel}>
|
||||||
|
{t('oauth_model_alias.alias_fork_label')}
|
||||||
|
</span>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={forkEnabled}
|
||||||
|
onChange={(value) => onToggleFork(source.provider, source.name, alias, value)}
|
||||||
|
ariaLabel={t('oauth_model_alias.alias_fork_label')}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.settingsDelete}
|
||||||
|
onClick={() => onUnlink(source.provider, source.name, alias)}
|
||||||
|
aria-label={t('oauth_model_alias.diagram_delete_link', {
|
||||||
|
provider: source.provider,
|
||||||
|
name: source.name
|
||||||
|
})}
|
||||||
|
title={t('oauth_model_alias.diagram_delete_link', {
|
||||||
|
provider: source.provider,
|
||||||
|
name: source.name
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<IconTrash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsSourceModalProps {
|
||||||
|
open: boolean;
|
||||||
|
t: TFunction;
|
||||||
|
source: SourceNode | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onToggleFork: (provider: string, sourceModel: string, alias: string, fork: boolean) => void;
|
||||||
|
onUnlink: (provider: string, sourceModel: string, alias: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsSourceModal({
|
||||||
|
open,
|
||||||
|
t,
|
||||||
|
source,
|
||||||
|
onClose,
|
||||||
|
onToggleFork,
|
||||||
|
onUnlink
|
||||||
|
}: SettingsSourceModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t('oauth_model_alias.diagram_settings_source_title')}
|
||||||
|
width={720}
|
||||||
|
footer={
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
{t('common.close')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{source ? (
|
||||||
|
source.aliases.length === 0 ? (
|
||||||
|
<div className={styles.settingsEmpty}>{t('oauth_model_alias.diagram_settings_empty')}</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.settingsList}>
|
||||||
|
{source.aliases.map((entry) => (
|
||||||
|
<div key={`${source.id}-${entry.alias}`} className={styles.settingsRow}>
|
||||||
|
<div className={styles.settingsNames}>
|
||||||
|
<span className={styles.settingsSource}>{source.name}</span>
|
||||||
|
<span className={styles.settingsArrow}>→</span>
|
||||||
|
<span className={styles.settingsAlias}>{entry.alias}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.settingsActions}>
|
||||||
|
<span className={styles.settingsLabel}>
|
||||||
|
{t('oauth_model_alias.alias_fork_label')}
|
||||||
|
</span>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={entry.fork === true}
|
||||||
|
onChange={(value) => onToggleFork(source.provider, source.name, entry.alias, value)}
|
||||||
|
ariaLabel={t('oauth_model_alias.alias_fork_label')}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.settingsDelete}
|
||||||
|
onClick={() => onUnlink(source.provider, source.name, entry.alias)}
|
||||||
|
aria-label={t('oauth_model_alias.diagram_delete_link', {
|
||||||
|
provider: source.provider,
|
||||||
|
name: source.name
|
||||||
|
})}
|
||||||
|
title={t('oauth_model_alias.diagram_delete_link', {
|
||||||
|
provider: source.provider,
|
||||||
|
name: source.name
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<IconTrash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/components/modelAlias/ModelMappingDiagramTypes.ts
Normal file
33
src/components/modelAlias/ModelMappingDiagramTypes.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export interface AuthFileModelItem {
|
||||||
|
id: string;
|
||||||
|
display_name?: string;
|
||||||
|
type?: string;
|
||||||
|
owned_by?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SourceNode {
|
||||||
|
id: string; // unique: provider::name
|
||||||
|
provider: string;
|
||||||
|
name: string;
|
||||||
|
aliases: { alias: string; fork: boolean }[]; // all aliases this source maps to
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AliasNode {
|
||||||
|
id: string; // alias
|
||||||
|
alias: string;
|
||||||
|
sources: SourceNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderNode {
|
||||||
|
provider: string;
|
||||||
|
sources: SourceNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextMenuState {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
type: 'alias' | 'background' | 'provider' | 'source';
|
||||||
|
data?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DiagramLine = { path: string; color: string; id: string };
|
||||||
Reference in New Issue
Block a user