Commit 5e866446 by CaIon

feat(dashboard): add sensitive data toggle to flow chart

Add an eye toggle in the flow section header that masks sensitive node
labels (users, tokens, nodes, groups, channels) in the Sankey while keeping
model names visible. Masking only rewrites display text; nodes stay distinct
via their key so graph structure, links, and highlighting are unaffected.
parent 8ad83bf6
...@@ -94,6 +94,8 @@ import { FlowNodeFilterControl } from './flow-node-filter' ...@@ -94,6 +94,8 @@ import { FlowNodeFilterControl } from './flow-node-filter'
interface FlowChartsProps { interface FlowChartsProps {
filters?: DashboardFilters filters?: DashboardFilters
// When false, sensitive node labels are masked in the rendered Sankey.
sensitiveVisible?: boolean
} }
const FLOW_METRIC_OPTIONS = [ const FLOW_METRIC_OPTIONS = [
...@@ -340,6 +342,7 @@ export function FlowCharts(props: FlowChartsProps) { ...@@ -340,6 +342,7 @@ export function FlowCharts(props: FlowChartsProps) {
staleTime: 60_000, staleTime: 60_000,
}) })
const maskSensitive = props.sensitiveVisible === false
const flowData = useMemo( const flowData = useMemo(
() => () =>
buildDashboardFlowData(isLoading ? [] : (flowRows ?? []), metric, { buildDashboardFlowData(isLoading ? [] : (flowRows ?? []), metric, {
...@@ -351,6 +354,7 @@ export function FlowCharts(props: FlowChartsProps) { ...@@ -351,6 +354,7 @@ export function FlowCharts(props: FlowChartsProps) {
visibleStages, visibleStages,
topNodeLimit, topNodeLimit,
overflowMode, overflowMode,
maskSensitive,
deletedTokenLabel: (tokenId) => t('Deleted ({{id}})', { id: tokenId }), deletedTokenLabel: (tokenId) => t('Deleted ({{id}})', { id: tokenId }),
otherNodeLabel: (kind) => t(FLOW_OTHER_NODE_LABEL_KEYS[kind]), otherNodeLabel: (kind) => t(FLOW_OTHER_NODE_LABEL_KEYS[kind]),
}), }),
...@@ -366,6 +370,7 @@ export function FlowCharts(props: FlowChartsProps) { ...@@ -366,6 +370,7 @@ export function FlowCharts(props: FlowChartsProps) {
selectedUsers, selectedUsers,
topNodeLimit, topNodeLimit,
visibleStages, visibleStages,
maskSensitive,
t, t,
] ]
) )
...@@ -468,6 +473,7 @@ export function FlowCharts(props: FlowChartsProps) { ...@@ -468,6 +473,7 @@ export function FlowCharts(props: FlowChartsProps) {
selectedNodes.map(flowNodeFilterKey).join(','), selectedNodes.map(flowNodeFilterKey).join(','),
selectedUsers.join(','), selectedUsers.join(','),
visibleStages.join(','), visibleStages.join(','),
maskSensitive ? 'masked' : 'plain',
flowRows?.length ?? 0, flowRows?.length ?? 0,
resolvedTheme, resolvedTheme,
].join('-') ].join('-')
......
...@@ -18,11 +18,18 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,11 +18,18 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import { useState, useCallback, useMemo, lazy, Suspense } from 'react' import { useState, useCallback, useMemo, lazy, Suspense } from 'react'
import { getRouteApi, useNavigate } from '@tanstack/react-router' import { getRouteApi, useNavigate } from '@tanstack/react-router'
import { Eye, EyeOff } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/stores/auth-store' import { useAuthStore } from '@/stores/auth-store'
import { ROLE } from '@/lib/roles' import { ROLE } from '@/lib/roles'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { SectionPageLayout } from '@/components/layout' import { SectionPageLayout } from '@/components/layout'
import { FadeIn } from '@/components/page-transition' import { FadeIn } from '@/components/page-transition'
import { ModelsChartPreferences } from './components/models/models-chart-preferences' import { ModelsChartPreferences } from './components/models/models-chart-preferences'
...@@ -179,6 +186,7 @@ export function Dashboard() { ...@@ -179,6 +186,7 @@ export function Dashboard() {
} }
} }
) )
const [flowSensitiveVisible, setFlowSensitiveVisible] = useState(true)
const handleFilterChange = useCallback((filters: DashboardFilters) => { const handleFilterChange = useCallback((filters: DashboardFilters) => {
setModelFilters(filters) setModelFilters(filters)
...@@ -242,14 +250,40 @@ export function Dashboard() { ...@@ -242,14 +250,40 @@ export function Dashboard() {
) : null ) : null
const flowActions = const flowActions =
activeSection === 'flow' ? ( activeSection === 'flow' ? (
<ModelsFilter <>
preferences={chartPreferences} <Tooltip>
currentFilters={modelFilters} <TooltipTrigger
onFilterChange={handleFilterChange} render={
onReset={handleResetFilters} <Button
titleKey='Flow Filters' variant='ghost'
descriptionKey='Filter the traffic flow view by time range and user.' size='icon'
/> onClick={() => setFlowSensitiveVisible((prev) => !prev)}
aria-label={
flowSensitiveVisible
? t('Hide sensitive data')
: t('Show sensitive data')
}
className='text-muted-foreground hover:text-foreground size-8'
/>
}
>
{flowSensitiveVisible ? <Eye /> : <EyeOff />}
</TooltipTrigger>
<TooltipContent>
{flowSensitiveVisible
? t('Hide sensitive data')
: t('Show sensitive data')}
</TooltipContent>
</Tooltip>
<ModelsFilter
preferences={chartPreferences}
currentFilters={modelFilters}
onFilterChange={handleFilterChange}
onReset={handleResetFilters}
titleKey='Flow Filters'
descriptionKey='Filter the traffic flow view by time range and user.'
/>
</>
) : null ) : null
const sectionActions = modelActions ?? flowActions const sectionActions = modelActions ?? flowActions
...@@ -339,7 +373,10 @@ export function Dashboard() { ...@@ -339,7 +373,10 @@ export function Dashboard() {
{activeSection === 'flow' && ( {activeSection === 'flow' && (
<FadeIn> <FadeIn>
<Suspense fallback={<ModelChartsFallback />}> <Suspense fallback={<ModelChartsFallback />}>
<LazyFlowCharts filters={modelFilters} /> <LazyFlowCharts
filters={modelFilters}
sensitiveVisible={flowSensitiveVisible}
/>
</Suspense> </Suspense>
</FadeIn> </FadeIn>
)} )}
......
...@@ -77,6 +77,7 @@ type FlowGraphOptions = { ...@@ -77,6 +77,7 @@ type FlowGraphOptions = {
otherNodeLabel?: (kind: FlowNodeKind) => string otherNodeLabel?: (kind: FlowNodeKind) => string
activeNode?: FlowNodeFilter activeNode?: FlowNodeFilter
activeLink?: FlowLinkSelection activeLink?: FlowLinkSelection
maskSensitive?: boolean
} }
type FlowHighlightSets = { type FlowHighlightSets = {
...@@ -127,6 +128,18 @@ const DEFAULT_OTHER_FLOW_NODE_LABELS: Record<FlowNodeKind, string> = { ...@@ -127,6 +128,18 @@ const DEFAULT_OTHER_FLOW_NODE_LABELS: Record<FlowNodeKind, string> = {
channel: 'Other channels', channel: 'Other channels',
} }
// Kinds whose labels can leak identity (people, keys, infra, business setup).
// Model names are public, so they stay visible even when masking is on.
const SENSITIVE_FLOW_KINDS = new Set<FlowNodeKind>([
'user',
'node',
'token',
'group',
'channel',
])
const OTHER_FLOW_NODE_ID_SET = new Set<string>(Object.values(OTHER_FLOW_NODE_IDS))
function numberValue(value: unknown): number { function numberValue(value: unknown): number {
const n = Number(value) const n = Number(value)
return Number.isFinite(n) ? n : 0 return Number.isFinite(n) ? n : 0
...@@ -663,6 +676,40 @@ function buildFlowHighlightSets( ...@@ -663,6 +676,40 @@ function buildFlowHighlightSets(
} }
} }
// Fully masks a label. Nodes stay distinct because the Sankey identifies them
// by `key` (the node id), not by this display text, so identical masked labels
// never merge.
const FLOW_MASK_TEXT = '\u2022\u2022\u2022\u2022'
function maskFlowLabel(label: string): string {
if (label.length === 0) return label
return FLOW_MASK_TEXT
}
// Masks sensitive node/link labels in place. Node identity (`id`) is untouched,
// so links, highlighting, and layout stay exactly the same; only the rendered
// text changes.
function maskFlowGraphLabels(
nodes: Map<string, DashboardFlowNode>,
links: Map<string, DashboardFlowLink>
): void {
const maskedById = new Map<string, string>()
for (const node of nodes.values()) {
if (!SENSITIVE_FLOW_KINDS.has(node.kind)) continue
if (OTHER_FLOW_NODE_ID_SET.has(node.id)) continue
const masked = maskFlowLabel(node.label)
node.label = masked
maskedById.set(node.id, masked)
}
if (maskedById.size === 0) return
for (const link of links.values()) {
const sourceMasked = maskedById.get(link.source)
if (sourceMasked !== undefined) link.sourceLabel = sourceMasked
const targetMasked = maskedById.get(link.target)
if (targetMasked !== undefined) link.targetLabel = targetMasked
}
}
function applyFlowHighlights( function applyFlowHighlights(
nodes: Iterable<DashboardFlowNode>, nodes: Iterable<DashboardFlowNode>,
links: Iterable<DashboardFlowLink>, links: Iterable<DashboardFlowLink>,
...@@ -738,6 +785,9 @@ function buildFlowGraph( ...@@ -738,6 +785,9 @@ function buildFlowGraph(
addLink(links, source, target, metrics, metric, color, root.id) addLink(links, source, target, metrics, metric, color, root.id)
} }
} }
if (options.maskSensitive) {
maskFlowGraphLabels(nodes, links)
}
applyFlowHighlights( applyFlowHighlights(
nodes.values(), nodes.values(),
links.values(), links.values(),
...@@ -948,6 +998,7 @@ export function buildDashboardFlowData( ...@@ -948,6 +998,7 @@ export function buildDashboardFlowData(
otherNodeLabel: options.otherNodeLabel, otherNodeLabel: options.otherNodeLabel,
activeNode: options.activeNode, activeNode: options.activeNode,
activeLink: options.activeLink, activeLink: options.activeLink,
maskSensitive: options.maskSensitive,
} }
), ),
filterOptions: { filterOptions: {
......
...@@ -82,6 +82,10 @@ export interface FlowBuildOptions { ...@@ -82,6 +82,10 @@ export interface FlowBuildOptions {
visibleStages?: FlowNodeKind[] visibleStages?: FlowNodeKind[]
topNodeLimit?: number topNodeLimit?: number
overflowMode?: FlowOverflowMode overflowMode?: FlowOverflowMode
// When true, sensitive node labels (users, tokens, nodes, groups, channels)
// are partially masked in the rendered graph while keeping node identity so
// the Sankey shape stays intact.
maskSensitive?: boolean
// Resolves the label for a token whose record no longer exists (deleted). // Resolves the label for a token whose record no longer exists (deleted).
// Lets the caller inject a localized string such as "Deleted (123)". // Lets the caller inject a localized string such as "Deleted (123)".
deletedTokenLabel?: (tokenId: number) => string deletedTokenLabel?: (tokenId: number) => string
......
...@@ -2051,6 +2051,7 @@ ...@@ -2051,6 +2051,7 @@
"Hidden — verify to reveal": "Hidden — verify to reveal", "Hidden — verify to reveal": "Hidden — verify to reveal",
"Hide": "Hide", "Hide": "Hide",
"Hide API key": "Hide API key", "Hide API key": "Hide API key",
"Hide sensitive data": "Hide sensitive data",
"Hide setup guide": "Hide setup guide", "Hide setup guide": "Hide setup guide",
"High Performance": "High Performance", "High Performance": "High Performance",
"High-risk operation confirmation": "High-risk Operation Confirmation", "High-risk operation confirmation": "High-risk Operation Confirmation",
...@@ -3870,6 +3871,7 @@ ...@@ -3870,6 +3871,7 @@
"Shorten": "Shorten", "Shorten": "Shorten",
"Show": "Show", "Show": "Show",
"Show All": "Show All", "Show All": "Show All",
"Show sensitive data": "Show sensitive data",
"Show all providers including unbound": "Show all providers including unbound", "Show all providers including unbound": "Show all providers including unbound",
"Show only bound providers": "Show only bound providers", "Show only bound providers": "Show only bound providers",
"Show or hide flow columns": "Show or hide flow columns", "Show or hide flow columns": "Show or hide flow columns",
......
...@@ -2051,6 +2051,7 @@ ...@@ -2051,6 +2051,7 @@
"Hidden — verify to reveal": "Masqué — vérifiez pour révéler", "Hidden — verify to reveal": "Masqué — vérifiez pour révéler",
"Hide": "Masquer", "Hide": "Masquer",
"Hide API key": "Masquer la clé API", "Hide API key": "Masquer la clé API",
"Hide sensitive data": "Masquer les données sensibles",
"Hide setup guide": "Masquer le guide de configuration", "Hide setup guide": "Masquer le guide de configuration",
"High Performance": "Hautes performances", "High Performance": "Hautes performances",
"High-risk operation confirmation": "Confirmation d'opération à haut risque", "High-risk operation confirmation": "Confirmation d'opération à haut risque",
...@@ -3870,6 +3871,7 @@ ...@@ -3870,6 +3871,7 @@
"Shorten": "Raccourcir", "Shorten": "Raccourcir",
"Show": "Afficher", "Show": "Afficher",
"Show All": "Tout afficher", "Show All": "Tout afficher",
"Show sensitive data": "Afficher les données sensibles",
"Show all providers including unbound": "Afficher tous les fournisseurs (y compris non liés)", "Show all providers including unbound": "Afficher tous les fournisseurs (y compris non liés)",
"Show only bound providers": "Afficher uniquement les fournisseurs liés", "Show only bound providers": "Afficher uniquement les fournisseurs liés",
"Show or hide flow columns": "Afficher ou masquer les colonnes du flux", "Show or hide flow columns": "Afficher ou masquer les colonnes du flux",
......
...@@ -2051,6 +2051,7 @@ ...@@ -2051,6 +2051,7 @@
"Hidden — verify to reveal": "非表示 — 確認して表示", "Hidden — verify to reveal": "非表示 — 確認して表示",
"Hide": "非表示にする", "Hide": "非表示にする",
"Hide API key": "APIキーを非表示", "Hide API key": "APIキーを非表示",
"Hide sensitive data": "機密データを非表示",
"Hide setup guide": "セットアップガイドを非表示", "Hide setup guide": "セットアップガイドを非表示",
"High Performance": "高パフォーマンス", "High Performance": "高パフォーマンス",
"High-risk operation confirmation": "高リスク操作の確認", "High-risk operation confirmation": "高リスク操作の確認",
...@@ -3870,6 +3871,7 @@ ...@@ -3870,6 +3871,7 @@
"Shorten": "短縮", "Shorten": "短縮",
"Show": "表示", "Show": "表示",
"Show All": "すべて表示", "Show All": "すべて表示",
"Show sensitive data": "機密データを表示",
"Show all providers including unbound": "未バインドを含むすべてのプロバイダーを表示", "Show all providers including unbound": "未バインドを含むすべてのプロバイダーを表示",
"Show only bound providers": "バインド済みのプロバイダーのみ表示", "Show only bound providers": "バインド済みのプロバイダーのみ表示",
"Show or hide flow columns": "フロー列の表示・非表示", "Show or hide flow columns": "フロー列の表示・非表示",
......
...@@ -2051,6 +2051,7 @@ ...@@ -2051,6 +2051,7 @@
"Hidden — verify to reveal": "Скрыто — подтвердите, чтобы показать", "Hidden — verify to reveal": "Скрыто — подтвердите, чтобы показать",
"Hide": "Скрыть", "Hide": "Скрыть",
"Hide API key": "Скрыть API ключ", "Hide API key": "Скрыть API ключ",
"Hide sensitive data": "Скрыть конфиденциальные данные",
"Hide setup guide": "Скрыть руководство по настройке", "Hide setup guide": "Скрыть руководство по настройке",
"High Performance": "Высокая производительность", "High Performance": "Высокая производительность",
"High-risk operation confirmation": "Подтверждение высокорисковой операции", "High-risk operation confirmation": "Подтверждение высокорисковой операции",
...@@ -3870,6 +3871,7 @@ ...@@ -3870,6 +3871,7 @@
"Shorten": "Сократить", "Shorten": "Сократить",
"Show": "Показать", "Show": "Показать",
"Show All": "Показать все", "Show All": "Показать все",
"Show sensitive data": "Показать конфиденциальные данные",
"Show all providers including unbound": "Показать всех провайдеров (включая непривязанные)", "Show all providers including unbound": "Показать всех провайдеров (включая непривязанные)",
"Show only bound providers": "Показать только привязанных провайдеров", "Show only bound providers": "Показать только привязанных провайдеров",
"Show or hide flow columns": "Показать или скрыть столбцы потока", "Show or hide flow columns": "Показать или скрыть столбцы потока",
......
...@@ -2051,6 +2051,7 @@ ...@@ -2051,6 +2051,7 @@
"Hidden — verify to reveal": "Ẩn — xác minh để hiển thị", "Hidden — verify to reveal": "Ẩn — xác minh để hiển thị",
"Hide": "Ẩn", "Hide": "Ẩn",
"Hide API key": "Ẩn khóa API", "Hide API key": "Ẩn khóa API",
"Hide sensitive data": "Ẩn dữ liệu nhạy cảm",
"Hide setup guide": "Ẩn hướng dẫn thiết lập", "Hide setup guide": "Ẩn hướng dẫn thiết lập",
"High Performance": "Hiệu suất cao", "High Performance": "Hiệu suất cao",
"High-risk operation confirmation": "Xác nhận thao tác rủi ro cao", "High-risk operation confirmation": "Xác nhận thao tác rủi ro cao",
...@@ -3870,6 +3871,7 @@ ...@@ -3870,6 +3871,7 @@
"Shorten": "Rút gọn", "Shorten": "Rút gọn",
"Show": "Hiển thị", "Show": "Hiển thị",
"Show All": "Hiển thị tất cả", "Show All": "Hiển thị tất cả",
"Show sensitive data": "Hiển thị dữ liệu nhạy cảm",
"Show all providers including unbound": "Hiển thị tất cả nhà cung cấp (bao gồm chưa liên kết)", "Show all providers including unbound": "Hiển thị tất cả nhà cung cấp (bao gồm chưa liên kết)",
"Show only bound providers": "Chỉ hiển thị nhà cung cấp đã liên kết", "Show only bound providers": "Chỉ hiển thị nhà cung cấp đã liên kết",
"Show or hide flow columns": "Hiện hoặc ẩn các cột luồng", "Show or hide flow columns": "Hiện hoặc ẩn các cột luồng",
......
...@@ -2051,6 +2051,7 @@ ...@@ -2051,6 +2051,7 @@
"Hidden — verify to reveal": "隐藏 — 验证以显示", "Hidden — verify to reveal": "隐藏 — 验证以显示",
"Hide": "隐藏", "Hide": "隐藏",
"Hide API key": "隐藏 API 密钥", "Hide API key": "隐藏 API 密钥",
"Hide sensitive data": "隐藏敏感数据",
"Hide setup guide": "隐藏设置引导", "Hide setup guide": "隐藏设置引导",
"High Performance": "高性能", "High Performance": "高性能",
"High-risk operation confirmation": "高危操作确认", "High-risk operation confirmation": "高危操作确认",
...@@ -3870,6 +3871,7 @@ ...@@ -3870,6 +3871,7 @@
"Shorten": "缩词", "Shorten": "缩词",
"Show": "显示", "Show": "显示",
"Show All": "显示全部", "Show All": "显示全部",
"Show sensitive data": "显示敏感数据",
"Show all providers including unbound": "显示所有提供商(包括未绑定)", "Show all providers including unbound": "显示所有提供商(包括未绑定)",
"Show only bound providers": "仅显示已绑定的提供商", "Show only bound providers": "仅显示已绑定的提供商",
"Show or hide flow columns": "显示或隐藏分流列", "Show or hide flow columns": "显示或隐藏分流列",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment