Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Users/mordvinx/testplane 404.new UI error boundaries #630

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
eee0828
feat(css): add min-width and min-height to app
Jan 16, 2025
86c22e4
refactor(visual-checks): extract VisualCheckStickyHeader FC
Jan 16, 2025
cf910c8
refactor(test-steps): raise CollapsibleSection, extract TestStep FC
Jan 17, 2025
cce2f47
feat(error-handling): add ErrorHandling components
Jan 17, 2025
f69b768
style(suite-title): remove unused interface
Jan 17, 2025
d7855ab
feat(test-step): add corrupted style for test step item
Jan 17, 2025
908c74d
feat(error-handling): add usages of ErrorHandler component
Jan 17, 2025
d0e8138
refactor(error-handler): rename file
Jan 17, 2025
13656c3
refactor(error-handler): reorganize components
Jan 17, 2025
c62ee5e
feat(error-handler): add top level error handler
Jan 20, 2025
803eb62
feat(error-handler): additional message
Jan 20, 2025
ded253f
fix(icons): add new icons
Jan 20, 2025
15ea2d4
feat(error-handler): change error font size from 15px to 13px
Jan 21, 2025
8289456
feat(error-handler): fallback components will take 100% of width
Jan 21, 2025
5197f81
feat(error-handler): change data corruption fallback style
Jan 21, 2025
2249c14
feat(error-handler): raise AssertViewResult error handling
Jan 21, 2025
b7e6fc7
feat(error-handler): reorganize fallbacks and add styling
Jan 21, 2025
7ebb071
feat(error-handling): clip stack if length is more than 50 lines
Jan 21, 2025
fceb96d
fix(imports): remove unused import
Jan 21, 2025
a6f0f59
feat(error-handling): change icon
Jan 21, 2025
5964bf5
fix(styles): pretty width limitation for separator
Jan 22, 2025
7e47aef
refactor(error-info): remove stack clipping
Jan 23, 2025
42221f3
feat(corrupted-test-step): update warning colors
Jan 23, 2025
4ff9ad5
refactor(error-actions): do not use internals
Jan 23, 2025
e9060c4
refactor(naming): rename ErrorHandler Root to Boundary
Jan 23, 2025
8f2a279
feat(error-handler): add recommended action for CardCrash
Jan 23, 2025
1d68781
feat(error-boundary): add handling for any typed values
Jan 23, 2025
0eb3fc9
feat(error-handling): change data corrution fallback align
Jan 23, 2025
2b29f6b
feat(error-handling): add handling for broken pages
Jan 23, 2025
7092327
feat(error-handling): full width buttons
Jan 23, 2025
535ee69
fix(error-info): fix weird borders
Jan 23, 2025
2c4958a
feat(error-handling): set code block max height
Jan 23, 2025
5c495ef
fix(error-handling): fix mistypes
Jan 24, 2025
e059567
Merge branch 'master' into users/mordvinx/TESTPLANE-404.new-ui-error-…
mordvinx Jan 24, 2025
d9627b0
Merge remote-tracking branch 'upstream/master' into users/mordvinx/TE…
Jan 24, 2025
72dc31f
feat(tests): add svg import stub
Jan 24, 2025
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
8 changes: 8 additions & 0 deletions lib/static/new-ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@ body {
}

.report {
min-width: 100%;
min-height: 100%;
font-family: var(--g-font-family-sans), sans-serif !important;
margin-bottom: 0 !important;;
}

#app {
position: relative;
min-width: 100%;
min-height: 100%;
}

