diff --git a/packages/code-studio/src/styleguide/ErrorViews.tsx b/packages/code-studio/src/styleguide/ErrorViews.tsx
new file mode 100644
index 000000000..08ec59fc0
--- /dev/null
+++ b/packages/code-studio/src/styleguide/ErrorViews.tsx
@@ -0,0 +1,59 @@
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint no-alert: "off" */
+import React, { CSSProperties } from 'react';
+import { ErrorView } from '@deephaven/components';
+import { sampleSectionIdAndClasses } from './utils';
+
+function ErrorViews(): React.ReactElement {
+ const columnStyle: CSSProperties = {
+ maxHeight: 500,
+ display: 'flex',
+ flexDirection: 'column',
+ maxWidth: 400,
+ };
+
+ const shortErrorMessage = 'This is a short error message';
+ const midErrorMessage = 'Mid length error message\n'.repeat(10);
+ const longErrorMessage = 'Really long error message\n'.repeat(100);
+
+ const midErrorType = 'MidError';
+ const longErrorType = 'SuperLongErrorMessageType';
+
+ return (
+
+
+ Error Views
+
+
Expandable
+
+
Always expanded
+
+
+ );
+}
+
+export default ErrorViews;
diff --git a/packages/code-studio/src/styleguide/StyleGuide.tsx b/packages/code-studio/src/styleguide/StyleGuide.tsx
index 72c368977..d7c33e1ae 100644
--- a/packages/code-studio/src/styleguide/StyleGuide.tsx
+++ b/packages/code-studio/src/styleguide/StyleGuide.tsx
@@ -37,6 +37,7 @@ import { RandomAreaPlotAnimation } from './RandomAreaPlotAnimation';
import SpectrumComparison from './SpectrumComparison';
import Pickers from './Pickers';
import ListViews from './ListViews';
+import ErrorViews from './ErrorViews';
const stickyProps = {
position: 'sticky',
@@ -133,6 +134,7 @@ function StyleGuide(): React.ReactElement {
+
);
diff --git a/packages/components/src/ErrorView.scss b/packages/components/src/ErrorView.scss
new file mode 100644
index 000000000..c0c290d69
--- /dev/null
+++ b/packages/components/src/ErrorView.scss
@@ -0,0 +1,83 @@
+@import '../scss/custom.scss';
+
+.error-view {
+ position: relative;
+ color: $danger;
+ border-radius: $border-radius;
+ background-color: negative-opacity($exception-transparency);
+ display: flex;
+ flex-direction: column;
+ flex-grow: 0;
+ font-family: $font-family-monospace;
+ transition: all $transition ease-in-out;
+ max-height: 150px;
+
+ &.expanded {
+ max-height: 100%;
+ }
+
+ .error-view-header {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ text-wrap: nowrap;
+ width: 100%;
+
+ .error-view-header-text {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding-left: $spacer;
+ padding-right: $spacer;
+ font-weight: bold;
+ flex-shrink: 1;
+ overflow: hidden;
+ white-space: nowrap;
+
+ span {
+ flex-shrink: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ padding-left: $spacer-1;
+ }
+ }
+
+ .error-view-buttons {
+ display: flex;
+ flex-direction: row;
+ gap: 1px;
+ overflow: hidden;
+ border-radius: 0 $border-radius 0 0;
+ flex-shrink: 0;
+
+ .btn-danger {
+ border-radius: 0;
+ color: var(--dh-color-contrast-dark);
+ opacity: 0.8;
+ padding: $spacer-1;
+ &:active {
+ color: var(--dh-color-contrast-dark);
+ }
+ }
+
+ .error-view-copy-button {
+ min-width: 3rem;
+ }
+ }
+ }
+
+ .error-view-text {
+ width: 100%;
+ padding: $spacer;
+ margin-bottom: 0;
+ color: $danger;
+ background-color: transparent;
+ border: 0;
+ resize: none;
+ outline: none;
+ white-space: pre;
+ flex-grow: 1;
+ overflow: auto;
+ }
+}
diff --git a/packages/components/src/ErrorView.tsx b/packages/components/src/ErrorView.tsx
new file mode 100644
index 000000000..d537de166
--- /dev/null
+++ b/packages/components/src/ErrorView.tsx
@@ -0,0 +1,92 @@
+import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
+import classNames from 'classnames';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { vsDiffAdded, vsDiffRemoved, vsWarning } from '@deephaven/icons';
+import {
+ useDebouncedCallback,
+ useResizeObserver,
+} from '@deephaven/react-hooks';
+import CopyButton from './CopyButton';
+import Button from './Button';
+import './ErrorView.scss';
+
+export type ErrorViewerProps = {
+ /** The message to display in the error view */
+ message: string;
+
+ /** Set to true if you want the error view to display expanded. Will not show the Show More/Less buttons if true. Defaults to false. */
+ isExpanded?: boolean;
+
+ /** The type of error message to display in the header. Defaults to Error. */
+ type?: string;
+};
+
+/**
+ * Component that displays an error message in a textarea so user can scroll and a copy button.
+ */
+function ErrorView({
+ message,
+ isExpanded: isExpandedProp = false,
+ type = 'Error',
+}: ErrorViewerProps): JSX.Element {
+ const [isExpandable, setIsExpandable] = useState(false);
+ const [isExpanded, setIsExpanded] = useState(false);
+ const viewRef = useRef(null);
+ const textRef = useRef(null);
+
+ const handleResize = useCallback(() => {
+ if (isExpanded || isExpandedProp || textRef.current == null) {
+ return;
+ }
+ const newIsExpandable =
+ textRef.current.scrollHeight > textRef.current.clientHeight;
+ setIsExpandable(newIsExpandable);
+ }, [isExpanded, isExpandedProp]);
+
+ const debouncedHandleResize = useDebouncedCallback(handleResize, 100);
+
+ useResizeObserver(viewRef.current, debouncedHandleResize);
+
+ useLayoutEffect(debouncedHandleResize, [debouncedHandleResize]);
+
+ return (
+
+
+
+
+ {type}
+
+
+
+ {(isExpandable || isExpanded) && !isExpandedProp && (
+
+ )}
+
+
+
+ {message}
+
+
+ );
+}
+
+export default ErrorView;
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index 35f9513f3..00147bf7a 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -23,6 +23,7 @@ export * from './DraggableItemList';
export { default as DragUtils } from './DragUtils';
export { default as EditableItemList } from './EditableItemList';
export * from './ErrorBoundary';
+export { default as ErrorView } from './ErrorView';
export { default as HierarchicalCheckboxMenu } from './HierarchicalCheckboxMenu';
export * from './HierarchicalCheckboxMenu';
export * from './ItemList';
diff --git a/tests/styleguide.spec.ts b/tests/styleguide.spec.ts
index 5c15fe69e..e0eee5247 100644
--- a/tests/styleguide.spec.ts
+++ b/tests/styleguide.spec.ts
@@ -45,6 +45,7 @@ const sampleSectionIds: string[] = [
'sample-section-spectrum-forms',
'sample-section-spectrum-overlays',
'sample-section-spectrum-well',
+ 'sample-section-error-views',
];
const buttonSectionIds: string[] = [
'sample-section-buttons-regular',
diff --git a/tests/styleguide.spec.ts-snapshots/error-views-chromium-linux.png b/tests/styleguide.spec.ts-snapshots/error-views-chromium-linux.png
new file mode 100644
index 000000000..c726cbddf
Binary files /dev/null and b/tests/styleguide.spec.ts-snapshots/error-views-chromium-linux.png differ
diff --git a/tests/styleguide.spec.ts-snapshots/error-views-firefox-linux.png b/tests/styleguide.spec.ts-snapshots/error-views-firefox-linux.png
new file mode 100644
index 000000000..cd847c32e
Binary files /dev/null and b/tests/styleguide.spec.ts-snapshots/error-views-firefox-linux.png differ
diff --git a/tests/styleguide.spec.ts-snapshots/error-views-webkit-linux.png b/tests/styleguide.spec.ts-snapshots/error-views-webkit-linux.png
new file mode 100644
index 000000000..441af5ec6
Binary files /dev/null and b/tests/styleguide.spec.ts-snapshots/error-views-webkit-linux.png differ