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

feat: Alerts component #621

Merged
merged 5 commits into from
Aug 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 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>
</>
));
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);
});
});
});
});
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}>
Copy link
Contributor

Choose a reason for hiding this comment

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

For our internal use case, the text will end up being a link to another Amundsen page. Given our desire to use the correct components for specific actions, do you think we should still support a button with onClick vs and anchor with an href...or both?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, I see.
I could make it support both easily. I will add an optional actionHref prop, if that has a value, I will render a router Link instead

{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;
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