Unverified Commit 1eae8e8d authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Announcements in Homepage (#591)

* feat: AnnouncementsList component (#540)

* Adds fake endpoint return for development

* Basic Announcements list

* Basic unstyled Announcements list

* Restoring proper announcements endpoint code

* Linting issues
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* feat: Announcements container and saga, api and reducer modifications (#541)

* Basic container

* Adding status code to announcement response

* Updating Announcements reducer, sagas and api to support loading and error states

* Linting details

* Completing the global state fixture

* Basic tests for connection
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Basic card with loading shimmer (#546)
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* feat: Card Styling for announcements (#550)

* Shimmering card styles

* Basic card typography styling

* Add links and link styles to cards

* Adjusting card copy per Knowl specs
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* feat: Wiring announcements block on Homepage (#551)

* Adds announcemetns to homepage, integrates basic card

* Spacing and details

* Adjusting loading state

* Adds card and see more links logging

* Updates layout size; focus detail

* Variables on List component styles

* Cleaning fake response
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* feat: Adds config for announcements (#562)

* Moves config tests, adds config for announcements and test
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Wiring announcements feature to the config option
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Updating configuration docs
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Extracting navLinks logic into the config utils
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Adds LPL typography to announcements; some layout fine-tuning (#581)
Signed-off-by: 's avatarMarcos Iglesias Valle <golodhros@gmail.com>
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Fixing merge conflicts
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>
parent 651059bf
......@@ -18,14 +18,15 @@ $loading-curve: cubic-bezier(0.45, 0, 0.15, 1);
.is-shimmer-animated {
animation: $loading-duration shimmer $loading-curve infinite;
background-image: linear-gradient(
to right,
$gray10 0%,
$gray10 33%,
$gray5 50%,
$gray10 67%,
$gray10 100%
);
background-image:
linear-gradient(
to right,
$gray10 0%,
$gray10 33%,
$gray5 50%,
$gray10 67%,
$gray10 100%
);
background-repeat: no-repeat;
background-size: 300% 100%;
}
......@@ -5,6 +5,8 @@
$resource-header-height: 84px;
$aside-separation-space: 40px;
$screen-lg-max: 1490px;
$screen-lg-container: 1440px;
.resource-detail-layout {
height: calc(100vh - #{$nav-bar-height} - #{$footer-height});
......@@ -198,6 +200,12 @@ $aside-separation-space: 40px;
min-width: $body-min-width;
}
@media (min-width: $screen-lg-max) {
#main > .container {
width: $screen-lg-container;
}
}
#main > .container {
margin: 96px auto 48px;
}
......
......@@ -3,6 +3,150 @@
@import 'variables';
// New Typography styles based on LPL
// Use these styles going forward
// Placeholder selectors
// Ref: http://thesassway.com/intermediate/understanding-placeholder-selectors
%text-headline-w1 {
font-family: $text-heading-font-family;
font-size: $w1-headline-font-size;
line-height: $w1-headline-line-height;
font-weight: $title-font-weight;
}
%text-headline-w2 {
font-family: $text-heading-font-family;
font-size: $w2-headline-font-size;
line-height: $w2-headline-line-height;
font-weight: $title-font-weight;
}
%text-headline-w3 {
font-family: $text-heading-font-family;
font-size: $w3-headline-font-size;
line-height: $w3-headline-line-height;
font-weight: $title-font-weight;
}
%text-title-w1 {
font-family: $text-body-font-family;
font-size: $w1-font-size;
line-height: $w1-line-height;
font-weight: $title-font-weight;
}
%text-title-w2 {
font-family: $text-body-font-family;
font-size: $w2-font-size;
line-height: $w2-line-height;
font-weight: $title-font-weight;
}
%text-title-w3 {
font-family: $text-body-font-family;
font-size: $w3-font-size;
line-height: $w3-line-height;
font-weight: $title-font-weight;
}
%text-subtitle-w1 {
font-family: $text-body-font-family;
font-size: $w1-font-size;
line-height: $w1-line-height;
font-weight: $subtitle-font-weight;
}
%text-subtitle-w2 {
font-family: $text-body-font-family;
font-size: $w2-font-size;
line-height: $w2-line-height;
font-weight: $subtitle-font-weight;
}
%text-subtitle-w3 {
font-family: $text-body-font-family;
font-size: $w3-font-size;
line-height: $w3-line-height;
font-weight: $subtitle-font-weight;
}
%text-body-w1 {
font-family: $text-body-font-family;
font-size: $w1-font-size;
line-height: $w1-line-height;
font-weight: $body-font-weight;
}
%text-body-w2 {
font-family: $text-body-font-family;
font-size: $w2-font-size;
line-height: $w2-line-height;
font-weight: $body-font-weight;
}
%text-body-w3 {
font-family: $text-body-font-family;
font-size: $w3-font-size;
line-height: $w3-line-height;
font-weight: $body-font-weight;
}
// Typography classes
// Headlines
.text-headline-w1 {
@extend %text-headline-w1;
}
.text-headline-w2 {
@extend %text-headline-w2;
}
.text-headline-w3 {
@extend %text-headline-w3;
}
// Titles
.text-title-w1 {
@extend %text-title-w1;
}
.text-title-w2 {
@extend %text-title-w2;
}
.text-title-w3 {
@extend %text-title-w3;
}
// Subtitles
.text-subtitle-w1 {
@extend %text-subtitle-w1;
}
.text-subtitle-w2 {
@extend %text-subtitle-w2;
}
.text-subtitle-w3 {
@extend %text-subtitle-w3;
}
// Body
.text-body-w1 {
@extend %text-body-w1;
}
.text-body-w2 {
@extend %text-body-w2;
}
.text-body-w3 {
@extend %text-body-w3;
}
// Typography Helpers
.text-center {
text-align: center;
}
......@@ -15,6 +159,40 @@
text-align: right;
}
.truncated {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Text for Screen Readers only
// Reference: Bootstrap 4 codebase
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
// From https://gist.github.com/igorescobar/d74a76629bab47d601d71c3a6e010ff2
@mixin truncate($font-size, $line-height, $lines-to-show) {
display: block; // Fallback for non-webkit
display: -webkit-box;
font-size: $font-size;
line-height: $line-height;
-webkit-line-clamp: $lines-to-show;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
// Old typography styles
// DEPRECATED - Don't use!
h1,
h2,
h3,
......@@ -184,23 +362,3 @@ body {
.text-primary {
color: $text-primary;
}
.truncated {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Text for Screen Readers only
// Reference: Bootstrap 4 codebase
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
......@@ -27,7 +27,6 @@ $stroke-underline: $gray40 !default;
// Typography
$text-primary: $gray100 !default;
$text-secondary: $gray60 !default;
$text-secondary: $gray60 !default;
$text-tertiary: $gray40 !default;
$text-placeholder: $gray40 !default;
$text-inverse: $white !default;
......@@ -130,3 +129,39 @@ $spacer-1: $spacer-size;
$spacer-2: $spacer-size * 2;
$spacer-3: $spacer-size * 3;
$spacer-4: $spacer-size * 4;
// Elevations (from LPL)
$elevation-level1: 0 0 1px 0 rgba(0, 0, 0, 0.12),
0 1px 1px 1px rgba(0, 0, 0, 0.08);
$elevation-level2: 0 0 1px 0 rgba(0, 0, 0, 0.12),
0 2px 3px 0 rgba(0, 0, 0, 0.16);
$elevation-level3: 0 0 1px 0 rgba(0, 0, 0, 0.12),
0 2px 4px 0 rgba(0, 0, 0, 0.16);
$elevation-level4: 0 0 1px 0 rgba(0, 0, 0, 0.12),
0 3px 6px 0 rgba(0, 0, 0, 0.16);
// New Typography variables based on LPL
$text-heading-font-family: $font-family-header;
$text-body-font-family: $font-family-body;
$w1-font-size: 20px;
$w1-line-height: 24px;
$w2-font-size: 16px;
$w2-line-height: 20px;
$w3-font-size: 14px;
$w3-line-height: 18px;
$w1-headline-font-size: 36px;
$w1-headline-line-height: 40px;
$w2-headline-font-size: 26px;
$w2-headline-line-height: 28px;
$w3-headline-font-size: 22px;
$w3-headline-line-height: 24px;
$title-font-weight: $font-weight-body-bold;
$subtitle-font-weight: $font-weight-body-semi-bold;
$body-font-weight: $font-weight-body-regular;
......@@ -13,7 +13,7 @@ import {
AnnouncementPageProps,
mapDispatchToProps,
mapStateToProps,
} from '..';
} from '.';
describe('AnnouncementPage', () => {
let props: AnnouncementPageProps;
......
......@@ -14,9 +14,11 @@ import './styles.scss';
import { GlobalState } from 'ducks/rootReducer';
import { GetAnnouncementsRequest } from 'ducks/announcements/types';
import { getAnnouncements } from 'ducks/announcements/reducer';
import { getAnnouncements } from 'ducks/announcements';
import { AnnouncementPost } from 'interfaces';
const ANNOUNCEMENTS_HEADER_TEXT = 'Announcements';
export interface StateFromProps {
posts: AnnouncementPost[];
}
......@@ -29,7 +31,9 @@ export type AnnouncementPageProps = StateFromProps & DispatchFromProps;
export class AnnouncementPage extends React.Component<AnnouncementPageProps> {
componentDidMount() {
this.props.announcementsGet();
const { announcementsGet } = this.props;
announcementsGet();
}
createPost(post: AnnouncementPost, postIndex: number) {
......@@ -47,7 +51,9 @@ export class AnnouncementPage extends React.Component<AnnouncementPageProps> {
}
createPosts() {
return this.props.posts.map((post, index) => {
const { posts } = this.props;
return posts.map((post, index) => {
return this.createPost(post, index);
});
}
......@@ -57,9 +63,9 @@ export class AnnouncementPage extends React.Component<AnnouncementPageProps> {
<DocumentTitle title="Announcements - Amundsen">
<main className="container announcement-container">
<div className="row">
<div className="col-xs-12">
<div className="col-xs-12 col-md-10 col-md-offset-1">
<h1 id="announcement-header" className="h3">
Announcements
{ANNOUNCEMENTS_HEADER_TEXT}
</h1>
<hr />
<div id="announcement-content" className="announcement-content">
......
......@@ -7,7 +7,7 @@ import * as DocumentTitle from 'react-document-title';
import { shallow } from 'enzyme';
import TagsListContainer from 'components/common/Tags';
import { BrowsePage } from '..';
import { BrowsePage } from '.';
describe('BrowsePage', () => {
const setup = () => {
......
......@@ -17,7 +17,7 @@ export class BrowsePage extends React.Component {
<DocumentTitle title={BROWSE_PAGE_DOCUMENT_TITLE}>
<main className="container">
<div className="row">
<div className="col-xs-12">
<div className="col-xs-12 col-md-10 col-md-offset-1">
<TagsListContainer shortTagsList={false} />
</div>
</div>
......
......@@ -9,13 +9,18 @@ import { RouteComponentProps } from 'react-router';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
import { resetSearchState } from 'ducks/search/reducer';
import { UpdateSearchStateReset } from 'ducks/search/types';
import MyBookmarks from 'components/common/Bookmark/MyBookmarks';
import Breadcrumb from 'components/common/Breadcrumb';
import PopularTables from 'components/common/PopularTables';
import { resetSearchState } from 'ducks/search/reducer';
import { UpdateSearchStateReset } from 'ducks/search/types';
import SearchBar from 'components/common/SearchBar';
import TagsListContainer from 'components/common/Tags';
import Announcements from 'components/common/Announcements';
import { announcementsEnabled } from 'config/config-utils';
import { SEARCH_BREADCRUMB_TEXT, HOMEPAGE_TITLE } from './constants';
export interface DispatchFromProps {
......@@ -36,7 +41,11 @@ export class HomePage extends React.Component<HomePageProps> {
return (
<main className="container home-page">
<div className="row">
<div className="col-xs-12 col-md-offset-1 col-md-10">
<div
className={`col-xs-12 ${
announcementsEnabled() ? 'col-md-8' : 'col-md-offset-1 col-md-10'
}`}
>
<h1 className="sr-only">{HOMEPAGE_TITLE}</h1>
<SearchBar />
<div className="filter-breadcrumb pull-right">
......@@ -56,6 +65,11 @@ export class HomePage extends React.Component<HomePageProps> {
<PopularTables />
</div>
</div>
{announcementsEnabled() && (
<div className="col-xs-12 col-md-offset-1 col-md-3">
<Announcements />
</div>
)}
</div>
</main>
);
......
......@@ -15,7 +15,11 @@ import { Dropdown, MenuItem } from 'react-bootstrap';
import { LoggedInUser } from 'interfaces';
import { feedbackEnabled, indexUsersEnabled } from 'config/config-utils';
import {
feedbackEnabled,
indexUsersEnabled,
getNavLinks,
} from 'config/config-utils';
import Feedback from 'components/Feedback';
import SearchBar from 'components/common/SearchBar';
......@@ -101,7 +105,7 @@ export class NavBar extends React.Component<NavBarProps> {
</div>
{this.renderSearchBar()}
<div id="nav-bar-right" className="ml-auto nav-bar-right">
{this.generateNavLinks(AppConfig.navLinks)}
{this.generateNavLinks(getNavLinks())}
{feedbackEnabled() && <Feedback />}
{loggedInUser && indexUsersEnabled() && (
<Dropdown id="user-dropdown" pullRight>
......
......@@ -83,9 +83,10 @@ $avatar-container-size: 40px;
.nav-search-bar {
flex-grow: 1;
margin: auto $spacer-2 auto auto;
.search-bar {
max-width: 560px;
margin: auto 0px auto auto;
margin: auto 0 auto auto;
}
}
......
......@@ -10,7 +10,6 @@ import { Issue } from 'interfaces';
import { getIssues } from 'ducks/issue/reducer';
import { logClick } from 'ducks/utilMethods';
import { GetIssuesRequest } from 'ducks/issue/types';
import LoadingSpinner from 'components/common/LoadingSpinner';
import ReportTableIssue from 'components/TableDetail/ReportTableIssue';
import { NO_DATA_ISSUES_TEXT } from './constants';
import './styles.scss';
......
import * as React from 'react';
import { Link, BrowserRouter } from 'react-router-dom';
import SanitizedHTML from 'react-sanitized-html';
import { mount } from 'enzyme';
import Card from '../../Card';
import AnnouncementsList, { AnnouncementsListProps } from '.';
const TWO_FAKE_ANNOUNCEMENTS = [
{
date: '12/31/1999',
title: 'Y2K',
html_content: '<div>The end of the world</div>',
},
{
date: '01/01/2000',
title: 'False Alarm',
html_content: '<div>Just kidding</div>',
},
];
const FOUR_FAKE_ANNOUNCEMENTS = [
{
date: '12/31/1999',
title: 'Y2K',
html_content: '<div>The end of the world</div>',
},
{
date: '01/01/2000',
title: 'False Alarm',
html_content: '<div>Just kidding</div>',
},
{
date: '12/31/2009',
title: 'Old Test',
html_content: '<div>Old test</div>',
},
{
date: '01/01/2020',
title: 'New Test',
html_content: '<div>New test</div>',
},
];
const EMPTY_ANNOUNCEMENTS = [];
const setup = (propOverrides?: Partial<AnnouncementsListProps>) => {
const props = {
announcements: [],
...propOverrides,
};
const wrapper = mount<typeof AnnouncementsList>(
<BrowserRouter>
<AnnouncementsList {...props} />
</BrowserRouter>
);
return { props, wrapper };
};
describe('AnnouncementsList', () => {
describe('render', () => {
it('renders without issues', () => {
expect(() => {
setup();
}).not.toThrow();
});
it('renders a title', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.announcements-list-title').length;
expect(actual).toEqual(expected);
});
describe('See more link', () => {
it('should render a "See more" link', () => {
const { wrapper } = setup({ announcements: TWO_FAKE_ANNOUNCEMENTS });
const expected = 1;
const actual = wrapper.find('a.announcements-list-more-link').length;
expect(actual).toEqual(expected);
});
it('renders a react router Link', () => {
const { wrapper } = setup({ announcements: TWO_FAKE_ANNOUNCEMENTS });
const expected = TWO_FAKE_ANNOUNCEMENTS.length + 1;
const actual = wrapper.find(Link).length;
expect(actual).toEqual(expected);
});
it('takes users to the announcements page', () => {
const { wrapper } = setup({ announcements: TWO_FAKE_ANNOUNCEMENTS });
const expected = '/announcements';
const actual = wrapper
.find('a.announcements-list-more-link')
.getDOMNode()
.attributes.getNamedItem('href').value;
expect(actual).toEqual(expected);
});
});
describe('announcements list', () => {
it('should render a list container', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.announcements-list').length;
expect(actual).toEqual(expected);
});
describe('when loading', () => {
it('should render three cards', () => {
const { wrapper } = setup({
announcements: EMPTY_ANNOUNCEMENTS,
isLoading: true,
});
const expected = 3;
const actual = wrapper.find(Card).length;
expect(actual).toEqual(expected);
});
it('should render three cards in loading state', () => {
const { wrapper } = setup({
announcements: EMPTY_ANNOUNCEMENTS,
isLoading: true,
});
const expected = 3;
const actual = wrapper.find('.card-shimmer-loader').length;
expect(actual).toEqual(expected);
});
it('should not render the see more link', () => {
const { wrapper } = setup({
announcements: EMPTY_ANNOUNCEMENTS,
isLoading: true,
});
const expected = 0;
const actual = wrapper.find('a.announcements-list-more-link').length;
expect(actual).toEqual(expected);
});
});
describe('when non-empty list of announcements', () => {
it('should render announcements', () => {
const { wrapper } = setup({ announcements: TWO_FAKE_ANNOUNCEMENTS });
const expected = 2;
const actual = wrapper.find('.announcement').length;
expect(actual).toEqual(expected);
});
it('should render announcement cards', () => {
const { wrapper } = setup({ announcements: TWO_FAKE_ANNOUNCEMENTS });
const expected = 2;
const actual = wrapper.find(Card).length;
expect(actual).toEqual(expected);
});
describe('when number of announcements is more than three', () => {
it('should render three announcements', () => {
const { wrapper } = setup({
announcements: FOUR_FAKE_ANNOUNCEMENTS,
});
const expected = 3;
const actual = wrapper.find(Card).length;
expect(actual).toEqual(expected);
});
});
});
describe('when empty list of announcements', () => {
it('should render the empty message', () => {
const { wrapper } = setup({ announcements: EMPTY_ANNOUNCEMENTS });
const expected = 1;
const actual = wrapper.find('.empty-announcement').length;
expect(actual).toEqual(expected);
});
it('should not render the see more link', () => {
const { wrapper } = setup({ announcements: EMPTY_ANNOUNCEMENTS });
const expected = 0;
const actual = wrapper.find('a.announcements-list-more-link').length;
expect(actual).toEqual(expected);
});
});
describe('when error on fetch', () => {
it('should render the error message', () => {
const { wrapper } = setup({
announcements: EMPTY_ANNOUNCEMENTS,
hasError: true,
});
const expected = 1;
const actual = wrapper.find('.error-announcement').length;
expect(actual).toEqual(expected);
});
it('should not render the see more link', () => {
const { wrapper } = setup({
announcements: EMPTY_ANNOUNCEMENTS,
hasError: true,
});
const expected = 0;
const actual = wrapper.find('a.announcements-list-more-link').length;
expect(actual).toEqual(expected);
});
});
});
});
});
import * as React from 'react';
import { Link } from 'react-router-dom';
import SanitizedHTML from 'react-sanitized-html';
import { logClick } from 'ducks/utilMethods';
import { AnnouncementPost } from 'interfaces';
import Card from '../../Card';
import {
MORE_LINK_TEXT,
NO_ANNOUNCEMENTS_TEXT,
ANNOUNCEMENTS_ERROR_TEXT,
HEADER_TEXT,
} from '../constants';
import './styles.scss';
const ANNOUNCEMENT_LIST_THRESHOLD = 3;
const ANNOUNCEMENTS_PAGE_PATH = '/announcements';
export interface AnnouncementsListProps {
announcements: AnnouncementPost[];
hasError?: boolean;
isLoading?: boolean;
}
const getLatestsAnnouncements = (announcements: AnnouncementPost[]) =>
announcements.length > ANNOUNCEMENT_LIST_THRESHOLD
? announcements.splice(announcements.length - ANNOUNCEMENT_LIST_THRESHOLD)
: announcements;
const times = (numItems: number) => new Array(numItems).fill(0);
const AnnouncementItem: React.FC<AnnouncementPost> = ({
date,
title,
html_content,
}: AnnouncementPost) => {
return (
<li className="announcement">
<Card
title={title}
subtitle={date}
href={ANNOUNCEMENTS_PAGE_PATH}
onClick={logClick}
copy={
<SanitizedHTML className="announcement-content" html={html_content} />
}
/>
</li>
);
};
const EmptyAnnouncementItem: React.FC = () => (
<li className="empty-announcement">{NO_ANNOUNCEMENTS_TEXT}</li>
);
const AnnouncementErrorItem: React.FC = () => (
<li className="error-announcement">{ANNOUNCEMENTS_ERROR_TEXT}</li>
);
const AnnouncementsList: React.FC<AnnouncementsListProps> = ({
announcements,
hasError,
isLoading,
}: AnnouncementsListProps) => {
const isEmpty = announcements.length === 0;
let listContent = null;
if (isEmpty) {
listContent = <EmptyAnnouncementItem />;
}
if (announcements.length > 0) {
listContent = getLatestsAnnouncements(
announcements
).map(({ date, title, html_content }) => (
<AnnouncementItem
key={`key:${date}`}
date={date}
title={title}
html_content={html_content}
/>
));
}
if (hasError) {
listContent = <AnnouncementErrorItem />;
}
if (isLoading) {
listContent = times(3).map((_, index) => (
<li className="announcement" key={`key:${index}`}>
<Card isLoading />
</li>
));
}
return (
<article className="announcements-list-container">
<h2 className="announcements-list-title">{HEADER_TEXT}</h2>
<ul className="announcements-list">{listContent}</ul>
{!isEmpty && (
<Link
to={ANNOUNCEMENTS_PAGE_PATH}
className="announcements-list-more-link"
onClick={logClick}
>
{MORE_LINK_TEXT}
</Link>
)}
</article>
);
};
export default AnnouncementsList;
@import 'variables';
@import 'typography-default';
$more-link-size: 18px;
$more-link-line-height: 21px;
$message-size: 14px;
$message-line-height: 16px;
$message-border-size: 1px;
.announcements-list-title {
@extend %text-title-w1;
}
.announcements-list {
list-style: none;
margin: $spacer-1 0 0 0;
padding: 0;
.announcement {
margin-bottom: -1px;
}
.announcement-content p {
margin: 0;
}
}
.announcements-list-more-link {
font-size: $more-link-size;
line-height: $more-link-line-height;
margin-top: 12px;
display: block;
}
.empty-announcement,
.error-announcement {
font-size: $message-size;
line-height: $message-line-height;
color: $text-primary;
text-align: center;
border-top: $message-border-size solid $gray20;
padding-top: $spacer-3;
}
export const HEADER_TEXT = 'Announcements';
export const MORE_LINK_TEXT = 'See more';
export const NO_ANNOUNCEMENTS_TEXT = 'No announcements to show';
export const ANNOUNCEMENTS_ERROR_TEXT = 'Could not fetch announcements';
import * as React from 'react';
import { shallow } from 'enzyme';
import globalState from 'fixtures/globalState';
import { ResourceType } from 'interfaces';
import { mapDispatchToProps, mapStateToProps } from '.';
describe('AnnouncementsListContainer', () => {
describe('mapDispatchToProps', () => {
let dispatch;
let props;
beforeAll(() => {
dispatch = jest.fn(() => Promise.resolve());
props = mapDispatchToProps(dispatch);
});
describe('announcementsGet', () => {
it('sets announcementsGet on the props', () => {
const expected = 'function';
const actual = typeof props.announcementsGet;
expect(actual).toEqual(expected);
});
it('should request the announcements', () => {
const expected = { type: 'amundsen/announcements/GET_REQUEST' };
props.announcementsGet();
expect(dispatch.mock.calls[0][0]).toEqual(expected);
});
});
});
describe('mapStateToProps', () => {
it('sets isLoading on the props', () => {
const expected = globalState.announcements.isLoading;
const actual = mapStateToProps(globalState).isLoading;
expect(actual).toEqual(expected);
});
it('sets statusCode on the props', () => {
const expected = globalState.announcements.statusCode;
const actual = mapStateToProps(globalState).statusCode;
expect(actual).toEqual(expected);
});
it('sets posts on the props', () => {
const expected = globalState.announcements.posts;
const actual = mapStateToProps(globalState).announcements;
expect(actual).toEqual(expected);
});
});
});
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { GlobalState } from 'ducks/rootReducer';
import { GetAnnouncementsRequest } from 'ducks/announcements/types';
import { getAnnouncements } from 'ducks/announcements';
import { AnnouncementPost } from 'interfaces';
import AnnouncementsList from './AnnouncementsList';
export interface StateFromProps {
isLoading: boolean;
statusCode: number;
announcements: AnnouncementPost[];
}
export interface DispatchFromProps {
announcementsGet: () => GetAnnouncementsRequest;
}
export type AnnouncementContainerProps = StateFromProps & DispatchFromProps;
const OK_STATUS_CODE = 200;
const AnnouncementsListContainer: React.FC<AnnouncementContainerProps> = ({
announcements,
announcementsGet,
isLoading,
statusCode,
}: AnnouncementContainerProps) => {
React.useEffect(() => {
announcementsGet();
}, []);
return (
<AnnouncementsList
hasError={statusCode !== OK_STATUS_CODE}
isLoading={isLoading}
announcements={announcements}
/>
);
};
export const mapStateToProps = (state: GlobalState) => {
return {
announcements: state.announcements.posts,
isLoading: state.announcements.isLoading,
statusCode: state.announcements.statusCode,
};
};
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ announcementsGet: getAnnouncements }, dispatch);
};
export default connect<StateFromProps, DispatchFromProps>(
mapStateToProps,
mapDispatchToProps
)(AnnouncementsListContainer);
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
@import 'variables';
.clickable-badge {
&:hover {
cursor: pointer;
}
.label {
padding: 0 0;
}
......@@ -29,37 +31,45 @@
outline-offset: -2px;
}
}
.badge-overlay-negative,
.badge-overlay-neutral,
.badge-overlay-positive,
.badge-overlay-primary {
&:hover,
&:focus {
background-color: rgba(
$color: $badge-overlay,
$alpha: $badge-opacity-light
);
background-color:
rgba(
$color: $badge-overlay,
$alpha: $badge-opacity-light
);
}
&:active {
background-color: rgba(
$color: $badge-overlay,
$alpha: $badge-pressed-light
);
background-color:
rgba(
$color: $badge-overlay,
$alpha: $badge-pressed-light
);
}
}
.badge-overlay-warning {
&:hover,
&:focus {
background-color: rgba(
$color: $badge-overlay,
$alpha: $badge-opacity-dark
);
background-color:
rgba(
$color: $badge-overlay,
$alpha: $badge-opacity-dark
);
}
&:active {
background-color: rgba(
$color: $badge-overlay,
$alpha: $badge-pressed-dark
);
background-color:
rgba(
$color: $badge-overlay,
$alpha: $badge-pressed-dark
);
}
}
}
import * as React from 'react';
import { Link, BrowserRouter } from 'react-router-dom';
import { mount } from 'enzyme';
import Card, { CardProps } from '.';
const setup = (propOverrides?: Partial<CardProps>) => {
const props = {
...propOverrides,
};
const wrapper = mount<typeof Card>(
<BrowserRouter>
<Card {...props} />
</BrowserRouter>
);
return { props, wrapper };
};
describe('Card', () => {
describe('render', () => {
it('renders without issues', () => {
expect(() => {
setup();
}).not.toThrow();
});
it('renders the main container', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.card').length;
expect(actual).toEqual(expected);
});
describe('header', () => {
it('renders a header section', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.card-header').length;
expect(actual).toEqual(expected);
});
describe('subtitle', () => {
it('renders a title if passed', () => {
const { wrapper } = setup({ title: 'test title' });
const expected = 1;
const actual = wrapper.find('.card-title').length;
expect(actual).toEqual(expected);
});
it('does not render a title if missing', () => {
const { wrapper } = setup();
const expected = 0;
const actual = wrapper.find('.card-title').length;
expect(actual).toEqual(expected);
});
});
describe('subtitle', () => {
it('renders a subtitle if passed', () => {
const { wrapper } = setup({ subtitle: 'test subtitle' });
const expected = 1;
const actual = wrapper.find('.card-subtitle').length;
expect(actual).toEqual(expected);
});
it('does not render a subtitle if missing', () => {
const { wrapper } = setup();
const expected = 0;
const actual = wrapper.find('.card-subtitle').length;
expect(actual).toEqual(expected);
});
});
});
describe('body', () => {
it('renders a body section', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.card-body').length;
expect(actual).toEqual(expected);
});
describe('copy', () => {
it('renders a copy if passed', () => {
const { wrapper } = setup({ copy: 'test copy' });
const expected = 1;
const actual = wrapper.find('.card-copy').length;
expect(actual).toEqual(expected);
});
it('does not render a copy if missing', () => {
const { wrapper } = setup();
const expected = 0;
const actual = wrapper.find('.card-copy').length;
expect(actual).toEqual(expected);
});
});
});
describe('when is loading', () => {
it('holds a loading state', () => {
const { wrapper } = setup({ isLoading: true });
const expected = 1;
const actual = wrapper.find('.card.is-loading').length;
expect(actual).toEqual(expected);
});
it('renders a shimmer loader', () => {
const { wrapper } = setup({ isLoading: true });
const expected = 1;
const actual = wrapper.find('.card-shimmer-loader').length;
expect(actual).toEqual(expected);
});
it('renders five rows of line loaders', () => {
const { wrapper } = setup({ isLoading: true });
const expected = 5;
const actual = wrapper.find('.card-shimmer-row').length;
expect(actual).toEqual(expected);
});
});
describe('when an href is passed', () => {
it('should render a link', () => {
const testPath = 'fakePath';
const { wrapper } = setup({ href: testPath });
const expected = 1;
const actual = wrapper.find('a.card').length;
expect(actual).toEqual(expected);
});
it('renders a react router Link', () => {
const testPath = 'fakePath';
const { wrapper } = setup({ href: testPath });
const expected = 1;
const actual = wrapper.find(Link).length;
expect(actual).toEqual(expected);
});
it('sets the link to the passed href', () => {
const testPath = 'fakePath';
const { wrapper } = setup({ href: testPath });
const expected = '/' + testPath;
const actual = wrapper
.find('a.card')
.getDOMNode()
.attributes.getNamedItem('href').value;
expect(actual).toEqual(expected);
});
});
});
describe('lifetime', () => {
describe('when clicking on an interactive card', () => {
it('should call the onClick handler', () => {
const clickSpy = jest.fn();
const { wrapper } = setup({
onClick: clickSpy,
href: 'testPath',
});
const expected = 1;
wrapper.find(Link).simulate('click');
const actual = clickSpy.mock.calls.length;
expect(actual).toEqual(expected);
});
});
});
});
import * as React from 'react';
import { Link } from 'react-router-dom';
import './styles.scss';
export interface CardProps {
title?: string;
subtitle?: string;
copy?: string | JSX.Element;
isLoading?: boolean;
href?: string;
onClick?: (e: React.SyntheticEvent) => void;
}
const CardShimmerLoader: React.FC = () => (
<div className="card-shimmer-loader">
<div className="card-shimmer-row shimmer-row-line--1 is-shimmer-animated" />
<div className="card-shimmer-row shimmer-row-line--2 is-shimmer-animated" />
<div className="card-shimmer-row shimmer-row-line--3 is-shimmer-animated" />
<div className="card-shimmer-loader-body">
<div className="card-shimmer-row shimmer-row-line--4 is-shimmer-animated" />
<div className="card-shimmer-row shimmer-row-line--5 is-shimmer-animated" />
</div>
</div>
);
const Card: React.FC<CardProps> = ({
href,
title,
subtitle,
copy,
onClick = null,
isLoading = false,
}: CardProps) => {
let card;
let cardContent = (
<>
<header className="card-header">
{title && <h2 className="card-title">{title}</h2>}
{subtitle && <h3 className="card-subtitle">{subtitle}</h3>}
</header>
<div className="card-body">
{copy && <div className="card-copy">{copy}</div>}
</div>
</>
);
if (isLoading) {
cardContent = <CardShimmerLoader />;
}
if (href) {
card = (
<Link
className={`card is-link ${isLoading ? 'is-loading' : ''}`}
to={href}
onClick={onClick}
>
{cardContent}
</Link>
);
} else {
card = (
<article className={`card ${isLoading ? 'is-loading' : ''}`}>
{cardContent}
</article>
);
}
return <>{card}</>;
};
export default Card;
@import 'variables';
@import 'typography-default';
$shimmer-loader-items: 1, 2, 3, 4, 5;
$shimmer-loader-row-height: 16px;
$shimmer-loader-row-min-width: 90;
$shimmer-loader-row-max-width: 230;
$card-height: 180px;
$card-header-height: 60px;
$card-border-size: 1px;
$card-focus-border-size: 2px;
$card-title-max-lines: 2;
$card-copy-max-lines: 3;
.card {
display: block;
padding: $spacer-3;
border-top: $card-border-size solid $gray20;
border-bottom: $card-border-size solid $gray20;
height: $card-height;
&.is-link {
&:focus {
text-decoration: none;
border: $card-focus-border-size solid $blue80;
border-radius: $spacer-1/2;
outline-offset: 0;
}
&:hover,
&:active {
text-decoration: none;
box-shadow: $elevation-level2;
border: 0;
}
}
}
.card-header {
height: $card-header-height;
}
.card-title {
@extend %text-title-w2;
color: $text-primary;
@include truncate($w2-font-size, $w2-line-height, $card-title-max-lines);
}
.card-subtitle {
@extend %text-body-w3;
color: $text-secondary;
}
.card-copy {
@extend %text-body-w3;
color: $text-primary;
margin: 0;
@include truncate($w3-font-size, $w3-line-height, $card-copy-max-lines);
}
.card-body {
padding-top: $spacer-2;
}
// Shimmer Loader
.card-shimmer-loader {
width: 100%;
}
.card-shimmer-row {
height: $shimmer-loader-row-height;
width: $shimmer-loader-row-min-width + px;
margin-bottom: $spacer-1;
&:last-child {
margin-bottom: 0;
}
}
@each $line in $shimmer-loader-items {
.shimmer-row-line--#{$line} {
width: (
random($shimmer-loader-row-max-width - $shimmer-loader-row-min-width) +
$shimmer-loader-row-min-width
) +
px;
}
}
.card-shimmer-loader-body {
margin-top: $spacer-4;
}
......@@ -21,9 +21,10 @@ $shimmer-loader-tag-min-width: 50;
@each $item in $shimmer-loader-items {
.shimmer-tag-loader-item--#{$item} {
width: (
width:
(
random($shimmer-loader-tag-max-width - $shimmer-loader-tag-min-width) +
$shimmer-loader-tag-min-width
$shimmer-loader-tag-min-width
) +
px;
}
......
......@@ -37,6 +37,9 @@ const configDefault: AppConfig = {
feedbackEnabled: false,
notificationsEnabled: false,
},
announcements: {
enabled: true,
},
navLinks: [
{
label: 'Announcements',
......
......@@ -18,6 +18,7 @@ export interface AppConfig {
issueTracking: IssueTrackingConfig;
logoPath: string | null;
mailClientFeatures: MailClientFeaturesConfig;
announcements: AnnoucementsFeaturesConfig;
navLinks: Array<LinkConfig>;
resourceConfig: ResourceConfig;
tableLineage: TableLineageConfig;
......@@ -188,6 +189,16 @@ interface MailClientFeaturesConfig {
feedbackEnabled: boolean;
notificationsEnabled: boolean;
}
/**
* AnnoucementsFeaturesConfig - Enable/disable UI features related to the announcements
*
* enabled - Enables the announcements feature
*/
interface AnnoucementsFeaturesConfig {
enabled: boolean;
}
/**
* TableProfileConfig - Customize the "Table Profile" section of the "Table Details" page.
*
......
......@@ -2,12 +2,13 @@ import AppConfig from 'config/config';
import { BadgeStyleConfig, BadgeStyle } from 'config/config-types';
import { TableMetadata } from 'interfaces/TableMetadata';
import { FilterConfig } from './config-types';
import { FilterConfig, LinkConfig } from './config-types';
import { ResourceType } from '../interfaces';
export const DEFAULT_DATABASE_ICON_CLASS = 'icon-database icon-color';
export const DEFAULT_DASHBOARD_ICON_CLASS = 'icon-dashboard icon-color';
const ANNOUNCEMENTS_LINK_LABEL = 'Announcements';
/**
* Returns the display name for a given source id for a given resource type.
......@@ -96,6 +97,13 @@ export function feedbackEnabled(): boolean {
return AppConfig.mailClientFeatures.feedbackEnabled;
}
/**
* Returns whether or not feedback features should be enabled
*/
export function announcementsEnabled(): boolean {
return AppConfig.announcements.enabled;
}
/**
* Returns whether or not dashboard features should be shown
*/
......@@ -138,6 +146,25 @@ export function getCuratedTags(): string[] {
return AppConfig.browse.curatedTags;
}
/**
* Checks if nav links are active
*/
const isNavLinkActive = (link: LinkConfig): boolean => {
if (!announcementsEnabled()) {
return link.label !== ANNOUNCEMENTS_LINK_LABEL;
}
return true;
};
/*
* Returns the updated list of navigation links given the other
* configuration options state
*/
export function getNavLinks(): LinkConfig[] {
return AppConfig.navLinks.filter(isNavLinkActive);
}
/**
* Returns whether to enable the table `explore` feature
*/
......
......@@ -105,6 +105,51 @@ describe('getBadgeConfig', () => {
});
});
describe('getNavLinks', () => {
const testNavLinks = [
{
label: 'TestLabel1',
id: 'nav::testPage1',
href: '/testPage1',
use_router: true,
},
{
label: 'TestLabel2',
id: 'nav::testPage2',
href: '/testPage2',
use_router: true,
},
];
AppConfig.navLinks = [
...testNavLinks,
{
label: 'Announcements',
id: 'nav::announcements',
href: '/announcements',
use_router: true,
},
];
describe('when announcements is active', () => {
it('returns all the navLinks', () => {
const actual = ConfigUtils.getNavLinks();
const expected = AppConfig.navLinks;
expect(actual).toEqual(expected);
});
});
describe('when announcements is deactivated', () => {
it('returns all the navLinks but the announcements', () => {
AppConfig.announcements.enabled = false;
const actual = ConfigUtils.getNavLinks();
const expected = testNavLinks;
expect(actual).toEqual(expected);
});
});
});
describe('feedbackEnabled', () => {
it('returns whether or not the feaadback feature is enabled', () => {
expect(ConfigUtils.feedbackEnabled()).toBe(
......@@ -113,6 +158,14 @@ describe('feedbackEnabled', () => {
});
});
describe('announcementsEnabled', () => {
it('returns whether or not the announcements feature is enabled', () => {
expect(ConfigUtils.announcementsEnabled()).toBe(
AppConfig.announcements.enabled
);
});
});
describe('issueTrackingEnabled', () => {
it('returns whether or not the issueTracking feature is enabled', () => {
expect(ConfigUtils.issueTrackingEnabled()).toBe(
......
import axios, { AxiosResponse } from 'axios';
import { AnnouncementPost } from 'interfaces';
import * as API from './v0';
jest.mock('axios');
describe('getAnnouncements', () => {
let expectedPosts: AnnouncementPost[];
let mockResponse: AxiosResponse<API.AnnouncementsAPI>;
beforeAll(() => {
expectedPosts = [
{
date: '12/31/1999',
title: 'Test',
html_content: '<div>Test content</div>',
},
];
});
describe('when success', () => {
it('resolves with array of posts and status code', async () => {
expect.assertions(1);
mockResponse = {
data: {
posts: expectedPosts,
msg: 'Success',
},
status: 200,
statusText: '',
headers: {},
config: {},
};
// @ts-ignore: TypeScript errors on Jest mock methods unless we extend AxiosStatic for tests
axios.mockResolvedValue(mockResponse);
const expected = {
posts: expectedPosts,
statusCode: mockResponse.status,
};
await API.getAnnouncements().then((response) => {
expect(response).toEqual(expected);
});
});
});
describe('when error', () => {
it('catches error and resolves with object containing error code', async () => {
expect.assertions(1);
mockResponse = {
data: {
posts: [],
msg: 'A client for retrieving announcements must be configured',
},
status: 500,
statusText: '',
headers: {},
config: {},
};
// @ts-ignore: TypeScript errors on Jest mock methods unless we extend AxiosStatic for tests
axios.mockRejectedValue(mockResponse);
const expected = {
posts: [],
statusCode: mockResponse.status,
};
await API.getAnnouncements().catch((response) => {
expect(response).toEqual(expected);
});
});
});
afterAll(() => {
// @ts-ignore: TypeScript errors on Jest mock methods unless we extend AxiosStatic for tests
axios.mockClear();
});
});
import axios, { AxiosResponse } from 'axios';
import { AnnouncementPost } from 'interfaces';
import * as API from '../v0';
jest.mock('axios');
describe('getAnnouncements', () => {
let expectedPosts: AnnouncementPost[];
let mockResponse: AxiosResponse<API.AnnouncementsAPI>;
beforeAll(() => {
expectedPosts = [
{
date: '12/31/1999',
title: 'Test',
html_content: '<div>Test content</div>',
},
];
mockResponse = {
data: {
posts: expectedPosts,
msg: 'Success',
},
status: 200,
statusText: '',
headers: {},
config: {},
};
// @ts-ignore: TypeScript errors on Jest mock methods unless we extend AxiosStatic for tests
axios.mockResolvedValue(mockResponse);
});
it('resolves with array of posts from response.data on success', async () => {
expect.assertions(1);
await API.getAnnouncements().then((posts) => {
expect(posts).toEqual(expectedPosts);
});
});
afterAll(() => {
// @ts-ignore: TypeScript errors on Jest mock methods unless we extend AxiosStatic for tests
axios.mockClear();
});
});
......@@ -7,11 +7,30 @@ export type AnnouncementsAPI = {
posts: AnnouncementPost[];
};
const SERVER_ERROR_CODE = 500;
export function getAnnouncements() {
return axios({
method: 'get',
url: '/api/announcements/v0/',
}).then((response: AxiosResponse<AnnouncementsAPI>) => {
return response.data.posts;
});
})
.then((response: AxiosResponse<AnnouncementsAPI>) => {
const { data, status } = response;
return {
posts: data.posts,
statusCode: status,
};
})
.catch((e) => {
const { response } = e;
const statusCode = response
? response.status || SERVER_ERROR_CODE
: SERVER_ERROR_CODE;
return Promise.reject({
posts: [],
statusCode,
});
});
}
import { expectSaga, testSaga } from 'redux-saga-test-plan';
import * as matchers from 'redux-saga-test-plan/matchers';
import { throwError } from 'redux-saga-test-plan/providers';
import { testSaga } from 'redux-saga-test-plan';
import * as API from '../api/v0';
import * as API from './api/v0';
import reducer, {
getAnnouncements,
getAnnouncementsFailure,
getAnnouncementsSuccess,
initialState,
AnnouncementsReducerState,
} from '../reducer';
import { getAnnouncementsWatcher, getAnnouncementsWorker } from '../sagas';
import { GetAnnouncements } from '../types';
} from '.';
import { getAnnouncementsWatcher, getAnnouncementsWorker } from './sagas';
import { GetAnnouncements } from './types';
describe('announcements ducks', () => {
const SERVER_ERROR_CODE = 500;
describe('Announcements ducks', () => {
describe('actions', () => {
it('getAnnouncements - returns the action to get all tags', () => {
const action = getAnnouncements();
......@@ -21,10 +21,16 @@ describe('announcements ducks', () => {
});
it('getAnnouncementsFailure - returns the action to process failure', () => {
const action = getAnnouncementsFailure();
const expectedPayload = {
statusCode: SERVER_ERROR_CODE,
posts: [],
};
const action = getAnnouncementsFailure(expectedPayload);
const { payload } = action;
expect(action.type).toBe(GetAnnouncements.FAILURE);
expect(payload.posts).toEqual([]);
expect(payload.statusCode).toEqual(SERVER_ERROR_CODE);
});
it('getAllTagsSuccess - returns the action to process success', () => {
......@@ -35,8 +41,14 @@ describe('announcements ducks', () => {
html_content: '<div>Test content</div>',
},
];
const action = getAnnouncementsSuccess(expectedPosts);
const expectedPayload = {
posts: expectedPosts,
statusCode: 200,
};
const action = getAnnouncementsSuccess(expectedPayload);
const { payload } = action;
expect(action.type).toBe(GetAnnouncements.SUCCESS);
expect(payload.posts).toBe(expectedPosts);
});
......@@ -44,34 +56,75 @@ describe('announcements ducks', () => {
describe('reducer', () => {
let testState: AnnouncementsReducerState;
beforeAll(() => {
testState = {
isLoading: false,
statusCode: 200,
posts: [],
};
});
it('should return the existing state if action is not handled', () => {
expect(reducer(testState, { type: 'INVALID.ACTION' })).toEqual(testState);
describe('when action is not handled', () => {
it('should return the existing state', () => {
const expected = testState;
const actual = reducer(testState, { type: 'INVALID.ACTION' });
expect(actual).toEqual(expected);
});
});
it('should handle GetAnnouncements.SUCCESS', () => {
const expectedPosts = [
{
date: '12/31/1999',
title: 'Test',
html_content: '<div>Test content</div>',
},
];
expect(
reducer(testState, getAnnouncementsSuccess(expectedPosts))
).toEqual({
posts: expectedPosts,
describe('when action is REQUEST', () => {
it('should handle GetAnnouncements.REQUEST', () => {
const expected = {
...testState,
isLoading: true,
statusCode: null,
};
const actual = reducer(testState, getAnnouncements());
expect(actual).toEqual(expected);
});
});
it('should return the initialState if GetAnnouncements.FAILURE', () => {
expect(reducer(testState, getAnnouncementsFailure())).toEqual(
initialState
);
describe('when action is SUCCESS', () => {
it('should handle GetAnnouncements.SUCCESS', () => {
const expectedPosts = [
{
date: '12/31/1999',
title: 'Test',
html_content: '<div>Test content</div>',
},
];
const payload = {
posts: expectedPosts,
statusCode: 200,
};
const expected = {
isLoading: false,
statusCode: 200,
posts: expectedPosts,
};
const actual = reducer(testState, getAnnouncementsSuccess(payload));
expect(actual).toEqual(expected);
});
});
describe('when action is FAILURE', () => {
it('should return the initialState with the status code', () => {
const expected = {
...initialState,
isLoading: false,
statusCode: SERVER_ERROR_CODE,
};
const actual = reducer(
testState,
getAnnouncementsFailure({ statusCode: SERVER_ERROR_CODE })
);
expect(actual).toEqual(expected);
});
});
});
......@@ -87,27 +140,41 @@ describe('announcements ducks', () => {
});
describe('getAnnouncementsWorker', () => {
it('gets posts', () => {
const mockPosts = [
{
date: '12/31/1999',
title: 'Test',
html_content: '<div>Test content</div>',
},
];
return expectSaga(getAnnouncementsWorker)
.provide([[matchers.call.fn(API.getAnnouncements), mockPosts]])
.put(getAnnouncementsSuccess(mockPosts))
.run();
it('executes flow for successfuly getting announcements', () => {
const mockResponse = {
posts: [
{
date: '12/31/1999',
title: 'Test',
html_content: '<div>Test content</div>',
},
],
statusCode: 200,
};
testSaga(getAnnouncementsWorker)
.next()
.call(API.getAnnouncements)
.next(mockResponse)
.put(getAnnouncementsSuccess(mockResponse))
.next()
.isDone();
});
it('handles request error', () => {
return expectSaga(getAnnouncementsWorker)
.provide([
[matchers.call.fn(API.getAnnouncements), throwError(new Error())],
])
.put(getAnnouncementsFailure())
.run();
it('executes flow for a failed request', () => {
const mockResponse = {
statusCode: SERVER_ERROR_CODE,
statusMessage: 'Error',
};
testSaga(getAnnouncementsWorker)
.next()
.call(API.getAnnouncements)
// @ts-ignore
.throw(mockResponse)
.put(getAnnouncementsFailure(mockResponse))
.next()
.isDone();
});
});
});
......
......@@ -4,28 +4,38 @@ import {
GetAnnouncements,
GetAnnouncementsRequest,
GetAnnouncementsResponse,
GetAnnouncementsPayload,
} from './types';
/* ACTIONS */
export function getAnnouncements(): GetAnnouncementsRequest {
return { type: GetAnnouncements.REQUEST };
}
export function getAnnouncementsFailure(): GetAnnouncementsResponse {
return { type: GetAnnouncements.FAILURE, payload: { posts: [] } };
export function getAnnouncementsFailure(
payload: GetAnnouncementsPayload
): GetAnnouncementsResponse {
return { type: GetAnnouncements.FAILURE, payload };
}
export function getAnnouncementsSuccess(
posts: AnnouncementPost[]
payload: GetAnnouncementsPayload
): GetAnnouncementsResponse {
return { type: GetAnnouncements.SUCCESS, payload: { posts } };
return {
type: GetAnnouncements.SUCCESS,
payload,
};
}
/* REDUCER */
export interface AnnouncementsReducerState {
posts: AnnouncementPost[];
isLoading: boolean;
statusCode: number;
}
export const initialState: AnnouncementsReducerState = {
posts: [],
isLoading: true,
statusCode: null,
};
export default function reducer(
......@@ -33,10 +43,25 @@ export default function reducer(
action
): AnnouncementsReducerState {
switch (action.type) {
case GetAnnouncements.REQUEST:
return {
...state,
isLoading: true,
statusCode: null,
};
case GetAnnouncements.FAILURE:
return initialState;
return {
...state,
isLoading: false,
statusCode: action.payload.statusCode,
};
case GetAnnouncements.SUCCESS:
return { posts: (<GetAnnouncementsResponse>action).payload.posts };
return {
...state,
isLoading: false,
statusCode: action.payload.statusCode,
posts: (<GetAnnouncementsResponse>action).payload.posts,
};
default:
return state;
}
......
......@@ -2,15 +2,16 @@ import { SagaIterator } from 'redux-saga';
import { call, put, takeEvery } from 'redux-saga/effects';
import * as API from './api/v0';
import { getAnnouncementsFailure, getAnnouncementsSuccess } from './reducer';
import { getAnnouncementsFailure, getAnnouncementsSuccess } from '.';
import { GetAnnouncements } from './types';
export function* getAnnouncementsWorker(): SagaIterator {
try {
const posts = yield call(API.getAnnouncements);
yield put(getAnnouncementsSuccess(posts));
} catch (e) {
yield put(getAnnouncementsFailure());
const response = yield call(API.getAnnouncements);
yield put(getAnnouncementsSuccess(response));
} catch (error) {
yield put(getAnnouncementsFailure(error));
}
}
export function* getAnnouncementsWatcher(): SagaIterator {
......
......@@ -5,12 +5,17 @@ export enum GetAnnouncements {
SUCCESS = 'amundsen/announcements/GET_SUCCESS',
FAILURE = 'amundsen/announcements/GET_FAILURE',
}
export interface GetAnnouncementsRequest {
type: GetAnnouncements.REQUEST;
}
export interface GetAnnouncementsResponse {
type: GetAnnouncements.SUCCESS | GetAnnouncements.FAILURE;
payload: {
posts: AnnouncementPost[];
};
payload: GetAnnouncementsPayload;
}
export interface GetAnnouncementsPayload {
posts?: AnnouncementPost[];
statusCode?: number;
}
......@@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios';
import { dashboardMetadata } from 'fixtures/metadata/dashboard';
import * as API from '../api/v0';
import * as API from './v0';
jest.mock('axios');
......
......@@ -5,7 +5,7 @@ import reducer, {
getDashboardSuccess,
initialDashboardState,
DashboardReducerState,
} from '../reducer';
} from './reducer';
describe('dashboard reducer', () => {
let testState: DashboardReducerState;
......
import { testSaga } from 'redux-saga-test-plan';
import { dashboardMetadata } from 'fixtures/metadata/dashboard';
import * as API from '../api/v0';
import * as Sagas from '../sagas';
import * as API from './api/v0';
import * as Sagas from './sagas';
import {
getDashboard,
getDashboardFailure,
getDashboardSuccess,
} from '../reducer';
import { GetDashboard } from '../types';
} from './reducer';
import { GetDashboard } from './types';
describe('dashboard sagas', () => {
describe('getDashboardWatcher', () => {
......
......@@ -9,6 +9,7 @@ export function* getDashboardWorker(action): SagaIterator {
try {
const { uri, searchIndex, source } = action.payload;
const response = yield call(API.getDashboard, uri, searchIndex, source);
yield put(getDashboardSuccess(response));
} catch (error) {
yield put(getDashboardFailure(error));
......
import { combineReducers } from 'redux';
import dashboard, { DashboardReducerState } from 'ducks/dashboard/reducer';
import announcements, {
AnnouncementsReducerState,
} from './announcements/reducer';
import announcements, { AnnouncementsReducerState } from './announcements';
import feedback, { FeedbackReducerState } from './feedback/reducer';
import popularTables, {
PopularTablesReducerState,
......
......@@ -7,6 +7,8 @@ import { dashboardMetadata } from './metadata/dashboard';
const globalState: GlobalState = {
announcements: {
isLoading: false,
statusCode: 200,
posts: [
{
date: '12/31/1999',
......
......@@ -4105,6 +4105,24 @@
}
}
},
"@babel/runtime-corejs3": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz",
"integrity": "sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A==",
"dev": true,
"requires": {
"core-js-pure": "^3.0.0",
"regenerator-runtime": "^0.13.4"
},
"dependencies": {
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
}
}
},
"@babel/standalone": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.11.2.tgz",
......@@ -9195,16 +9213,6 @@
"sprintf-js": "~1.0.2"
}
},
"aria-query": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz",
"integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=",
"dev": true,
"requires": {
"ast-types-flow": "0.0.7",
"commander": "^2.11.0"
}
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
......@@ -9929,6 +9937,12 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"dev": true
},
"axe-core": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.5.5.tgz",
"integrity": "sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==",
"dev": true
},
"axios": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
......@@ -11606,6 +11620,12 @@
"restore-cursor": "^3.1.0"
}
},
"cli-spinners": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.4.0.tgz",
"integrity": "sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==",
"dev": true
},
"cli-table3": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz",
......@@ -11764,6 +11784,12 @@
}
}
},
"clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
"dev": true
},
"clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
......@@ -12660,6 +12686,15 @@
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
},
"defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
"dev": true,
"requires": {
"clone": "^1.0.2"
}
},
"define-properties": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz",
......@@ -13998,22 +14033,49 @@
}
},
"eslint-plugin-jsx-a11y": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz",
"integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz",
"integrity": "sha512-i1S+P+c3HOlBJzMFORRbC58tHa65Kbo8b52/TwCwSKLohwvpfT5rm2GjGWzOHTEuq4xxf2aRlHHTtmExDQOP+g==",
"dev": true,
"requires": {
"@babel/runtime": "^7.4.5",
"aria-query": "^3.0.0",
"array-includes": "^3.0.3",
"@babel/runtime": "^7.10.2",
"aria-query": "^4.2.2",
"array-includes": "^3.1.1",
"ast-types-flow": "^0.0.7",
"axobject-query": "^2.0.2",
"damerau-levenshtein": "^1.0.4",
"emoji-regex": "^7.0.2",
"axe-core": "^3.5.4",
"axobject-query": "^2.1.2",
"damerau-levenshtein": "^1.0.6",
"emoji-regex": "^9.0.0",
"has": "^1.0.3",
"jsx-ast-utils": "^2.2.1"
"jsx-ast-utils": "^2.4.1",
"language-tags": "^1.0.5"
},
"dependencies": {
"@babel/runtime": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
"integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"aria-query": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
"integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.10.2",
"@babel/runtime-corejs3": "^7.10.2"
}
},
"emoji-regex": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.0.0.tgz",
"integrity": "sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w==",
"dev": true
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
......@@ -14022,6 +14084,22 @@
"requires": {
"function-bind": "^1.1.1"
}
},
"jsx-ast-utils": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz",
"integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==",
"dev": true,
"requires": {
"array-includes": "^3.1.1",
"object.assign": "^4.1.0"
}
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
}
}
},
......@@ -15134,6 +15212,99 @@
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-unused-sass-variables": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-unused-sass-variables/-/find-unused-sass-variables-2.0.0.tgz",
"integrity": "sha512-P9QHY8AUkREpnAwCgzUysQJ5Z+Uf9NR3wpaeVN1886nNPXGxy4tgapdVzieYX9ISMi5akJGEKr0hrpzFcFMLQA==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
"commander": "^5.1.0",
"escape-string-regexp": "^4.0.0",
"glob": "^7.1.6",
"ora": "^4.0.4",
"postcss": "^7.0.27",
"postcss-scss": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"dev": true
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"supports-color": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
......@@ -17216,6 +17387,12 @@
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="
},
"is-interactive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
"dev": true
},
"is-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz",
......@@ -22386,16 +22563,6 @@
"verror": "1.10.0"
}
},
"jsx-ast-utils": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz",
"integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==",
"dev": true,
"requires": {
"array-includes": "^3.0.3",
"object.assign": "^4.1.0"
}
},
"keycode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",
......@@ -22428,6 +22595,21 @@
"integrity": "sha512-eYboRV94Vco725nKMlpkn3nV2+96p9c3gKXRsYqAJSswSENvBhN7n5L+uDhY58xQa0UukWsDMTGELzmD8Q+wTA==",
"dev": true
},
"language-subtag-registry": {
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz",
"integrity": "sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg==",
"dev": true
},
"language-tags": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz",
"integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=",
"dev": true,
"requires": {
"language-subtag-registry": "~0.3.2"
}
},
"lazy-cache": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
......@@ -24927,6 +25109,150 @@
}
}
},
"ora": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-4.1.1.tgz",
"integrity": "sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==",
"dev": true,
"requires": {
"chalk": "^3.0.0",
"cli-cursor": "^3.1.0",
"cli-spinners": "^2.2.0",
"is-interactive": "^1.0.0",
"log-symbols": "^3.0.0",
"mute-stream": "0.0.8",
"strip-ansi": "^6.0.0",
"wcwidth": "^1.0.1"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"log-symbols": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
"integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
"dev": true,
"requires": {
"chalk": "^2.4.2"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
},
"supports-color": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"original": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
......@@ -32861,6 +33187,15 @@
"neo-async": "^2.5.0"
}
},
"wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
"dev": true,
"requires": {
"defaults": "^1.0.3"
}
},
"webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
......@@ -21,6 +21,7 @@
"eslint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx,.ts,.tsx .",
"test:watch": "cross-env TZ=UTC jest --watch",
"tsc": "tsc",
"clean-sass-vars": "find-unused-sass-variables ./js",
"stylelint": "stylelint '**/*.scss'",
"stylelint-fix": "stylelint --fix '**/*.scss'",
"format": "prettier --loglevel warn --write \"**/*.{ts,tsx,css,scss}\"",
......@@ -85,6 +86,7 @@
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.5",
"eslint-plugin-react-hooks": "^4.0.4",
"find-unused-sass-variables": "^2.0.0",
"html-webpack-plugin": "4.3.0",
"husky": "^4.2.5",
"jest": "^25.5.4",
......@@ -516,7 +518,8 @@
"if",
"each",
"include",
"mixin"
"mixin",
"extend"
]
}
]
......
......@@ -4,10 +4,19 @@ This document describes how to leverage the frontend service's application confi
**NOTE: This document is a work in progress and does not include 100% of features. We welcome PRs to complete this document**
## Announcements Config
Annoncements is a feature that allows to disclose new features, changes or any other news to Amundsen's users.
<img src='img/announcements_feature.png' width='50%' />
To enable this feature, change the `announcements.enable` boolean value by overriding it on [config-custom.ts](https://github.com/amundsen-io/amundsenfrontendlibrary/blob/master/amundsen_application/static/js/config/config-custom.ts#L1). Once activated, an "Announcements" link will be available in the global navigation, and a new list of announcements will show up on the right sidebar on the Homepage.
## Badge Config
Badges are a special type of tag that cannot be edited through the UI.
`BadgeConfig` can be used to customize the text and color of badges. This config defines a mapping of badge name to a `BadgeStyle` and optional `displayName`. Badges that are not defined will default to use the `BadgeStyle.default` style and `displayName` use the badge name with any `_` or `-` characters replaced with a space.
`BadgeConfig` can be used to customize the text and color of badges. This config defines a mapping of badge name to a `BadgeStyle` and optional `displayName`. Badges that are not defined will default to use the `BadgeStyle.default` style and `displayName` use the badge name with any `_` or `-` characters replaced with a space.
## Browse Tags Feature
......@@ -16,9 +25,10 @@ _TODO: Please add doc_
## Custom Logo
1. Add your logo to the folder in `amundsen_application/static/images/`.
2. Set the the `logoPath` key on the to the location of your image.
2. Set the the `logoPath` key on the to the location of your image.
## Date
This config allows you to specify various date formats across the app. There are three date formats in use shown below. These correspond to the `formatDate`, `formatDateTimeShort` and `formatDateTimeLong` utility functions.
default: 'MMM DD, YYYY'
......@@ -27,28 +37,32 @@ This config allows you to specify various date formats across the app. There are
Reference for formatting: https://devhints.io/datetime#momentjs-format
## Google Analytics
_TODO: Please add doc_
## Indexing Optional Resources
In Amundsen, we currently support indexing other optional resources beyond tables.
### Index Users
Users themselves are data resources and user metadata helps to facilitate network based discovery. When users are indexed they will show up in search results, and selecting a user surfaces a profile page that displays that user's relationships with different data resources.
After ingesting user metadata into the search and metadata services, set `IndexUsersConfig.enabled` to `true` on the application configuration to display the UI for the aforementioned features.
### Index Dashboards
Introducing dashboards into Amundsen allows users to discovery data analysis that has been already done. When dashboards are indexed they will show up in search results, and selecting a dashboard surfaces a page where users can explore dashboard metadata.
After ingesting dashboard metadata into the search and metadata services, set `IndexDashboardsConfig.enabled` to `true` on the application configuration to display the UI for the aforementioned features.
## Mail Client Features
Amundsen has two features that leverage the custom mail client -- the feedback tool and notifications.
As these are optional features, our `MailClientFeaturesConfig` can be used to hide/display any UI related to these features:
1. Set `MailClientFeaturesConfig.feedbackEnabled` to `true` in order to display the `Feedback` component in the UI.
2. Set `MailClientFeaturesConfig.notificationsEnabled` to `true` in order to display the optional UI for users to request more information about resources on the `TableDetail` page.
......@@ -60,24 +74,31 @@ client, please see this [entry](flask_config.md#mail-client-features) in our fla
_TODO: Please add doc_
## Resource Configurations
This configuration drives resource specific aspects of the application's user interface. Each supported resource should be mapped to an object that matches or extends the `BaseResourceConfig`.
### Base Configuration
All resource configurations must match or extend the `BaseResourceConfig`. This configuration supports the following options:
1. `displayName`: The name displayed throughout the application to refer to this resource type.
2. `filterCategories`: An optional `FilterConfig` object. When set for a given resource, that resource will display filter options in the search page UI.
3. `supportedSources`: An optional `SourcesConfig` object.
#### Filter Categories
The `FilterConfig` is an array of objects that match any of the supported filter options. We currently support a `MultiSelectFilterCategory` and a `SingleFilterCategory`. See our [config-types](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/js/config/config-types.ts) for more information about each option.
#### Supported Sources
The `SourcesConfig` can be used for the customizations detailed below. See examples in [config-default.ts](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/js/config/config-default.ts).
##### Custom Icons
You can configure custom icons to be used throughout the UI when representing entities from particular sources. On the `supportedSources` object, add an entry with the `id` used to reference that source and map to an object that specifies the `iconClass` for that database. This `iconClass` should be defined in [icons.scss](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/css/_icons.scss).
##### Display Names
You can configure a specific display name to be used throughout the UI when representing entities from particular sources. On the `supportedSources` object, add an entry with the `id` used to reference that source and map to an object that specified the `displayName` for that source.
## Table Lineage
......@@ -86,8 +107,9 @@ _TODO: Please add doc_
## Table Profile
_TODO: Please add doc*_
_TODO: Please add doc\*_
## Issue Tracking Features
In order to enable Issue Tracking set `IssueTrackingConfig.enabled` to `true` to see UI features. Further configuration
is required to fully enable the feature, please see this [entry](flask_config.md#issue-tracking-integration-features)
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