/* Aside header styles */
:root {
--gn-aside-header-item-current-background-color: #1f2937;
Expand Down
47 changes: 26 additions & 21 deletions lib/static/new-ui/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {CustomScripts} from '@/static/new-ui/components/CustomScripts';
import {State} from '@/static/new-ui/types/store';
import {AnalyticsProvider} from '@/static/new-ui/providers/analytics';
import {MetrikaScript} from '@/static/new-ui/components/MetrikaScript';
import {ErrorHandler} from '../features/error-handling/components/ErrorHandling';

export function App(): ReactNode {
const pages = [
Expand All @@ -33,26 +34,30 @@ export function App(): ReactNode {
const customScripts = (store.getState() as State).config.customScripts;

return <StrictMode>
<CustomScripts scripts={customScripts} />
<ThemeProvider theme='light'>
<ToasterProvider>
<Provider store={store}>
<MetrikaScript/>
<AnalyticsProvider>
<HashRouter>
<MainLayout menuItems={pages}>
<LoadingBar/>
<Routes>
<Route element={<Navigate to={'/suites'}/>} path={'/'}/>
{pages.map(page => <Route element={page.element} path={page.url} key={page.url}>{page.children}</Route>)}
</Routes>
<GuiniToolbarOverlay/>
<ToasterComponent />
</MainLayout>
</HashRouter>
</AnalyticsProvider>
</Provider>
</ToasterProvider>
</ThemeProvider>
<ErrorHandler.Root fallback={<ErrorHandler.AppCrash />}>
<CustomScripts scripts={customScripts} />
<ThemeProvider theme='light'>
<ToasterProvider>
<Provider store={store}>
<MetrikaScript/>
<AnalyticsProvider>
<HashRouter>
<ErrorHandler.Root fallback={<ErrorHandler.AppCrash />}>
<MainLayout menuItems={pages}>
<LoadingBar/>
<Routes>
<Route element={<Navigate to={'/suites'}/>} path={'/'}/>
{pages.map(page => <Route element={page.element} path={page.url} key={page.url}>{page.children}</Route>)}
</Routes>
<GuiniToolbarOverlay/>
<ToasterComponent />
</MainLayout>
</ErrorHandler.Root>
</HashRouter>
</AnalyticsProvider>
</Provider>
</ToasterProvider>
</ThemeProvider>
</ErrorHandler.Root>
</StrictMode>;
}
61 changes: 40 additions & 21 deletions lib/static/new-ui/components/AssertViewResult/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {Screenshot} from '@/static/new-ui/components/Screenshot';
import {ImageLabel} from '@/static/new-ui/components/ImageLabel';
import {getImageDisplayedSize} from '@/static/new-ui/utils';
import styles from './index.module.css';
import {ErrorHandler} from '../../features/error-handling/components/ErrorHandling';

interface AssertViewResultProps {
result: ImageEntity;
Expand All @@ -17,27 +18,45 @@ interface AssertViewResultProps {

function AssertViewResultInternal({result, diffMode, style}: AssertViewResultProps): ReactNode {
if (result.status === TestStatus.FAIL) {
return <DiffViewer diffMode={diffMode} {...result} />;
} else if (result.status === TestStatus.ERROR) {
return <div className={styles.screenshotContainer}>
<ImageLabel title={'Actual'} subtitle={getImageDisplayedSize(result.actualImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />
</div>;
} else if (result.status === TestStatus.SUCCESS || result.status === TestStatus.UPDATED) {
return <div className={styles.screenshotContainer}>
<ImageLabel title={'Expected'} subtitle={getImageDisplayedSize(result.expectedImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.expectedImg} />
</div>;
} else if (result.status === TestStatus.STAGED) {
return <div className={styles.screenshotContainer}>
<ImageLabel title={'Staged'} subtitle={getImageDisplayedSize(result.actualImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />
</div>;
} else if (result.status === TestStatus.COMMITED) {
return <div className={styles.screenshotContainer}>
<ImageLabel title={'Committed'} subtitle={getImageDisplayedSize(result.actualImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />
</div>;
return <ErrorHandler.Root watchFor={[result]} fallback={<ErrorHandler.CardCrash />}>
<DiffViewer diffMode={diffMode} {...result} />
</ErrorHandler.Root>;
}

if (result.status === TestStatus.ERROR) {
return <ErrorHandler.Root watchFor={[result]} fallback={<ErrorHandler.CardCrash />}>
<div className={styles.screenshotContainer}>
<ImageLabel title={'Actual'} subtitle={getImageDisplayedSize(result.actualImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />
</div>
</ErrorHandler.Root>;
}

if (result.status === TestStatus.SUCCESS || result.status === TestStatus.UPDATED) {
return <ErrorHandler.Root watchFor={[result]} fallback={<ErrorHandler.CardCrash />}>
<div className={styles.screenshotContainer}>
<ImageLabel title={'Expected'} subtitle={getImageDisplayedSize(result.expectedImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.expectedImg} />
</div>
</ErrorHandler.Root>;
}

if (result.status === TestStatus.STAGED) {
return <ErrorHandler.Root watchFor={[result]} fallback={<ErrorHandler.CardCrash />}>
<div className={styles.screenshotContainer}>
<ImageLabel title={'Staged'} subtitle={getImageDisplayedSize(result.actualImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />
</div>
</ErrorHandler.Root>;
}

if (result.status === TestStatus.COMMITED) {
return <ErrorHandler.Root watchFor={[result]} fallback={<ErrorHandler.CardCrash />}>
<div className={styles.screenshotContainer}>
<ImageLabel title={'Committed'} subtitle={getImageDisplayedSize(result.actualImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />
</div>
</ErrorHandler.Root>;
}

return null;
Expand Down
5 changes: 1 addition & 4 deletions lib/static/new-ui/components/SuiteTitle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import styles from './index.module.css';

interface SuiteTitleProps {
className?: string;
}

interface SuiteTitlePropsInternal extends SuiteTitleProps {
suitePath: string[];
browserName: string;
stateName?: string;
Expand All @@ -19,7 +16,7 @@ interface SuiteTitlePropsInternal extends SuiteTitleProps {
onNext: () => void;
}

export function SuiteTitle(props: SuiteTitlePropsInternal): ReactNode {
export function SuiteTitle(props: SuiteTitleProps): ReactNode {
const suiteName = props.suitePath[props.suitePath.length - 1];
const suitePath = props.suitePath.slice(0, -1);

Expand Down
6 changes: 6 additions & 0 deletions lib/static/new-ui/components/TreeViewItem/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@
background: var(--g-color-private-red-100);
color: var(--g-color-private-red-600-solid);
}

.tree-view-item--corrupted {
--g-color-base-simple-hover: var(--g-color-private-orange-50);
background: var(--g-color-private-orange-100);
color: var(--g-color-private-orange-600-solid);
}
5 changes: 3 additions & 2 deletions lib/static/new-ui/components/TreeViewItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface TreeListItemProps<T> {
id: string;
list: UseListResult<T>;
mapItemDataToContentProps: (data: T) => ListItemViewContentType;
isFailed?: boolean;
status?: 'error' | 'corrupted';
onItemClick?: (data: {id: string}) => unknown;
}

Expand All @@ -34,7 +34,8 @@ export function TreeViewItem<T>(props: TreeListItemProps<T>): ReactNode {
>
<ListItemView
className={classNames([styles.treeViewItem, {
[styles['tree-view-item--error']]: props.isFailed
[styles['tree-view-item--corrupted']]: props.status === 'corrupted',
[styles['tree-view-item--error']]: props.status === 'error'
}])}
activeOnHover={true}
style={{'--indent': indent + Number(!hasChildren)} as React.CSSProperties}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, {Component, DependencyList, ErrorInfo, ReactNode} from 'react';
import {BoundaryProps, BoundaryState} from './interfaces';
import {ErrorContextProvider} from './context';

export class Boundary extends Component<BoundaryProps, BoundaryState> {
constructor(props: BoundaryProps) {
super(props);
this.state = {
watchFor: props.watchFor,
hasError: false,
error: null,
errorInfo: null
};
}

private static messageStyle = 'background: crimson; color: white;';
private static timestampStyle = 'color: gray; font-size: smaller';

private static isNothingChanged(prev?: DependencyList, next?: DependencyList): boolean {
if (prev === next) {
return true;
}

if (prev === undefined || next === undefined) {
return false;
}

if (prev.length !== next.length) {
return false;
}

return prev.every((item, index) => item === next[index]);
}

private restore(): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А где используется restore, я что-то пока не увидел?

this.setState({hasError: false, error: null});
}

static getDerivedStateFromProps(nextProps: BoundaryProps, prevState: BoundaryState): null | BoundaryState {
if (Boundary.isNothingChanged(prevState.watchFor, nextProps.watchFor)) {
return null;
}

return {...prevState, error: null, hasError: false, errorInfo: null, watchFor: nextProps.watchFor};
}

componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.setState({hasError: true, error, errorInfo});

const timestamp = new Date().toTimeString();

console.groupCollapsed(
`%cError boundary catched error named "${error.name}". See details below:` + '%c @ ' + timestamp,
Boundary.messageStyle,
Boundary.timestampStyle
);

console.error(error);
console.error('Component stack: ', errorInfo.componentStack);
console.groupEnd();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

render(): ReactNode {
if (this.state.hasError) {
return (
<ErrorContextProvider value={{state: this.state, restore: this.restore.bind(this)}}>
{this.props.fallback}
</ErrorContextProvider>
);
}

return this.props.children;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {ArrowsRotateLeft} from '@gravity-ui/icons';
import {Button, Icon} from '@gravity-ui/uikit';
import React, {ReactNode} from 'react';
import GithubIcon from '../../../../../icons/github-icon.svg';
import {NEW_ISSUE_LINK} from '@/constants';

function reportIssue(): void {
window.open(NEW_ISSUE_LINK, '_blank');
}

export function FileIssue(): ReactNode {
return <Button view="outlined" onClick={reportIssue}>
<img src={GithubIcon} alt="icon" />
File an issue
</Button>;
}

function reloadPage(): void {
window.location.reload();
}

export function ReloadPage(): ReactNode {
return <Button view="outlined" onClick={reloadPage}>
<Icon data={ArrowsRotateLeft} />
Refresh this page
</Button>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import {ErrorContext} from './interfaces';

const Context = React.createContext<ErrorContext| null>(null);

export const ErrorContextProvider = Context.Provider;

export const useErrorContext = (): ErrorContext => {
const ctx = React.useContext(Context);

if (ctx === null) {
throw new Error('useErrorContext must be used within ErrorContextProvider');
}

return ctx;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {TriangleExclamation} from '@gravity-ui/icons';
import {Divider, Icon, Link, Text} from '@gravity-ui/uikit';
import classNames from 'classnames';
import React, {ReactNode} from 'react';
import TestplaneIcon from '../../../../../icons/testplane-mono-black.svg';
import {ErrorInfo as ErrorInfoFc} from '../../../../components/ErrorInfo';
import styles from './index.module.css';
import {useErrorContext} from './context';
import {FileIssue, ReloadPage} from './actions';
import {NEW_ISSUE_LINK} from '@/constants';

export function AppCrash(): ReactNode {
const {state} = useErrorContext();

return <div className={styles.crashAbsoluteWrapper}>
<div className={classNames(styles.crash)}>
<img src={TestplaneIcon} alt="icon" width={32} height={32}/>

<Text variant="subheader-3">Something went wrong</Text>
<Text variant="body-1" color="secondary">Testplane UI has crashed</Text>

<ErrorInfoFc className={styles.errorInfo} name={state.error.name} stack={state.error.stack} />

<div className={classNames(styles.actionRow)}>
<ReloadPage />

<FileIssue />
</div>

<Text variant="body-1" color="secondary">
We would appreciate a detailed<br/>
report with reproduction steps.
</Text>
</div>
</div>;
}

export function CardCrash(): ReactNode {
const {state} = useErrorContext();

return <div className={classNames(styles.crash)}>
<Icon data={TriangleExclamation} size={52}/>

<Text variant="subheader-3">Something went wrong</Text>
<Text variant="body-1" color="secondary">The data is corrupted or there’s a bug on our side</Text>

<ErrorInfoFc className={styles.errorInfo} name={state.error.name} stack={state.error.stack} />

<Text variant="body-1">Try choosing another item</Text>

<div className={classNames(styles.pickActionSeparator)}>
<Divider className={classNames(styles.pickActionSeparatorLine)} />
<Text variant="caption-1" color="secondary">OR</Text>
<Divider className={classNames(styles.pickActionSeparatorLine)} />
</div>

<FileIssue />
</div>;
}

export function DataCorruption(): ReactNode {
const {state} = useErrorContext();

return <div className={classNames(styles.crash)}>
<Text variant="body-1" color="secondary">The data is corrupted or there’s a bug on our side. <Link href={NEW_ISSUE_LINK} target='_blank'>File an issue</Link></Text>

<ErrorInfoFc className={styles.errorInfo} name={state.error.name} stack={state.error.stack} />
</div>;
}
Loading