fix(helm): don't block install with dry-run errors [r8s-454] (#976)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import clsx from 'clsx';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
export type BadgeType =
|
||||
| 'success'
|
||||
| 'danger'
|
||||
@@ -73,7 +75,8 @@ export function Badge({
|
||||
type = 'info',
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<Props>) {
|
||||
'data-cy': dataCy,
|
||||
}: PropsWithChildren<Props> & Partial<AutomationTestingProps>) {
|
||||
const baseClasses =
|
||||
'inline-flex w-fit items-center !text-xs font-medium rounded-full px-2 py-0.5';
|
||||
|
||||
@@ -81,6 +84,7 @@ export function Badge({
|
||||
<span
|
||||
className={clsx(baseClasses, typeClasses[type], className)}
|
||||
role="status"
|
||||
data-cy={dataCy}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
115
app/react/components/ExpandableMessageByLines.stories.tsx
Normal file
115
app/react/components/ExpandableMessageByLines.stories.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ExpandableMessageByLines } from './ExpandableMessageByLines';
|
||||
|
||||
export default {
|
||||
component: ExpandableMessageByLines,
|
||||
title: 'Components/ExpandableMessageByLines',
|
||||
argTypes: {
|
||||
maxLines: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: [2, 5, 10, 20, 50],
|
||||
},
|
||||
description: 'Maximum number of lines to show before truncating',
|
||||
},
|
||||
children: {
|
||||
control: 'text',
|
||||
description: 'The text content to display',
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
interface Args {
|
||||
children: string;
|
||||
maxLines?: 2 | 5 | 10 | 20 | 50;
|
||||
}
|
||||
|
||||
// Short text that won't be truncated
|
||||
export const ShortText: StoryObj<Args> = {
|
||||
args: {
|
||||
children: 'This is a short message that should not be truncated.',
|
||||
maxLines: 10,
|
||||
},
|
||||
};
|
||||
|
||||
// Long text that will be truncated
|
||||
export const LongText: StoryObj<Args> = {
|
||||
args: {
|
||||
children: `This is a very long message that should be truncated after the specified number of lines.
|
||||
It contains multiple lines of text to demonstrate the expandable functionality.
|
||||
The component will show a "Show more" button when the content exceeds the maxLines limit.
|
||||
When clicked, it will expand to show the full content and change to "Show less".
|
||||
This is useful for displaying long error messages, logs, or any text content that might be too long for the UI.`,
|
||||
maxLines: 5,
|
||||
},
|
||||
};
|
||||
|
||||
// Text with line breaks
|
||||
export const TextWithLineBreaks: StoryObj<Args> = {
|
||||
args: {
|
||||
children: `Line 1: This is the first line
|
||||
Line 2: This is the second line
|
||||
Line 3: This is the third line
|
||||
Line 4: This is the fourth line
|
||||
Line 5: This is the fifth line
|
||||
Line 6: This is the sixth line
|
||||
Line 7: This is the seventh line
|
||||
Line 8: This is the eighth line
|
||||
Line 9: This is the ninth line
|
||||
Line 10: This is the tenth line`,
|
||||
maxLines: 5,
|
||||
},
|
||||
};
|
||||
|
||||
// Very short maxLines
|
||||
export const VeryShortMaxLines: StoryObj<Args> = {
|
||||
args: {
|
||||
children: `This text will be truncated after just 2 lines.
|
||||
This is the second line.
|
||||
This is the third line that should be hidden initially.
|
||||
This is the fourth line that should also be hidden.`,
|
||||
maxLines: 2,
|
||||
},
|
||||
};
|
||||
|
||||
// Error message example
|
||||
export const ErrorMessage: StoryObj<Args> = {
|
||||
args: {
|
||||
children: `Error: Failed to connect to the Docker daemon at unix:///var/run/docker.sock.
|
||||
Is the docker daemon running?
|
||||
|
||||
This error typically occurs when:
|
||||
1. Docker daemon is not running
|
||||
2. User doesn't have permission to access the Docker socket
|
||||
3. Docker socket path is incorrect
|
||||
4. Docker service has crashed
|
||||
|
||||
To resolve this issue:
|
||||
1. Start the Docker daemon: sudo systemctl start docker
|
||||
2. Add user to docker group: sudo usermod -aG docker $USER
|
||||
3. Verify Docker is running: docker ps
|
||||
4. Check Docker socket permissions: ls -la /var/run/docker.sock`,
|
||||
maxLines: 5,
|
||||
},
|
||||
};
|
||||
|
||||
// Log output example
|
||||
export const LogOutput: StoryObj<Args> = {
|
||||
args: {
|
||||
children: `2024-01-15T10:30:45.123Z INFO [ContainerService] Starting container nginx:latest
|
||||
2024-01-15T10:30:45.234Z DEBUG [ContainerService] Container ID: abc123def456
|
||||
2024-01-15T10:30:45.345Z INFO [ContainerService] Container started successfully
|
||||
2024-01-15T10:30:45.456Z DEBUG [NetworkService] Creating network bridge
|
||||
2024-01-15T10:30:45.567Z INFO [NetworkService] Network created: portainer_network
|
||||
2024-01-15T10:30:45.678Z DEBUG [VolumeService] Mounting volume /data
|
||||
2024-01-15T10:30:45.789Z INFO [VolumeService] Volume mounted successfully
|
||||
2024-01-15T10:30:45.890Z DEBUG [ContainerService] Setting up port mapping 80:80
|
||||
2024-01-15T10:30:45.901Z INFO [ContainerService] Port mapping configured
|
||||
2024-01-15T10:30:45.912Z DEBUG [ContainerService] Setting environment variables
|
||||
2024-01-15T10:30:45.923Z INFO [ContainerService] Environment variables set
|
||||
2024-01-15T10:30:45.934Z DEBUG [ContainerService] Starting container process
|
||||
2024-01-15T10:30:45.945Z INFO [ContainerService] Container process started`,
|
||||
maxLines: 10,
|
||||
},
|
||||
};
|
||||
137
app/react/components/ExpandableMessageByLines.test.tsx
Normal file
137
app/react/components/ExpandableMessageByLines.test.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { ExpandableMessageByLines } from '@@/ExpandableMessageByLines';
|
||||
|
||||
describe('ExpandableMessageByLines', () => {
|
||||
// Mock scrollHeight and clientHeight for testing truncation
|
||||
const mockScrollHeight = vi.fn();
|
||||
const mockClientHeight = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the properties on HTMLDivElement prototype
|
||||
Object.defineProperty(HTMLDivElement.prototype, 'scrollHeight', {
|
||||
get: mockScrollHeight,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(HTMLDivElement.prototype, 'clientHeight', {
|
||||
get: mockClientHeight,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Basic Rendering', () => {
|
||||
it('should render text content', () => {
|
||||
const text = 'This is test content';
|
||||
// Mock non-truncated content (scrollHeight === clientHeight)
|
||||
mockScrollHeight.mockReturnValue(100);
|
||||
mockClientHeight.mockReturnValue(100);
|
||||
|
||||
render(<ExpandableMessageByLines>{text}</ExpandableMessageByLines>);
|
||||
|
||||
expect(screen.getByText(text)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show expand button only when text is truncated', () => {
|
||||
const text = 'This is test content that should be truncated';
|
||||
// Mock truncated content (scrollHeight > clientHeight)
|
||||
mockScrollHeight.mockReturnValue(300);
|
||||
mockClientHeight.mockReturnValue(200);
|
||||
|
||||
render(<ExpandableMessageByLines>{text}</ExpandableMessageByLines>);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Show more' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('expandable-message-lines-button')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide expand button when text is not truncated', () => {
|
||||
const text = 'Short text';
|
||||
// Mock non-truncated content (scrollHeight === clientHeight)
|
||||
mockScrollHeight.mockReturnValue(50);
|
||||
mockClientHeight.mockReturnValue(50);
|
||||
|
||||
render(<ExpandableMessageByLines>{text}</ExpandableMessageByLines>);
|
||||
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('expandable-message-lines-button')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Expand/Collapse Functionality', () => {
|
||||
it('should toggle between Show more and Show less when button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const text =
|
||||
'This is a long text that should be truncated and show the expand button';
|
||||
|
||||
// Mock truncated content (scrollHeight > clientHeight)
|
||||
mockScrollHeight.mockReturnValue(400);
|
||||
mockClientHeight.mockReturnValue(200);
|
||||
|
||||
render(<ExpandableMessageByLines>{text}</ExpandableMessageByLines>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
// Initially should show "Show more"
|
||||
expect(screen.getByText('Show more')).toBeInTheDocument();
|
||||
|
||||
// Click to expand
|
||||
await user.click(button);
|
||||
expect(screen.getByText('Show less')).toBeInTheDocument();
|
||||
|
||||
// Click to collapse
|
||||
await user.click(button);
|
||||
expect(screen.getByText('Show more')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text Content Handling', () => {
|
||||
it('should not show button for single space strings when not truncated', () => {
|
||||
// Mock non-truncated content (scrollHeight === clientHeight)
|
||||
mockScrollHeight.mockReturnValue(20);
|
||||
mockClientHeight.mockReturnValue(20);
|
||||
|
||||
render(<ExpandableMessageByLines> </ExpandableMessageByLines>);
|
||||
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show button for single space strings when truncated', () => {
|
||||
// Mock truncated content (scrollHeight > clientHeight)
|
||||
mockScrollHeight.mockReturnValue(100);
|
||||
mockClientHeight.mockReturnValue(50);
|
||||
|
||||
render(<ExpandableMessageByLines> </ExpandableMessageByLines>);
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle different maxLines values', () => {
|
||||
const longText =
|
||||
'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10\nLine 11\nLine 12';
|
||||
// Mock truncated content for 5 lines (scrollHeight > clientHeight)
|
||||
mockScrollHeight.mockReturnValue(240);
|
||||
mockClientHeight.mockReturnValue(100);
|
||||
|
||||
render(
|
||||
<ExpandableMessageByLines maxLines={5}>
|
||||
{longText}
|
||||
</ExpandableMessageByLines>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
expect(screen.getByText('Show more')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
76
app/react/components/ExpandableMessageByLines.tsx
Normal file
76
app/react/components/ExpandableMessageByLines.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useRef, useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
// use enum so that the tailwind classes aren't interpolated
|
||||
type MaxLines = 2 | 5 | 10 | 20 | 50;
|
||||
const lineClampClasses: Record<MaxLines, string> = {
|
||||
2: 'line-clamp-[2]',
|
||||
5: 'line-clamp-[5]',
|
||||
10: 'line-clamp-[10]',
|
||||
20: 'line-clamp-[20]',
|
||||
50: 'line-clamp-[50]',
|
||||
};
|
||||
|
||||
interface LineBasedProps {
|
||||
children: string;
|
||||
maxLines?: MaxLines;
|
||||
}
|
||||
|
||||
export function ExpandableMessageByLines({
|
||||
children,
|
||||
maxLines = 10,
|
||||
}: LineBasedProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const checkTruncation = useCallback(() => {
|
||||
const el = contentRef.current;
|
||||
if (el) {
|
||||
setIsTruncated(el.scrollHeight > el.clientHeight);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkTruncation();
|
||||
|
||||
// Use requestAnimationFrame for better performance
|
||||
let rafId: number;
|
||||
function handleResize() {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(checkTruncation);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [children, maxLines, checkTruncation, isExpanded]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start">
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`whitespace-pre-wrap break-words overflow-hidden ${
|
||||
isExpanded ? '' : lineClampClasses[maxLines]
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{(isTruncated || isExpanded) && (
|
||||
<Button
|
||||
color="link"
|
||||
size="xsmall"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="!ml-0 !p-0 mt-1"
|
||||
data-cy="expandable-message-lines-button"
|
||||
>
|
||||
{isExpanded ? 'Show less' : 'Show more'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user