Skip to content

Commit

Permalink
feat: Alerts component (#621)
Browse files Browse the repository at this point in the history
* Alert component, unstyled

Signed-off-by: Marcos Iglesias <miglesiasvalle@lyft.com>

* Using SVG icon and styling

Signed-off-by: Marcos Iglesias <miglesiasvalle@lyft.com>

* Knowl adjustments
Supporting an action link

Signed-off-by: Marcos Iglesias <miglesiasvalle@lyft.com>

* Extracts SVGIcons, adds story, adds link and custom action element to Alert

Signed-off-by: Marcos Iglesias <miglesiasvalle@lyft.com>

* Fixing null check

Signed-off-by: Marcos Iglesias <miglesiasvalle@lyft.com>
  • Loading branch information
Golodhros authored Aug 28, 2020
1 parent 7c7c8e9 commit 537c143
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 6 deletions.
8 changes: 4 additions & 4 deletions amundsen_application/static/.betterer.results
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,18 @@ exports[`strict null compilation`] = {
[171, 6, 3, "Type \'string\' is not assignable to type \'never\'.", "193424690"],
[172, 6, 5, "Type \'string\' is not assignable to type \'never\'.", "183222373"],
[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"],
[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"],
[305, 20, 35, "Argument of type \'ProgrammaticDescription[] | undefined\' is not assignable to parameter of type \'ProgrammaticDescription[]\'.\\n Type \'undefined\' is not assignable to type \'ProgrammaticDescription[]\'.", "4249007202"],
[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"]
],
"js/components/common/Announcements/AnnouncementsList/index.spec.tsx:1710887993": [
[95, 23, 124, "Object is possibly \'null\'.", "4248337497"]
"js/components/common/Announcements/AnnouncementsList/index.spec.tsx:1395073325": [
[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"],
[73, 4, 11, "Type \'Element[]\' is not assignable to type \'null\'.", "3768376622"],
[85, 4, 11, "Type \'Element\' is not assignable to type \'null\'.", "3768376622"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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>
</>
));
115 changes: 115 additions & 0 deletions amundsen_application/static/js/components/common/Alert/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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);
});
});
});
});
62 changes: 62 additions & 0 deletions amundsen_application/static/js/components/common/Alert/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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;
34 changes: 34 additions & 0 deletions amundsen_application/static/js/components/common/Alert/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './AlertIcon';

export enum IconSizes {
REGULAR = 24,
SMALL = 16,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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>
</>
));
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ const StorySection: React.FC<BlockProps> = ({
text,
title,
}: BlockProps) => (
<div style={{ padding: '2em', maxWidth: 600 }}>
<div style={{ padding: '2em 2em 1em', maxWidth: 600 }}>
<h1 className="text-headline-w1">{title}</h1>
{text && <p className="text-body-w1">{text}</p>}
{children}
<div style={{ paddingTop: '1em' }}>{children}</div>
</div>
);

Expand Down

0 comments on commit 537c143

Please sign in to comment.