Merge pull request #1539 from github/koesie10/alert-components

Add Alert component for showing warnings and errors
This commit is contained in:
Koen Vlaswinkel
2022-09-26 15:00:38 +02:00
committed by GitHub
4 changed files with 212 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { VSCodeButton, VSCodeLink } from '@vscode/webview-ui-toolkit/react';
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
import { Alert } from '../../view/common';
export default {
title: 'Alert',
component: Alert,
decorators: [
(Story) => (
<VariantAnalysisContainer>
<Story />
</VariantAnalysisContainer>
)
],
} as ComponentMeta<typeof Alert>;
const Template: ComponentStory<typeof Alert> = (args) => (
<Alert {...args} />
);
export const Warning = Template.bind({});
Warning.args = {
type: 'warning',
title: 'This query found a warning',
message: <>Warning content with <VSCodeLink>links</VSCodeLink></>,
};
export const WarningInverse = Template.bind({});
WarningInverse.args = {
...Warning.args,
message: 'Warning content',
inverse: true,
};
export const WarningExample = Template.bind({});
WarningExample.args = {
type: 'warning',
title: 'Query manually stopped',
message: 'This query was manually stopped before the analysis completed. Results may be partial.',
};
export const Error = Template.bind({});
Error.args = {
type: 'error',
title: 'This query found an error',
message: <>Error content with <VSCodeLink>links</VSCodeLink></>,
};
export const ErrorInverse = Template.bind({});
ErrorInverse.args = {
...Error.args,
message: 'Error content',
inverse: true,
};
export const ErrorExample = Template.bind({});
ErrorExample.args = {
type: 'error',
title: 'Request failed',
message: <>
Request to https://api.github.com/repos/octodemo/Hello-World/code-scanning/codeql/queries failed.{' '}
<VSCodeLink>Check logs</VSCodeLink> and try running this query again.
</>,
};
export const ErrorWithButtons = Template.bind({});
ErrorWithButtons.args = {
type: 'error',
title: 'Request failed',
message: 'Request to https://api.github.com/repos/octodemo/Hello-World/code-scanning/codeql/queries failed. Try running this query again.',
actions: (
<>
<VSCodeButton appearance="secondary">View logs</VSCodeButton>
<VSCodeButton>Retry</VSCodeButton>
</>
),
};

View File

@@ -0,0 +1,98 @@
import * as React from 'react';
import { ReactNode } from 'react';
import styled from 'styled-components';
type ContainerProps = {
type: 'warning' | 'error';
inverse?: boolean;
};
const getBackgroundColor = ({ type, inverse }: ContainerProps): string => {
if (!inverse) {
return 'var(--vscode-notifications-background)';
}
switch (type) {
case 'warning':
return 'var(--vscode-editorWarning-foreground)';
case 'error':
return 'var(--vscode-debugExceptionWidget-border)';
}
};
const getTextColor = ({ type, inverse }: ContainerProps): string => {
if (!inverse) {
return 'var(--vscode-editor-foreground)';
}
switch (type) {
case 'warning':
return 'var(--vscode-editor-background)';
case 'error':
return 'var(--vscode-list-activeSelectionForeground)';
}
};
const getBorderColor = ({ type }: ContainerProps): string => {
switch (type) {
case 'warning':
return 'var(--vscode-editorWarning-foreground)';
case 'error':
return 'var(--vscode-editorError-foreground)';
}
};
const getTypeText = (type: ContainerProps['type']): string => {
switch (type) {
case 'warning':
return 'Warning';
case 'error':
return 'Error';
}
};
const Container = styled.div<ContainerProps>`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1em;
padding: 1em;
color: ${props => getTextColor(props)};
background-color: ${props => getBackgroundColor(props)};
border: 1px solid ${props => getBorderColor(props)};
`;
const Title = styled.div`
font-size: 0.85em;
font-weight: 800;
text-transform: uppercase;
`;
const ActionsContainer = styled.div`
display: flex;
flex-direction: row;
gap: 0.75em;
margin-left: auto;
`;
type Props = {
type: 'warning' | 'error';
title: string;
message: ReactNode;
actions?: ReactNode;
// Inverse the color scheme
inverse?: boolean;
};
export const Alert = ({ type, title, message, actions, inverse }: Props) => {
return (
<Container type={type} inverse={inverse}>
<Title>{getTypeText(type)}: {title}</Title>
<span>{message}</span>
{actions && <ActionsContainer>{actions}</ActionsContainer>}
</Container>
);
};

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { Alert } from '../Alert';
describe(Alert.name, () => {
it('renders a warning correctly', () => {
render(<Alert type="warning" title="Warning title" message="Warning content" />);
expect(screen.getByText('Warning: Warning title')).toBeInTheDocument();
expect(screen.getByText('Warning content')).toBeInTheDocument();
});
it('renders an error correctly', () => {
render(<Alert type="error" title="Error title" message="Error content" />);
expect(screen.getByText('Error: Error title')).toBeInTheDocument();
expect(screen.getByText('Error content')).toBeInTheDocument();
});
it('renders actions correctly', () => {
render(
<Alert
type="error"
title="Error title"
message="Error content"
actions={<>My actions content</>}
/>
);
expect(screen.getByText('My actions content')).toBeInTheDocument();
});
});

View File

@@ -1,4 +1,5 @@
export * from './icon';
export * from './Alert';
export * from './CodePaths';
export * from './FileCodeSnippet';
export * from './HorizontalSpace';