Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/k8s-ui/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './api-resources'
export * from './skeleton-yaml'
export * from './k8s-errors'
export * from './parse-go-time'
export * from './user-initials'
56 changes: 56 additions & 0 deletions packages/k8s-ui/src/utils/user-initials.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, it, expect } from 'vitest'
import { computeUserInitials } from './user-initials'

// Pinning the SKY-825 bug 41 contract: the previous implementation
// only looked at separator-split segments, so any username without a
// '.', '_', or '-' (e.g. "mkohli", "alice") produced empty initials
// and the UserMenu fell back to a generic silhouette icon. The new
// helper guarantees a non-empty, uppercase 1-2-letter label whenever
// there's a usable username.

describe('computeUserInitials', () => {
it('uses segment initials when separators are present', () => {
expect(computeUserInitials('mary.kohli')).toBe('MK')
expect(computeUserInitials('mary_kohli')).toBe('MK')
expect(computeUserInitials('mary-kohli')).toBe('MK')
})

it('caps segment initials at 2 even with many separators', () => {
expect(computeUserInitials('a.b.c.d')).toBe('AB')
})

it('falls back to leading letters when no separators (the SKY-825 bug 41 fix)', () => {
expect(computeUserInitials('mkohli')).toBe('MK')
expect(computeUserInitials('alice')).toBe('AL')
})

it('returns a single letter for single-character usernames', () => {
expect(computeUserInitials('a')).toBe('A')
})

it('strips the @-domain before computing', () => {
expect(computeUserInitials('mary.kohli@example.com')).toBe('MK')
expect(computeUserInitials('mkohli@example.com')).toBe('MK')
})

it('uppercases the result', () => {
expect(computeUserInitials('alice')).toBe('AL')
expect(computeUserInitials('ALICE')).toBe('AL')
expect(computeUserInitials('aLiCe')).toBe('AL')
})

it('returns empty string for null/undefined/empty inputs (caller falls back to silhouette)', () => {
expect(computeUserInitials(null)).toBe('')
expect(computeUserInitials(undefined)).toBe('')
expect(computeUserInitials('')).toBe('')
})

it('handles consecutive separators without producing empty segments', () => {
expect(computeUserInitials('mary..kohli')).toBe('MK')
expect(computeUserInitials('mary__kohli')).toBe('MK')
})

it('handles email-only usernames with @ as the first character', () => {
expect(computeUserInitials('@example.com')).toBe('')
})
})
38 changes: 38 additions & 0 deletions packages/k8s-ui/src/utils/user-initials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Computes a 1- or 2-character avatar label for a username.
*
* Rules:
* - operate on the local-part (before any '@') only — domains
* never carry useful identity for an in-app avatar.
* - if the local-part contains separators (`.`, `_`, `-`), use the
* first letter of each segment (max 2). e.g. "mary.kohli" → "MK".
* - otherwise, use the first 1-2 letters of the whole local-part.
* e.g. "mkohli" → "MK".
* - always uppercase.
* - returns '' for inputs with no usable letters so the caller can
* decide on a graceful fallback (silhouette icon, '?').
*
* Without the fallback to the leading letters, usernames like
* "mkohli" produced no segment initials, radar's UserMenu showed a
* generic silhouette, and the user perceived duplicated identity
* affordance because another circle in the header (radar-hub-web's
* own avatar) showed correctly-computed initials. (SKY-825 bug 41)
*/
export function computeUserInitials(username: string | null | undefined): string {
if (!username) return ''
const localPart = username.split('@')[0]
if (!localPart) return ''
const segments = localPart.split(/[._-]/).filter(Boolean)
// 2+ segments → use the first letter of each (e.g. "mary.kohli" → "MK").
// Otherwise fall back to the leading letters of the whole local-part
// so single-segment usernames like "mkohli" still produce "MK"
// instead of just "M". (SKY-825 bug 41)
if (segments.length >= 2) {
return segments
.slice(0, 2)
.map(s => s[0]?.toUpperCase() ?? '')
.filter(Boolean)
.join('')
}
return localPart.slice(0, 2).toUpperCase()
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
}
8 changes: 2 additions & 6 deletions web/src/components/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useRef, useEffect, useCallback } from 'react'
import { User, LogOut } from 'lucide-react'
import { useAuthMe } from '../api/client'
import { useQueryClient } from '@tanstack/react-query'
import { computeUserInitials } from '@skyhook-io/k8s-ui/utils/user-initials'

export function UserMenu() {
const { data: authMe } = useAuthMe()
Expand Down Expand Up @@ -40,12 +41,7 @@ export function UserMenu() {
return null
}

const initials = authMe.username
.split('@')[0]
.split(/[._-]/)
.slice(0, 2)
.map(s => s[0]?.toUpperCase() || '')
.join('')
const initials = computeUserInitials(authMe.username)

return (
<div ref={menuRef} className="relative">
Expand Down
14 changes: 12 additions & 2 deletions web/src/components/helm/ChartBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,12 @@ function LocalChartCard({ chart, onSelect }: LocalChartCardProps) {
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-theme-text-primary truncate">{chart.name}</h4>
{/* Long chart names truncate with no way to reveal the
full string. Wrap in a Tooltip so the user can hover
for the unabbreviated name. (SKY-825 bug 43) */}
<Tooltip content={chart.name} wrapperClassName="min-w-0 flex-1">
<h4 className="text-sm font-medium text-theme-text-primary truncate">{chart.name}</h4>
</Tooltip>
{chart.deprecated && (
<span className={clsx('px-1 py-0.5 text-[10px] rounded', SEVERITY_BADGE.warning)}>
deprecated
Expand Down Expand Up @@ -489,7 +494,12 @@ function ArtifactHubChartCard({ chart, onSelect }: ArtifactHubChartCardProps) {

{/* Name and org */}
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium text-theme-text-primary truncate">{chart.name}</h4>
{/* Tooltip on the truncated name so users can read the
full chart name without resizing the panel.
(SKY-825 bug 43) */}
<Tooltip content={chart.name} wrapperClassName="block">
Comment thread
cursor[bot] marked this conversation as resolved.
<h4 className="text-sm font-medium text-theme-text-primary truncate">{chart.name}</h4>
</Tooltip>
<div className="flex items-center gap-2 mt-0.5 text-xs text-theme-text-tertiary">
<span className="flex items-center gap-1">
<Building2 className="w-3 h-3" />
Expand Down
Loading