Merge pull request #699 from AutumnNazi/fix/redis-key-detail-actions-layout

This commit is contained in:
Syngnat
2026-07-24 13:26:14 +08:00
2 changed files with 53 additions and 13 deletions

View File

@@ -321,6 +321,44 @@ describe('RedisViewer tree interactions', () => {
renderer!.unmount();
});
it('renders key detail actions on a separate row below the metadata', async () => {
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<RedisViewer connectionId="redis-1" redisDB={0} />);
});
await flushEffects();
const leafNode = findFirstLeafNode(antdState.treeProps.treeData);
await act(async () => {
antdState.treeProps.onSelect?.([leafNode.key]);
});
await flushEffects();
const header = renderer!.root.findByProps({ className: 'redis-key-detail-header' });
const summary = renderer!.root.findByProps({ className: 'redis-key-detail-summary' });
const identity = renderer!.root.findByProps({ className: 'redis-key-detail-identity' });
const metadata = renderer!.root.findByProps({ className: 'redis-key-detail-metadata' });
const actions = renderer!.root.findByProps({ className: 'redis-key-detail-actions' });
expect(header.props.style).toMatchObject({ flexDirection: 'column' });
expect(summary.props.style).toMatchObject({ minWidth: 0, width: '100%' });
expect(identity.parent).toBe(summary);
expect(metadata.parent).toBe(summary);
expect(actions.parent).toBe(summary);
expect(summary.children.indexOf(identity)).toBeLessThan(summary.children.indexOf(metadata));
expect(summary.children.indexOf(metadata)).toBeLessThan(summary.children.indexOf(actions));
expect(actions.props.style).toMatchObject({
alignSelf: 'flex-start',
flexWrap: 'wrap',
maxWidth: '100%',
});
expect(findButtonByText(renderer!, 'Set TTL')).toBeTruthy();
expect(findButtonByText(renderer!, 'Refresh')).toBeTruthy();
expect(findButtonByText(renderer!, 'Delete Key')).toBeTruthy();
renderer!.unmount();
});
it('loads every key page when the load-all action is clicked', async () => {
redisBackend.RedisScanKeys.mockReset();
redisBackend.RedisScanKeys

View File

@@ -2017,12 +2017,12 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
return (
<div className={isV2Ui ? 'gn-v2-redis-value-layout' : undefined} style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 12 }}>
<div className={isV2Ui ? 'gn-v2-redis-value-header' : undefined} style={{ ...workbenchCardStyle, padding: 18, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexShrink: 0 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<div className={`redis-key-detail-header${isV2Ui ? ' gn-v2-redis-value-header' : ''}`} style={{ ...workbenchCardStyle, padding: 18, display: 'flex', flexDirection: 'column', gap: 16, flexShrink: 0 }}>
<div className="redis-key-detail-summary" style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0, width: '100%' }}>
<span style={{ fontSize: 12, textTransform: 'uppercase', letterSpacing: '.08em', color: workbenchTheme.textMuted, fontWeight: 600 }}>
{tr('redis_viewer.title.active_key')}
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', minWidth: 0 }}>
<div className="redis-key-detail-identity" style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
<Tooltip title={selectedKey}>
<strong style={{ maxWidth: 340, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 26, color: workbenchTheme.textPrimary }}>
{selectedKey}
@@ -2043,20 +2043,22 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
}}
/>
</Tooltip>
</div>
<div className="redis-key-detail-metadata" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', minWidth: 0 }}>
<Tag color={getTypeColor(keyValue.type)} style={pillTagStyle}>{keyValue.type}</Tag>
<Tag icon={<ClockCircleOutlined />} style={mutedPillTagStyle}>{formatTTL(keyValue.ttl)}</Tag>
{keyValue.length > 0 && <Tag style={mutedPillTagStyle}>{tr('redis_viewer.label.length', { count: keyValue.length })}</Tag>}
</div>
</div>
<div className={isV2Ui ? 'gn-v2-redis-value-actions' : undefined} style={{ ...workbenchSubCardStyle, padding: 4, display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
<Button size="small" style={actionButtonStyle} onClick={() => {
ttlForm.setFieldsValue({ ttl: keyValue.ttl > 0 ? keyValue.ttl : -1 });
setTtlModalOpen(true);
}}>{tr('redis_viewer.action.set_ttl')}</Button>
<Button size="small" style={actionButtonStyle} onClick={() => loadKeyValue(selectedKey)} icon={<ReloadOutlined />}>{tr('redis_viewer.action.refresh')}</Button>
<Popconfirm title={tr('redis_viewer.confirm.delete_key', { key: selectedKey })} onConfirm={handleDeleteCurrentKey}>
<Button size="small" style={dangerActionButtonStyle} icon={<DeleteOutlined />}>{tr('redis_viewer.action.delete_key')}</Button>
</Popconfirm>
<div className="redis-key-detail-actions" style={{ display: 'flex', gap: 4, alignItems: 'center', alignSelf: 'flex-start', flexWrap: 'wrap', maxWidth: '100%' }}>
<Button size="small" style={actionButtonStyle} onClick={() => {
ttlForm.setFieldsValue({ ttl: keyValue.ttl > 0 ? keyValue.ttl : -1 });
setTtlModalOpen(true);
}}>{tr('redis_viewer.action.set_ttl')}</Button>
<Button size="small" style={actionButtonStyle} onClick={() => loadKeyValue(selectedKey)} icon={<ReloadOutlined />}>{tr('redis_viewer.action.refresh')}</Button>
<Popconfirm title={tr('redis_viewer.confirm.delete_key', { key: selectedKey })} onConfirm={handleDeleteCurrentKey}>
<Button size="small" style={dangerActionButtonStyle} icon={<DeleteOutlined />}>{tr('redis_viewer.action.delete_key')}</Button>
</Popconfirm>
</div>
</div>
</div>
<div className={isV2Ui ? 'gn-v2-redis-view-mode' : undefined} style={{ ...workbenchSubCardStyle, padding: 6, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexShrink: 0 }}>