Unverified Commit 537c1432 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Alerts component (#621)

* Alert component, unstyled
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Using SVG icon and styling
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Knowl adjustments
Supporting an action link
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Extracts SVGIcons, adds story, adds link and custom action element to Alert
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Fixing null check
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>
parent 7c7c8e92
...@@ -113,7 +113,7 @@ exports[`strict null compilation`] = { ...@@ -113,7 +113,7 @@ exports[`strict null compilation`] = {
[171, 6, 3, "Type \'string\' is not assignable to type \'never\'.", "193424690"], [171, 6, 3, "Type \'string\' is not assignable to type \'never\'.", "193424690"],
[172, 6, 5, "Type \'string\' is not assignable to type \'never\'.", "183222373"], [172, 6, 5, "Type \'string\' is not assignable to type \'never\'.", "183222373"],
[182, 8, 7, "Type \'Element\' is not assignable to type \'never\'.", "3716929964"], [182, 8, 7, "Type \'Element\' is not assignable to type \'never\'.", "3716929964"],
[183, 11, 26, "Type \'{ itemsPerPage: number; source: string; }\' is missing the following properties from type \'Readonly<Pick<TableDashboardResourceListProps, \\"source\\" | \\"dashboards\\" | \\"isLoading\\" | \\"errorText\\" | \\"itemsPerPage\\"> & OwnProps>\': dashboards, isLoading, errorText", "2224258167"], [183, 11, 26, "Type \'{ itemsPerPage: number; source: string; }\' is missing the following properties from type \'Readonly<Pick<TableDashboardResourceListProps, \\"source\\" | \\"isLoading\\" | \\"dashboards\\" | \\"errorText\\" | \\"itemsPerPage\\"> & OwnProps>\': isLoading, dashboards, errorText", "2224258167"],
[188, 8, 3, "Type \'string\' is not assignable to type \'never\'.", "193424690"], [188, 8, 3, "Type \'string\' is not assignable to type \'never\'.", "193424690"],
[189, 8, 5, "Type \'string | Element\' is not assignable to type \'never\'.\\n Type \'string\' is not assignable to type \'never\'.", "183222373"], [189, 8, 5, "Type \'string | Element\' is not assignable to type \'never\'.\\n Type \'string\' is not assignable to type \'never\'.", "183222373"],
[263, 16, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "3817619378"], [263, 16, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "3817619378"],
...@@ -121,10 +121,10 @@ exports[`strict null compilation`] = { ...@@ -121,10 +121,10 @@ exports[`strict null compilation`] = {
[319, 20, 36, "Argument of type \'ProgrammaticDescription[] | undefined\' is not assignable to parameter of type \'ProgrammaticDescription[]\'.\\n Type \'undefined\' is not assignable to type \'ProgrammaticDescription[]\'.", "2770872537"], [319, 20, 36, "Argument of type \'ProgrammaticDescription[] | undefined\' is not assignable to parameter of type \'ProgrammaticDescription[]\'.\\n Type \'undefined\' is not assignable to type \'ProgrammaticDescription[]\'.", "2770872537"],
[324, 16, 36, "Argument of type \'ProgrammaticDescription[] | undefined\' is not assignable to parameter of type \'ProgrammaticDescription[]\'.\\n Type \'undefined\' is not assignable to type \'ProgrammaticDescription[]\'.", "2776557981"] [324, 16, 36, "Argument of type \'ProgrammaticDescription[] | undefined\' is not assignable to parameter of type \'ProgrammaticDescription[]\'.\\n Type \'undefined\' is not assignable to type \'ProgrammaticDescription[]\'.", "2776557981"]
], ],
"js/components/common/Announcements/AnnouncementsList/index.spec.tsx:1710887993": [ "js/components/common/Announcements/AnnouncementsList/index.spec.tsx:1395073325": [
[95, 23, 124, "Object is possibly \'null\'.", "4248337497"] [94, 23, 124, "Object is possibly \'null\'.", "4248337497"]
], ],
"js/components/common/Announcements/AnnouncementsList/index.tsx:1484765516": [ "js/components/common/Announcements/AnnouncementsList/index.tsx:3478884749": [
[70, 4, 11, "Type \'Element\' is not assignable to type \'null\'.", "3768376622"], [70, 4, 11, "Type \'Element\' is not assignable to type \'null\'.", "3768376622"],
[73, 4, 11, "Type \'Element[]\' is not assignable to type \'null\'.", "3768376622"], [73, 4, 11, "Type \'Element[]\' is not assignable to type \'null\'.", "3768376622"],
[85, 4, 11, "Type \'Element\' is not assignable to type \'null\'.", "3768376622"], [85, 4, 11, "Type \'Element\' is not assignable to type \'null\'.", "3768376622"],
......
import React from 'react';
import { storiesOf } from '@storybook/react';
import StorySection from '../StorySection';
import Alert from '.';
const stories = storiesOf('Components/Alert', module);
stories.add('Alert', () => (
<>
<StorySection title="Alert">
<Alert
message="Alert text that can be short"
onAction={() => {
alert('action executed!');
}}
/>
</StorySection>
<StorySection title="Alert with text link">
<Alert
message={
<span>
Alert text that has a <a href="https://lyft.com">link</a>
</span>
}
/>
</StorySection>
<StorySection title="Alert with Action as button">
<Alert
message="Alert text that can be short"
actionText="Action Text"
onAction={() => {
alert('action executed!');
}}
/>
</StorySection>
<StorySection title="Alert with Action as link">
<Alert
message="Alert text that can be short"
actionText="Action Text"
actionHref="http://www.lyft.com"
/>
</StorySection>
<StorySection title="Alert with Action as custom link">
<Alert
message="Alert text that can be short"
actionLink={
<a className="test-action-link" href="http://testSite.com">
Custom Link
</a>
}
/>
</StorySection>
<StorySection title="Alert with long text">
<Alert message="Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam perspiciatis non ipsa officia expedita magnam mollitia, excepturi iste eveniet qui nisi eum illum, quas voluptas, reprehenderit quam molestias cum quisquam!" />
</StorySection>
<StorySection title="Alert with long text and action">
<Alert
actionText="Action Text"
onAction={() => {
alert('action executed!');
}}
message="Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam perspiciatis non ipsa officia expedita magnam mollitia, excepturi iste eveniet qui nisi eum illum, quas voluptas, reprehenderit quam molestias cum quisquam!"
/>
</StorySection>
</>
));
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { mount } from 'enzyme';
import Alert, { AlertProps } from '.';
const setup = (propOverrides?: Partial<AlertProps>) => {
const props: AlertProps = {
message: 'Test Message',
onAction: jest.fn(),
...propOverrides,
};
const wrapper = mount(<Alert {...props} />);
return { props, wrapper };
};
describe('Alert', () => {
describe('render', () => {
it('should render an alert icon', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.alert-triangle-svg-icon').length;
expect(actual).toEqual(expected);
});
it('should render the alert message text', () => {
const { props, wrapper } = setup();
const expected = props.message;
const actual = wrapper.find('.alert-message').text();
expect(actual).toEqual(expected);
});
describe('when passing an action text and action handler', () => {
it('should render the action button', () => {
const { wrapper } = setup({ actionText: 'Action Text' });
const expected = 1;
const actual = wrapper.find('.btn-link').length;
expect(actual).toEqual(expected);
});
it('should render the action text', () => {
const { props, wrapper } = setup({ actionText: 'Action Text' });
const expected = props.actionText;
const actual = wrapper.find('.btn-link').text();
expect(actual).toEqual(expected);
});
});
describe('when passing an action text and action href', () => {
it('should render the action link', () => {
const { wrapper } = setup({
actionHref: 'http://testSite.com',
actionText: 'Action Text',
});
const expected = 1;
const actual = wrapper.find('.action-link').length;
expect(actual).toEqual(expected);
});
it('should render the action text', () => {
const { props, wrapper } = setup({
actionHref: 'http://testSite.com',
actionText: 'Action Text',
});
const expected = props.actionText;
const actual = wrapper.find('.action-link').text();
expect(actual).toEqual(expected);
});
});
describe('when passing a custom action link', () => {
it('should render the custom action link', () => {
const { wrapper } = setup({
actionLink: (
<a className="test-action-link" href="http://testSite.com">
Custom Link
</a>
),
});
const expected = 1;
const actual = wrapper.find('.test-action-link').length;
expect(actual).toEqual(expected);
});
});
});
describe('lifetime', () => {
describe('when clicking on the action button', () => {
it('should call the handler', () => {
const handlerSpy = jest.fn();
const { wrapper } = setup({
actionText: 'Action Text',
onAction: handlerSpy,
});
const expected = 1;
wrapper.find('button.btn-link').simulate('click');
const actual = handlerSpy.mock.calls.length;
expect(actual).toEqual(expected);
});
});
});
});
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { AlertIcon, IconSizes } from '../SVGIcons';
import './styles.scss';
const STROKE_COLOR = '#b8072c'; // $red70
export interface AlertProps {
message: string | React.ReactNode;
actionLink?: React.ReactNode;
actionText?: string;
actionHref?: string;
onAction?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
const Alert: React.FC<AlertProps> = ({
message,
onAction,
actionText,
actionHref,
actionLink,
}: AlertProps) => {
let action: null | React.ReactNode = null;
if (actionText && onAction) {
action = (
<span className="alert-action">
<button type="button" className="btn btn-link" onClick={onAction}>
{actionText}
</button>
</span>
);
}
if (actionText && actionHref) {
action = (
<span className="alert-action">
<a className="action-link" href={actionHref}>
{actionText}
</a>
</span>
);
}
if (actionLink) {
action = <span className="alert-action">{actionLink}</span>;
}
return (
<div className="alert">
<AlertIcon stroke={STROKE_COLOR} size={IconSizes.SMALL} />
<p className="alert-message">{message}</p>
{action}
</div>
);
};
export default Alert;
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
@import 'variables';
@import 'typography';
$alert-border-radius: 4px;
$alert-message-line-height: 24px;
.alert {
background-color: $body-bg;
border-radius: $alert-border-radius;
display: flex;
padding: $spacer-1 $spacer-1 * 1.5 $spacer-1 $spacer-2;
justify-content: flex-start;
box-shadow: $elevation-level2;
.alert-message {
@extend %text-body-w2;
margin: 0;
display: inline;
}
.alert-triangle-svg-icon {
flex-shrink: 0;
align-self: center;
margin-right: $spacer-1;
}
.alert-action {
margin: auto 0 auto auto;
}
}
import * as React from 'react';
import { IconSizes } from '.';
const DEFAULT_STROKE_COLOR = 'currentColor';
export interface IconProps {
stroke?: string;
size?: number;
}
export const AlertIcon: React.FC<IconProps> = ({
stroke = DEFAULT_STROKE_COLOR,
size = IconSizes.REGULAR,
}: IconProps) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={stroke}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="alert-triangle-svg-icon"
>
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0zM12 9v4M12 17h0" />
</svg>
);
};
export * from './AlertIcon';
export enum IconSizes {
REGULAR = 24,
SMALL = 16,
}
import React from 'react';
import { storiesOf } from '@storybook/react';
import StorySection from '../StorySection';
import { AlertIcon } from '.';
const stories = storiesOf('Attributes/Icons', module);
stories.add('SVG Icons', () => (
<>
<StorySection title="Alert">
<AlertIcon />
</StorySection>
</>
));
...@@ -11,10 +11,10 @@ const StorySection: React.FC<BlockProps> = ({ ...@@ -11,10 +11,10 @@ const StorySection: React.FC<BlockProps> = ({
text, text,
title, title,
}: BlockProps) => ( }: BlockProps) => (
<div style={{ padding: '2em', maxWidth: 600 }}> <div style={{ padding: '2em 2em 1em', maxWidth: 600 }}>
<h1 className="text-headline-w1">{title}</h1> <h1 className="text-headline-w1">{title}</h1>
{text && <p className="text-body-w1">{text}</p>} {text && <p className="text-body-w1">{text}</p>}
{children} <div style={{ paddingTop: '1em' }}>{children}</div>
</div> </div>
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment