feat(dashboard): add traffic flow sankey chart (#5465)
* feat(dashboard): add traffic flow sankey chart Add dashboard flow APIs and a Sankey-based flow view with user, optional API key, model, and channel layers.\n\nReuse the dashboard VChart palette, add precise link/node tooltips and interactions, and cover filtering, layer ordering, color stability, and error states with tests. * feat: build flow chart from quota data --------- Co-authored-by: CaIon <i@caion.me>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'node:test'
|
||||
import type { FlowUserFilterOption } from '../types'
|
||||
import {
|
||||
compactFlowSelectionLabel,
|
||||
flowDisplayState,
|
||||
requireSuccessfulFlowRows,
|
||||
visibleFlowUsers,
|
||||
} from './flow-selection'
|
||||
|
||||
const users: FlowUserFilterOption[] = [
|
||||
{
|
||||
value: 'user:1',
|
||||
label: 'dry',
|
||||
valueLabel: '100',
|
||||
valueRaw: 100,
|
||||
color: '#1664ff',
|
||||
},
|
||||
{
|
||||
value: 'user:2',
|
||||
label: 'jrc',
|
||||
valueLabel: '70',
|
||||
valueRaw: 70,
|
||||
color: '#1ac6ff',
|
||||
},
|
||||
]
|
||||
|
||||
describe('dashboard flow selection helpers', () => {
|
||||
test('limits user chips to currently visible users', () => {
|
||||
assert.deepEqual(
|
||||
visibleFlowUsers(users, []).map((user) => user.value),
|
||||
['user:1', 'user:2']
|
||||
)
|
||||
assert.deepEqual(
|
||||
visibleFlowUsers(users, ['user:2']).map((user) => user.value),
|
||||
['user:2']
|
||||
)
|
||||
})
|
||||
|
||||
test('filters visible users without mutating the source options', () => {
|
||||
const visible = visibleFlowUsers(users, ['user:1'])
|
||||
|
||||
assert.deepEqual(
|
||||
visible.map((user) => user.value),
|
||||
['user:1']
|
||||
)
|
||||
assert.deepEqual(
|
||||
users.map((user) => user.value),
|
||||
['user:1', 'user:2']
|
||||
)
|
||||
})
|
||||
|
||||
test('formats compact selected counts for flow multiselect summaries', () => {
|
||||
assert.equal(compactFlowSelectionLabel(0), '*')
|
||||
assert.equal(compactFlowSelectionLabel(1), '1')
|
||||
assert.equal(compactFlowSelectionLabel(23), '23')
|
||||
})
|
||||
|
||||
test('prioritizes loading and error states before empty flow data', () => {
|
||||
assert.equal(
|
||||
flowDisplayState({
|
||||
isLoading: true,
|
||||
isError: true,
|
||||
linkCount: 0,
|
||||
themeReady: true,
|
||||
}),
|
||||
'loading'
|
||||
)
|
||||
assert.equal(
|
||||
flowDisplayState({
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
linkCount: 0,
|
||||
themeReady: true,
|
||||
}),
|
||||
'error'
|
||||
)
|
||||
assert.equal(
|
||||
flowDisplayState({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
linkCount: 0,
|
||||
themeReady: true,
|
||||
}),
|
||||
'empty'
|
||||
)
|
||||
assert.equal(
|
||||
flowDisplayState({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
linkCount: 1,
|
||||
themeReady: false,
|
||||
}),
|
||||
'loading'
|
||||
)
|
||||
})
|
||||
|
||||
test('throws unsuccessful flow responses instead of treating them as empty data', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
requireSuccessfulFlowRows(
|
||||
{ success: false, data: [], message: 'database unavailable' },
|
||||
'Failed to load'
|
||||
),
|
||||
/database unavailable/
|
||||
)
|
||||
assert.deepEqual(
|
||||
requireSuccessfulFlowRows(
|
||||
{ success: true, data: [{ user_id: 1, quota: 10 }] },
|
||||
'Failed to load'
|
||||
),
|
||||
[{ user_id: 1, quota: 10 }]
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user