All files / kernel-ui/src/components MessagePanel.tsx

100% Statements 26/26
93.75% Branches 15/16
100% Functions 7/7
100% Lines 25/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108                          3x 8x   1x   1x   1x     5x       3x 8x   4x   1x   1x   1x   1x             3x 12x 12x     12x 12x 14x 14x           12x     12x   12x     12x                                                     8x                          
import {
  Box,
  TextButton,
  TextButtonSize,
  Text as TextComponent,
  TextVariant,
} from '@metamask/design-system-react';
import { useEffect, useRef } from 'react';
 
import { usePanelContext } from '../context/PanelContext.tsx';
import type { OutputType } from '../context/PanelContext.tsx';
import { LoadingDots } from './shared/LoadingDots.tsx';
 
const getLogTypeIcon = (type: OutputType): string => {
  switch (type) {
    case 'received':
      return '←';
    case 'error':
      return '⚠';
    case 'success':
      return '✓';
    case 'sent':
    default:
      return '→';
  }
};
 
const getLogTypeStyles = (type: OutputType): string => {
  switch (type) {
    case 'sent':
      return 'text-text-muted';
    case 'received':
      return 'text-text-default mb-2';
    case 'error':
      return 'text-error-default mb-2';
    case 'success':
      return 'text-success-default mb-2';
    default:
      return 'text-text-default';
  }
};
 
/**
 * @returns A panel for sending messages to the kernel.
 */
export const MessagePanel: React.FC = () => {
  const { panelLogs, clearLogs, isLoading } = usePanelContext();
  const messageScrollRef = useRef<HTMLDivElement>(null);
 
  // Scroll to the bottom of the message output when the panel logs change
  useEffect(() => {
    const scrollToBottom = (): void => {
      Eif (messageScrollRef.current) {
        messageScrollRef.current.scrollTop =
          messageScrollRef.current.scrollHeight;
      }
    };
 
    // Scroll immediately
    scrollToBottom();
 
    // Also scroll after a small delay to ensure DOM is updated
    const timeoutId = setTimeout(scrollToBottom, 0);
 
    return () => clearTimeout(timeoutId);
  }, [panelLogs]);
 
  return (
    <Box className="h-full flex flex-col rounded-md overflow-hidden">
      <Box className="p-3 border-b border-muted flex justify-between items-center h-10">
        <TextComponent variant={TextVariant.BodySm}>
          Message History
        </TextComponent>
        {panelLogs.length > 0 && (
          <TextButton
            size={TextButtonSize.BodyXs}
            data-testid="clear-logs-button"
            onClick={clearLogs}
            className="min-w-0"
          >
            Clear
          </TextButton>
        )}
      </Box>
      <div
        className="flex-1 font-mono text-xs leading-relaxed p-3 rounded-none text-text-default overflow-y-auto"
        style={{
          boxShadow: 'inset 0 0 10px 0 var(--color-shadow-default)',
        }}
        ref={messageScrollRef}
        role="log"
      >
        <Box data-testid="message-output">
          {panelLogs.map((log, index) => (
            <Box key={index} className={`${getLogTypeStyles(log.type)}`}>
              <span className="inline-block text-center mr-1">
                {getLogTypeIcon(log.type)}
              </span>
              <span className="whitespace-pre-wrap">{log.message}</span>
            </Box>
          ))}
          {isLoading && <LoadingDots />}
        </Box>
      </div>
    </Box>
  );
};