Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { Box, Typography } from '@material-ui/core';
import React, { useState } from 'react';
import { Box, Typography, Collapse } from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { useStyles } from './styles';

export interface TabItemData {
Expand All @@ -15,31 +16,51 @@ export interface TabItemData {
status?: 'success' | 'warning' | 'error' | 'info' | 'default';
/** Whether the tab is disabled */
disabled?: boolean;
/** Optional children tabs for creating collapsible groups */
children?: TabItemData[];
/** Whether this is a group item (collapsible) */
isGroup?: boolean;
}

interface VerticalTabItemProps {
tab: TabItemData;
isActive: boolean;
onClick: () => void;
onClick: (tabId: string) => void;
activeTabId?: string;
level?: number;
}

export const VerticalTabItem: React.FC<VerticalTabItemProps> = ({
tab,
isActive,
onClick,
activeTabId,
level = 0,
}) => {
const classes = useStyles();
const [expanded, setExpanded] = useState(true);

const isGroup = tab.isGroup || (tab.children && tab.children.length > 0);
// const hasActiveChild = tab.children?.some(
// child => child.id === activeTabId || child.children?.some(c => c.id === activeTabId)
// );

const handleClick = () => {
if (!tab.disabled) {
onClick();
if (isGroup) {
setExpanded(!expanded);
} else if (!tab.disabled) {
onClick(tab.id);
}
};

const handleKeyDown = (event: React.KeyboardEvent) => {
if ((event.key === 'Enter' || event.key === ' ') && !tab.disabled) {
event.preventDefault();
onClick();
if (isGroup) {
setExpanded(!expanded);
} else {
onClick(tab.id);
}
}
};

Expand All @@ -58,36 +79,76 @@ export const VerticalTabItem: React.FC<VerticalTabItemProps> = ({
}
};

// Calculate padding: base padding + level offset + icon space if nested without icon
const iconSpace = 16; // Icon width (20px) + gap (12px)
const basePadding = 16;
const levelOffset = level * 16;

// If nested (level > 0) and no icon, add icon space to align with parent text
const extraPadding = level > 0 && !tab.icon ? iconSpace : 0;
const paddingLeft = basePadding + levelOffset + extraPadding;

return (
<Box
role="tab"
aria-selected={isActive}
aria-disabled={tab.disabled}
tabIndex={tab.disabled ? -1 : 0}
className={[
classes.tabItem,
isActive && classes.tabItemActive,
tab.disabled && classes.tabItemDisabled,
]
.filter(Boolean)
.join(' ')}
onClick={handleClick}
onKeyDown={handleKeyDown}
>
{tab.icon && <Box className={classes.tabIcon}>{tab.icon}</Box>}
<Box className={classes.tabContent}>
<Typography className={classes.tabLabel} component="span">
{tab.label}
</Typography>
</Box>
<Box className={classes.tabIndicators}>
{tab.status && (
<span className={`${classes.statusDot} ${getStatusClass()}`} />
)}
{tab.count !== undefined && (
<span className={classes.countBadge}>{tab.count}</span>
)}
<>
<Box
role="tab"
aria-selected={isActive}
aria-disabled={tab.disabled}
tabIndex={tab.disabled ? -1 : 0}
className={[
classes.tabItem,
isActive && classes.tabItemActive,
tab.disabled && classes.tabItemDisabled,
isGroup && classes.tabItemGroup,
level > 0 && classes.tabItemNested,
]
.filter(Boolean)
.join(' ')}
onClick={handleClick}
onKeyDown={handleKeyDown}
style={{ paddingLeft: `${paddingLeft}px` }}
>
{tab.icon && <Box className={classes.tabIcon}>{tab.icon}</Box>}
<Box className={classes.tabContent}>
<Typography className={classes.tabLabel} component="span">
{tab.label}
</Typography>
</Box>
<Box className={classes.tabIndicators}>
{tab.status && (
<span className={`${classes.statusDot} ${getStatusClass()}`} />
)}
{tab.count !== undefined && (
<span className={classes.countBadge}>{tab.count}</span>
)}
{isGroup && (
<Box
className={[
classes.expandIcon,
expanded && classes.expandIconExpanded,
]
.filter(Boolean)
.join(' ')}
>
<ExpandMoreIcon fontSize="small" />
</Box>
)}
</Box>
</Box>
</Box>
{isGroup && tab.children && (
<Collapse in={expanded}>
{tab.children.map(child => (
<VerticalTabItem
key={child.id}
tab={child}
isActive={child.id === activeTabId}
onClick={onClick}
activeTabId={activeTabId}
level={level + 1}
/>
))}
</Collapse>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ export const VerticalTabNav: React.FC<VerticalTabNavProps> = ({
key={tab.id}
tab={tab}
isActive={activeTabId === tab.id}
onClick={() => onChange(tab.id)}
onClick={onChange}
activeTabId={activeTabId}
/>
))}
</Box>
Expand Down
26 changes: 26 additions & 0 deletions packages/design-system/src/components/VerticalTabNav/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,30 @@ export const useStyles = makeStyles((theme: Theme) => ({
overflowY: 'auto',
padding: theme.spacing(2),
},
tabItemGroup: {
fontWeight: 600,
'&:hover': {
backgroundColor:
theme.palette.type === 'dark' ? 'rgba(255, 255, 255, 0.05)' : '#fafbfc',
},
},
tabItemNested: {
fontSize: 13,
'& $tabLabel': {
fontSize: 13,
},
},
expandIcon: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: theme.palette.text.secondary,
transition: 'transform 0.2s ease-in-out',
'& svg': {
fontSize: 18,
},
},
expandIconExpanded: {
transform: 'rotate(180deg)',
},
}));
84 changes: 83 additions & 1 deletion plugins/openchoreo-common/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,79 @@ export function getRepositoryUrl(
return `${url}${separator}tree/${branch || 'main'}/${path}`;
}

/**
* Common acronyms and abbreviations that should be displayed in uppercase
*/
const KNOWN_ACRONYMS = new Set([
'cpu',
'ai',
'gpu',
'ram',
'api',
'url',
'uri',
'http',
'https',
'ftp',
'ssh',
'ssl',
'tls',
'tcp',
'udp',
'ip',
'dns',
'html',
'css',
'json',
'xml',
'yaml',
'sql',
'db',
'id',
'uuid',
'jwt',
'oauth',
'smtp',
'imap',
'pop',
'csv',
'pdf',
'ui',
'ux',
'cli',
'sdk',
'jvm',
'npm',
'ttl',
'vpc',
'aws',
'gcp',
'iam',
'cidr',
'arn',
'rpc',
'grpc',
'rest',
'cors',
'csrf',
'xss',
'dos',
'ddos',
'vm',
'os',
'io',
'ide',
'gui',
'ascii',
'utf',
'iso',
'mime',
'sha',
'md',
'aes',
'rsa',
]);

/**
* Converts camelCase or snake_case strings to Title Case for display labels
*
Expand All @@ -96,8 +169,10 @@ export function getRepositoryUrl(
* sanitizeLabel('imagePullPolicy') // "Image Pull Policy"
* sanitizeLabel('image_pull_policy') // "Image Pull Policy"
* sanitizeLabel('CPU') // "CPU" (preserves acronyms)
* sanitizeLabel('httpPort') // "Http Port"
* sanitizeLabel('cpu') // "CPU" (converts known acronym)
* sanitizeLabel('httpPort') // "HTTP Port"
* sanitizeLabel('maxRetries3') // "Max Retries 3"
* sanitizeLabel('apiUrl') // "API URL"
* ```
*/
export function sanitizeLabel(key: string): string {
Expand All @@ -119,11 +194,18 @@ export function sanitizeLabel(key: string): string {
const titleCased = words.map(word => {
if (!word) return '';

const lowerWord = word.toLowerCase();

// Keep all-caps acronyms as-is (e.g., CPU, HTTP, URL)
if (word.length > 1 && word === word.toUpperCase()) {
return word;
}

// Convert known acronyms to uppercase
if (KNOWN_ACRONYMS.has(lowerWord)) {
return word.toUpperCase();
}

// Capitalize first letter, lowercase the rest
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ import {
Box,
Typography,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles(theme => ({
deleteButton: {
color: theme.palette.error.dark,
borderColor: theme.palette.error.dark,
'&:hover': {
borderColor: theme.palette.error.dark,
backgroundColor: `${theme.palette.error.dark}0A`, // 4% opacity
},
},
}));

interface DeleteConfirmationDialogProps {
open: boolean;
Expand All @@ -28,6 +40,7 @@ export const DeleteConfirmationDialog: FC<DeleteConfirmationDialogProps> = ({
initialTraitFormDataMap,
deleting,
}) => {
const classes = useStyles();
const hasInitialComponentTypeOverrides =
initialComponentTypeFormData &&
Object.keys(initialComponentTypeFormData).length > 0;
Expand Down Expand Up @@ -88,19 +101,21 @@ export const DeleteConfirmationDialog: FC<DeleteConfirmationDialogProps> = ({

return (
<Dialog open={open} onClose={onCancel} maxWidth="sm" fullWidth>
<DialogTitle>Delete Overrides?</DialogTitle>
<DialogTitle disableTypography>
<Typography variant="h4">Delete Overrides?</Typography>
</DialogTitle>

<DialogContent dividers>{getDeleteMessage()}</DialogContent>
<DialogContent>{getDeleteMessage()}</DialogContent>

<DialogActions>
<Button onClick={onCancel} disabled={deleting}>
<Button onClick={onCancel} disabled={deleting} variant="contained">
Cancel
</Button>
<Button
onClick={onConfirm}
color="secondary"
variant="contained"
variant="outlined"
disabled={deleting}
className={classes.deleteButton}
>
{deleting ? 'Deleting...' : 'Confirm Delete'}
</Button>
Expand Down
Loading