Commit 9100e15e by CaIon

feat: add channel sensitive info toggle

parent 4206d7fd
...@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import * as React from 'react' import * as React from 'react'
import { useState, type ReactNode } from 'react' import { useState, type ReactNode } from 'react'
import { type Table } from '@tanstack/react-table' import type { Table } from '@tanstack/react-table'
import { useDebounce } from '@/hooks' import { useDebounce } from '@/hooks'
import { ChevronDown, Loader2, X as Cross2Icon } from 'lucide-react' import { ChevronDown, Loader2, X as Cross2Icon } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
...@@ -77,6 +77,11 @@ export type DataTableToolbarProps<TData> = { ...@@ -77,6 +77,11 @@ export type DataTableToolbarProps<TData> = {
*/ */
additionalSearch?: ReactNode additionalSearch?: ReactNode
/** /**
* Extra controls displayed immediately after the filter chips, before the
* right-aligned action cluster.
*/
afterFilters?: ReactNode
/**
* Whether non-table filters (e.g. `additionalSearch` or `expandable` * Whether non-table filters (e.g. `additionalSearch` or `expandable`
* inputs) are currently active. Controls Reset button visibility * inputs) are currently active. Controls Reset button visibility
* when no column filters are set. * when no column filters are set.
...@@ -282,20 +287,25 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) { ...@@ -282,20 +287,25 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
// Reset: outline text-only for form mode (always visible, disabled when // Reset: outline text-only for form mode (always visible, disabled when
// nothing to reset); ghost text + X for filter-as-you-type mode (only // nothing to reset); ghost text + X for filter-as-you-type mode (only
// visible when active filters exist). // visible when active filters exist).
const resetButton = hasSearch ? ( let resetButton: ReactNode = null
<Button variant='outline' onClick={handleReset} disabled={!isFiltered}> if (hasSearch) {
{t('Reset')} resetButton = (
</Button> <Button variant='outline' onClick={handleReset} disabled={!isFiltered}>
) : isFiltered ? ( {t('Reset')}
<Button </Button>
variant='ghost' )
onClick={handleReset} } else if (isFiltered) {
className='text-muted-foreground hover:text-foreground gap-1 px-2' resetButton = (
> <Button
{t('Reset')} variant='ghost'
<Cross2Icon /> onClick={handleReset}
</Button> className='text-muted-foreground hover:text-foreground gap-1 px-2'
) : null >
{t('Reset')}
<Cross2Icon />
</Button>
)
}
const searchButton = hasSearch ? ( const searchButton = hasSearch ? (
<Button onClick={props.onSearch} disabled={props.searchLoading}> <Button onClick={props.onSearch} disabled={props.searchLoading}>
...@@ -341,6 +351,7 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) { ...@@ -341,6 +351,7 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
{props.customSearch !== undefined ? props.customSearch : searchInput} {props.customSearch !== undefined ? props.customSearch : searchInput}
{props.additionalSearch} {props.additionalSearch}
{filterChips} {filterChips}
{props.afterFilters}
<div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'> <div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'>
{expandToggle} {expandToggle}
</div> </div>
...@@ -376,6 +387,7 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) { ...@@ -376,6 +387,7 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
{props.customSearch !== undefined ? props.customSearch : searchInput} {props.customSearch !== undefined ? props.customSearch : searchInput}
{props.additionalSearch} {props.additionalSearch}
{filterChips} {filterChips}
{props.afterFilters}
{expanded && hasExpandable && props.expandable} {expanded && hasExpandable && props.expandable}
<div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'> <div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'>
......
...@@ -24,6 +24,9 @@ import { CHANNEL_STATUS } from '../constants' ...@@ -24,6 +24,9 @@ import { CHANNEL_STATUS } from '../constants'
import { isTagAggregateRow, parseGroupsList } from '../lib' import { isTagAggregateRow, parseGroupsList } from '../lib'
import type { Channel } from '../types' import type { Channel } from '../types'
import { ChannelRowActionsLayoutContext } from './channel-row-actions-context' import { ChannelRowActionsLayoutContext } from './channel-row-actions-context'
import { useChannels } from './channels-provider'
const SENSITIVE_MASK = '••••'
/** /**
* Bespoke channel card for the card view. Reuses every column's existing cell * Bespoke channel card for the card view. Reuses every column's existing cell
...@@ -35,6 +38,7 @@ import { ChannelRowActionsLayoutContext } from './channel-row-actions-context' ...@@ -35,6 +38,7 @@ import { ChannelRowActionsLayoutContext } from './channel-row-actions-context'
*/ */
export function ChannelCard({ row }: { row: Row<Channel> }) { export function ChannelCard({ row }: { row: Row<Channel> }) {
const { t } = useTranslation() const { t } = useTranslation()
const { sensitiveVisible } = useChannels()
const isTagRow = isTagAggregateRow(row.original) const isTagRow = isTagAggregateRow(row.original)
const cells = row.getAllCells() const cells = row.getAllCells()
...@@ -99,7 +103,11 @@ export function ChannelCard({ row }: { row: Row<Channel> }) { ...@@ -99,7 +103,11 @@ export function ChannelCard({ row }: { row: Row<Channel> }) {
{/* Left column */} {/* Left column */}
<div className='flex min-w-0 flex-1 flex-col gap-3 overflow-hidden'> <div className='flex min-w-0 flex-1 flex-col gap-3 overflow-hidden'>
<div className='min-w-0 text-sm'> <div className='min-w-0 text-sm'>
{!isTagRow && <div className={labelClass}>#{row.original.id}</div>} {!isTagRow && (
<div className={labelClass}>
#{sensitiveVisible ? row.original.id : SENSITIVE_MASK}
</div>
)}
{nameCell} {nameCell}
</div> </div>
<div className='min-w-0'> <div className='min-w-0'>
...@@ -138,7 +146,12 @@ export function ChannelCard({ row }: { row: Row<Channel> }) { ...@@ -138,7 +146,12 @@ export function ChannelCard({ row }: { row: Row<Channel> }) {
{groups.length > 0 ? ( {groups.length > 0 ? (
<div className='-ml-1.5 flex flex-wrap gap-1'> <div className='-ml-1.5 flex flex-wrap gap-1'>
{groups.map((g) => ( {groups.map((g) => (
<GroupBadge key={g} group={g} size='sm' /> <GroupBadge
key={g}
group={g}
label={sensitiveVisible ? undefined : SENSITIVE_MASK}
size='sm'
/>
))} ))}
</div> </div>
) : ( ) : (
......
...@@ -285,6 +285,7 @@ function WeightCell({ channel }: { channel: Channel }) { ...@@ -285,6 +285,7 @@ function WeightCell({ channel }: { channel: Channel }) {
* notation (e.g. "$28万"); the precise value stays available in the tooltip. * notation (e.g. "$28万"); the precise value stays available in the tooltip.
*/ */
const MAX_INLINE_BALANCE_CHARS = 8 const MAX_INLINE_BALANCE_CHARS = 8
const SENSITIVE_MASK = '••••'
/** /**
* Balance cell component with click to update * Balance cell component with click to update
...@@ -292,6 +293,7 @@ const MAX_INLINE_BALANCE_CHARS = 8 ...@@ -292,6 +293,7 @@ const MAX_INLINE_BALANCE_CHARS = 8
function BalanceCell({ channel }: { channel: Channel }) { function BalanceCell({ channel }: { channel: Channel }) {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { sensitiveVisible } = useChannels()
const isTagRow = isTagAggregateRow(channel) const isTagRow = isTagAggregateRow(channel)
const balance = channel.balance || 0 const balance = channel.balance || 0
const usedQuota = channel.used_quota || 0 const usedQuota = channel.used_quota || 0
...@@ -320,6 +322,8 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -320,6 +322,8 @@ function BalanceCell({ channel }: { channel: Channel }) {
: remainingFull : remainingFull
const usedLabel = `${t('Used:')} ${usedFull}` const usedLabel = `${t('Used:')} ${usedFull}`
const remainingLabel = `${t('Remaining:')} ${remainingFull}` const remainingLabel = `${t('Remaining:')} ${remainingFull}`
const maskedUsedLabel = `${t('Used:')} ${SENSITIVE_MASK}`
const maskedRemainingLabel = `${t('Remaining:')} ${SENSITIVE_MASK}`
// Tag row: only show cumulative used quota // Tag row: only show cumulative used quota
if (isTagRow) { if (isTagRow) {
...@@ -329,7 +333,11 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -329,7 +333,11 @@ function BalanceCell({ channel }: { channel: Channel }) {
<TooltipTrigger <TooltipTrigger
render={ render={
<StatusBadge <StatusBadge
label={`${t('Used:')} ${usedDisplay}`} label={
sensitiveVisible
? `${t('Used:')} ${usedDisplay}`
: maskedUsedLabel
}
variant='neutral' variant='neutral'
size='sm' size='sm'
copyable={false} copyable={false}
...@@ -339,7 +347,7 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -339,7 +347,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
} }
/> />
<TooltipContent> <TooltipContent>
<p>{usedLabel}</p> <p>{sensitiveVisible ? usedLabel : maskedUsedLabel}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
...@@ -376,12 +384,20 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -376,12 +384,20 @@ function BalanceCell({ channel }: { channel: Channel }) {
await handleUpdateChannelBalance(channel.id, queryClient) await handleUpdateChannelBalance(channel.id, queryClient)
setIsUpdating(false) setIsUpdating(false)
} }
let remainingBadgeLabel = remainingDisplay let remainingBadgeLabel = sensitiveVisible
if (isUpdating) { ? remainingDisplay
: SENSITIVE_MASK
if (sensitiveVisible && isUpdating) {
remainingBadgeLabel = t('Updating...') remainingBadgeLabel = t('Updating...')
} else if (channel.type === 57) { } else if (sensitiveVisible && channel.type === 57) {
remainingBadgeLabel = t('Account Info') remainingBadgeLabel = t('Account Info')
} }
let remainingTooltipLabel = remainingLabel
if (!sensitiveVisible) {
remainingTooltipLabel = maskedRemainingLabel
} else if (channel.type === 57) {
remainingTooltipLabel = t('Click to view Codex usage')
}
let remainingBadgeVariant: StatusBadgeProps['variant'] = variant let remainingBadgeVariant: StatusBadgeProps['variant'] = variant
if (channel.type === 57) { if (channel.type === 57) {
remainingBadgeVariant = 'info' remainingBadgeVariant = 'info'
...@@ -396,7 +412,7 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -396,7 +412,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
<TooltipTrigger <TooltipTrigger
render={ render={
<StatusBadge <StatusBadge
label={usedDisplay} label={sensitiveVisible ? usedDisplay : SENSITIVE_MASK}
variant='neutral' variant='neutral'
size='sm' size='sm'
copyable={false} copyable={false}
...@@ -406,7 +422,7 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -406,7 +422,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
} }
/> />
<TooltipContent> <TooltipContent>
<p>{usedLabel}</p> <p>{sensitiveVisible ? usedLabel : maskedUsedLabel}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
...@@ -424,11 +440,7 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -424,11 +440,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
} }
/> />
<TooltipContent> <TooltipContent>
<p> <p>{remainingTooltipLabel}</p>
{channel.type === 57
? t('Click to view Codex usage')
: remainingLabel}
</p>
{channel.type !== 57 && <p>{t('Click to update balance')}</p>} {channel.type !== 57 && <p>{t('Click to update balance')}</p>}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
...@@ -439,6 +451,8 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -439,6 +451,8 @@ function BalanceCell({ channel }: { channel: Channel }) {
onOpenChange={setCodexUsageOpen} onOpenChange={setCodexUsageOpen}
channelName={channel.name} channelName={channel.name}
channelId={channel.id} channelId={channel.id}
channelDisplayName={sensitiveVisible ? undefined : SENSITIVE_MASK}
channelDisplayId={sensitiveVisible ? undefined : SENSITIVE_MASK}
response={codexUsageResponse} response={codexUsageResponse}
onRefresh={async () => { onRefresh={async () => {
if (isUpdating) { if (isUpdating) {
...@@ -472,6 +486,7 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -472,6 +486,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
*/ */
export function useChannelsColumns(): ColumnDef<Channel>[] { export function useChannelsColumns(): ColumnDef<Channel>[] {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const { sensitiveVisible } = useChannels()
const locale = i18n.resolvedLanguage || i18n.language const locale = i18n.resolvedLanguage || i18n.language
return [ return [
// Checkbox column // Checkbox column
...@@ -513,7 +528,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] { ...@@ -513,7 +528,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
meta: { mobileHidden: true }, meta: { mobileHidden: true },
cell: ({ row }) => { cell: ({ row }) => {
const id = row.getValue('id') as number const id = row.getValue('id') as number
return <TableId value={id} /> return <TableId value={sensitiveVisible ? id : SENSITIVE_MASK} />
}, },
size: 80, size: 80,
}, },
...@@ -570,7 +585,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] { ...@@ -570,7 +585,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<div className='flex items-center gap-1.5'> <div className='flex items-center gap-1.5'>
<TruncatedText <TruncatedText
text={name} text={sensitiveVisible ? name : SENSITIVE_MASK}
className='font-medium' className='font-medium'
maxWidth='max-w-[180px]' maxWidth='max-w-[180px]'
/> />
...@@ -929,7 +944,12 @@ export function useChannelsColumns(): ColumnDef<Channel>[] { ...@@ -929,7 +944,12 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
return ( return (
<BadgeListCell <BadgeListCell
items={groupArray.map((g) => ( items={groupArray.map((g) => (
<GroupBadge key={g} group={g} size='sm' /> <GroupBadge
key={g}
group={g}
label={sensitiveVisible ? undefined : SENSITIVE_MASK}
size='sm'
/>
))} ))}
/> />
) )
......
...@@ -53,6 +53,8 @@ type ChannelsContextType = { ...@@ -53,6 +53,8 @@ type ChannelsContextType = {
setEnableTagMode: (enabled: boolean) => void setEnableTagMode: (enabled: boolean) => void
idSort: boolean idSort: boolean
setIdSort: (enabled: boolean) => void setIdSort: (enabled: boolean) => void
sensitiveVisible: boolean
setSensitiveVisible: (visible: boolean) => void
upstream: UpstreamUpdateState upstream: UpstreamUpdateState
} }
...@@ -78,6 +80,7 @@ export function ChannelsProvider({ children }: { children: React.ReactNode }) { ...@@ -78,6 +80,7 @@ export function ChannelsProvider({ children }: { children: React.ReactNode }) {
const [idSort, setIdSort] = useState(() => { const [idSort, setIdSort] = useState(() => {
return localStorage.getItem('channels-id-sort') === 'true' return localStorage.getItem('channels-id-sort') === 'true'
}) })
const [sensitiveVisible, setSensitiveVisible] = useState(true)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const refreshChannels = useCallback(async () => { const refreshChannels = useCallback(async () => {
...@@ -98,6 +101,8 @@ export function ChannelsProvider({ children }: { children: React.ReactNode }) { ...@@ -98,6 +101,8 @@ export function ChannelsProvider({ children }: { children: React.ReactNode }) {
setEnableTagMode, setEnableTagMode,
idSort, idSort,
setIdSort, setIdSort,
sensitiveVisible,
setSensitiveVisible,
upstream, upstream,
}} }}
> >
......
...@@ -19,17 +19,20 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,17 +19,20 @@ For commercial licensing, please contact support@quantumnous.com
import { useState, useMemo } from 'react' import { useState, useMemo } from 'react'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { getRouteApi } from '@tanstack/react-router' import { getRouteApi } from '@tanstack/react-router'
import { import type { OnChangeFn, SortingState, Row } from '@tanstack/react-table'
type OnChangeFn, import { Eye, EyeOff } from 'lucide-react'
type SortingState,
type Row,
} from '@tanstack/react-table'
import { useMediaQuery } from '@/hooks' import { useMediaQuery } from '@/hooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { getLobeIcon } from '@/lib/lobe-icon' import { getLobeIcon } from '@/lib/lobe-icon'
import { useTableUrlState } from '@/hooks/use-table-url-state' import { useTableUrlState } from '@/hooks/use-table-url-state'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import {
DISABLED_ROW_DESKTOP, DISABLED_ROW_DESKTOP,
DISABLED_ROW_MOBILE, DISABLED_ROW_MOBILE,
DataTablePage, DataTablePage,
...@@ -77,7 +80,12 @@ function isDisabledChannelRow(channel: Channel) { ...@@ -77,7 +80,12 @@ function isDisabledChannelRow(channel: Channel) {
export function ChannelsTable() { export function ChannelsTable() {
const { t } = useTranslation() const { t } = useTranslation()
const { enableTagMode, idSort } = useChannels() const {
enableTagMode,
idSort,
sensitiveVisible,
setSensitiveVisible,
} = useChannels()
const isMobile = useMediaQuery('(max-width: 640px)') const isMobile = useMediaQuery('(max-width: 640px)')
// Table state // Table state
...@@ -343,7 +351,10 @@ export function ChannelsTable() { ...@@ -343,7 +351,10 @@ export function ChannelsTable() {
const groupFilterOptions = [ const groupFilterOptions = [
{ label: t('All Groups'), value: 'all' }, { label: t('All Groups'), value: 'all' },
...groupOptions, ...groupOptions.map((option) => ({
...option,
label: sensitiveVisible ? option.label : '••••',
})),
] ]
return ( return (
...@@ -398,14 +409,36 @@ export function ChannelsTable() { ...@@ -398,14 +409,36 @@ export function ChannelsTable() {
singleSelect: true, singleSelect: true,
}, },
], ],
afterFilters: (
<Tooltip>
<TooltipTrigger
render={
<Button
variant='ghost'
size='icon'
onClick={() => setSensitiveVisible(!sensitiveVisible)}
aria-label={sensitiveVisible ? t('Hide') : t('Show')}
className='text-muted-foreground hover:text-foreground size-8'
/>
}
>
{sensitiveVisible ? <Eye /> : <EyeOff />}
</TooltipTrigger>
<TooltipContent>
{sensitiveVisible ? t('Hide') : t('Show')}
</TooltipContent>
</Tooltip>
),
}}
getRowClassName={(row, { isMobile }) => {
if (!isDisabledChannelRow(row.original)) {
return undefined
}
if (isMobile) {
return DISABLED_ROW_MOBILE
}
return DISABLED_ROW_DESKTOP
}} }}
getRowClassName={(row, { isMobile }) =>
isDisabledChannelRow(row.original)
? isMobile
? DISABLED_ROW_MOBILE
: DISABLED_ROW_DESKTOP
: undefined
}
bulkActions={<DataTableBulkActions table={table} />} bulkActions={<DataTableBulkActions table={table} />}
/> />
) )
......
...@@ -139,6 +139,8 @@ type CodexUsageDialogProps = { ...@@ -139,6 +139,8 @@ type CodexUsageDialogProps = {
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
channelName?: string channelName?: string
channelId?: number channelId?: number
channelDisplayName?: string
channelDisplayId?: string
response: CodexUsageDialogData | null response: CodexUsageDialogData | null
onRefresh?: () => void | Promise<void> onRefresh?: () => void | Promise<void>
isRefreshing?: boolean isRefreshing?: boolean
...@@ -885,6 +887,8 @@ export function CodexUsageDialog({ ...@@ -885,6 +887,8 @@ export function CodexUsageDialog({
onOpenChange, onOpenChange,
channelName, channelName,
channelId, channelId,
channelDisplayName,
channelDisplayId,
response, response,
onRefresh, onRefresh,
isRefreshing, isRefreshing,
...@@ -931,9 +935,14 @@ export function CodexUsageDialog({ ...@@ -931,9 +935,14 @@ export function CodexUsageDialog({
? String(resetCredits) ? String(resetCredits)
: '-' : '-'
const canResetCodexUsage = Number(resetCredits) > 0 const canResetCodexUsage = Number(resetCredits) > 0
const channelLabel = `${channelName || '-'}${ const channelLabelName = channelDisplayName ?? channelName ?? '-'
channelId ? ` (#${channelId})` : '' let channelLabelId = ''
}` if (channelDisplayId != null) {
channelLabelId = ` (#${channelDisplayId})`
} else if (channelId) {
channelLabelId = ` (#${channelId})`
}
const channelLabel = `${channelLabelName}${channelLabelId}`
const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(payload) const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(payload)
const errorMessage = const errorMessage =
......
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