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

[Newsfeed] UI plugin for Kibana #49579

Merged
merged 88 commits into from
Nov 13, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
14c027f
Added base folder structure for Newsfeed plugin
YulNaumenko Oct 28, 2019
8dc7bcd
Added base folders for lib and component
YulNaumenko Oct 28, 2019
c1b5e12
Added newsfeed button to navigation controls on the right side
YulNaumenko Oct 29, 2019
7b78a9f
add getApi() to return api data observable (#49581)
tsullivan Oct 29, 2019
17503cc
Added flyout base body and provided EuiHeaderAlert component inside t…
YulNaumenko Oct 29, 2019
55445b6
Moved newsfeed plugin to OSS and added for the styles purpose new fol…
YulNaumenko Oct 29, 2019
229126b
Added subscribe on fetch newsfeed change
YulNaumenko Oct 30, 2019
8561c1e
Add NewsfeedApiDriver class (#49710)
tsullivan Oct 30, 2019
c5a9d12
add corner case handling
tsullivan Oct 30, 2019
993af30
Merge branch 'newsfeed_system_ui' of github.com:elastic/kibana into n…
YulNaumenko Oct 30, 2019
f6651c9
Added data binding to the ui
YulNaumenko Oct 30, 2019
c329bb1
added EuiHeaderAlert style overrides (#49739)
ryankeairns Oct 30, 2019
dcbbc5e
Fixed due to comments on PR
YulNaumenko Oct 30, 2019
d4cec01
Merge branch 'newsfeed_system_ui' of github.com:elastic/kibana into n…
YulNaumenko Oct 30, 2019
ebdd42d
add missing fields to NewsfeedItem and FetchResult
tsullivan Oct 30, 2019
2794cf5
fix templating of service url
tsullivan Oct 30, 2019
d4e908c
gracefully handle temporary request failure
tsullivan Oct 30, 2019
f3e2841
Mapped missing fields for data and badge
YulNaumenko Oct 30, 2019
ca46b3a
Merge branch 'newsfeed_system_ui' of github.com:elastic/kibana into n…
YulNaumenko Oct 30, 2019
c0f20be
Fixed typos issues
YulNaumenko Oct 30, 2019
bb0f1d8
integrate i18n.getLocale()
tsullivan Oct 30, 2019
f2920fb
Merge remote-tracking branch 'upstream/master' into newsfeed_system_ui
YulNaumenko Oct 30, 2019
0996836
Merge branch 'newsfeed_system_ui' of github.com:elastic/kibana into n…
YulNaumenko Oct 30, 2019
5f8cff5
allow service url root to be changed in dev mode
tsullivan Oct 30, 2019
01c214b
replace a lot of consts with config
tsullivan Oct 30, 2019
cad0561
fix flyout height (#49809)
ryankeairns Oct 30, 2019
bc90e7b
Add "error" field to FetchResult: Error | null
tsullivan Oct 31, 2019
2c07ada
simplify fetch error handling
tsullivan Oct 31, 2019
0ee4151
Do not store hash for items that are filtered out
tsullivan Oct 31, 2019
09b3958
add expireOn in case it is useful to UI
tsullivan Oct 31, 2019
f6d8c01
always use staging url for dev config
tsullivan Oct 31, 2019
4295f62
unit test for newsfeed api driver
tsullivan Oct 31, 2019
d790dac
simplify modelItems
tsullivan Oct 31, 2019
d4c1627
Fixed eslint errors
YulNaumenko Oct 31, 2019
286712d
Fixed label translations
YulNaumenko Oct 31, 2019
fbed395
Add unit test for concatenating the stored hashes with the new
tsullivan Oct 31, 2019
2e9c9fe
add newsfeed to i18n.json
tsullivan Oct 31, 2019
3ecaa70
Fixed expression error
YulNaumenko Oct 31, 2019
cd351ea
Merge branch 'newsfeed_system_ui' of github.com:elastic/kibana into n…
YulNaumenko Oct 31, 2019
469df39
Merge remote-tracking branch 'upstream/master' into newsfeed_system_ui
YulNaumenko Oct 31, 2019
0fb4ff7
--wip-- [skip ci]
tsullivan Oct 31, 2019
ae2d96e
fix parse error
tsullivan Oct 31, 2019
ad97e1d
fix test
tsullivan Oct 31, 2019
3b56a69
test(newsfeed): Added testing endpoint which simulates the Elastic Ne…
gmmorris Oct 31, 2019
3f02bd6
Merge branch 'newsfeed_system_ui' of https://github.com/elastic/kiban…
gmmorris Oct 31, 2019
bee3894
add tests for getApi()
tsullivan Oct 31, 2019
2428828
add tests for getApi
tsullivan Oct 31, 2019
b001961
Added no news page
YulNaumenko Oct 31, 2019
4331261
Merge branch 'newsfeed_system_ui' of github.com:elastic/kibana into n…
YulNaumenko Oct 31, 2019
2cede99
Merge branch 'newsfeed_system_ui' of https://github.com/elastic/kiban…
gmmorris Oct 31, 2019
074b1e9
fix fetch not happening after page refresh with sessionStorage primed
tsullivan Oct 31, 2019
c396648
test(newsfeed): Added testing endpoint which simulates the Elastic Ne…
gmmorris Oct 31, 2019
6e175e5
Added loading screen
YulNaumenko Oct 31, 2019
67a81b2
Small fixes due to comments
YulNaumenko Oct 31, 2019
f0daf99
Fixed issue with stop fetching news on error catch
YulNaumenko Oct 31, 2019
4a98ec4
Merge branch 'newsfeed_system_ui' of https://github.com/elastic/kiban…
gmmorris Nov 1, 2019
374bd5c
test(newsfeed): Configure FTS to point newsfeed to the simulated news…
gmmorris Nov 1, 2019
e858d4c
Fixed browser error message: Invariant Violation: [React Intl] Could …
YulNaumenko Nov 1, 2019
df8f6a5
Fixed typo issue in label name
YulNaumenko Nov 2, 2019
dc5fb6e
polish the code changes
tsullivan Oct 31, 2019
de4dfd1
Merge remote-tracking branch 'upstream/master' into newsfeed_system_ui
tsullivan Nov 4, 2019
cad7960
Add simple jest/enzyme tests for the components
tsullivan Nov 4, 2019
6dc0ffd
honor utc format
tsullivan Nov 7, 2019
a6fab7c
Filter pre-published items
tsullivan Nov 7, 2019
364cad0
Fall back to en
tsullivan Nov 7, 2019
6eef2df
retry tests
tsullivan Nov 7, 2019
9949b76
comment clarfication
tsullivan Nov 7, 2019
143e8ad
Setup newsfeed service fixture from test/common/config
tsullivan Nov 11, 2019
a77b64a
Added base functional tests for newsfeed functionality
YulNaumenko Nov 12, 2019
7d6b271
valid urlroot is for prod
tsullivan Nov 12, 2019
c067b27
Merge remote-tracking branch 'upstream/master' into newsfeed_system_ui
tsullivan Nov 12, 2019
748919e
add documentation for the supported enabled setting
tsullivan Nov 12, 2019
3ab5eb7
more urlRoot
tsullivan Nov 12, 2019
8c2cd8d
--wip-- [skip ci]
tsullivan Nov 12, 2019
705ac83
add the before for fn
tsullivan Nov 12, 2019
2c92aa6
update jest snapshot
tsullivan Nov 12, 2019
db1e692
add ui_capabilties test
tsullivan Nov 12, 2019
15814a3
Fixed failing test
YulNaumenko Nov 12, 2019
e55b94b
finish newsfeed error functional test
tsullivan Nov 12, 2019
cf64690
include ui_capability config
tsullivan Nov 12, 2019
2c9449a
Merge remote-tracking branch 'upstream/master' into newsfeed_system_ui
tsullivan Nov 12, 2019
446657a
Merge remote-tracking branch 'upstream/master' into newsfeed_system_ui
tsullivan Nov 12, 2019
e430554
error case testing in ci group 6
tsullivan Nov 12, 2019
2c68980
refactor(newsfeed): moved newsfeed api call so that it is done before…
gmmorris Nov 12, 2019
ff82850
code polish
tsullivan Nov 12, 2019
eeff602
enabled newsfeed_err test in CI
tsullivan Nov 12, 2019
c63eed2
Merge remote-tracking branch 'upstream/master' into newsfeed_system_ui
tsullivan Nov 12, 2019
a61b155
Merge remote-tracking branch 'upstream/master' into newsfeed_system_ui
YulNaumenko Nov 13, 2019
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
6 changes: 6 additions & 0 deletions x-pack/plugins/newsfeed/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "newsfeed",
"version": "kibana",
"server": false,
"ui": true
}
74 changes: 74 additions & 0 deletions x-pack/plugins/newsfeed/public/components/flyout_list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, useContext } from 'react';
import {
EuiIcon,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiLink,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiText,
EuiBadge,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiHeaderAlert } from './header_alert/header_alert';
import { NewsfeedContext } from './newsfeed_header_nav_button';

export const NewsfeedFlyout = () => {
const { setFlyoutVisible } = useContext(NewsfeedContext);
const closeFlyout = useCallback(() => setFlyoutVisible(false), []);
return (
<EuiFlyout onClose={closeFlyout} size="s" aria-labelledby="flyoutSmallTitle">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2 id="flyoutSmallTitle">What's new</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiHeaderAlert
title="Control access to features"
text="Show or hide applications and features per space in Kibana."
action={<EuiLink href="/guides/feature-controls">Learn about feature controls</EuiLink>}
date="1 May 2019"
badge={<EuiBadge>7.1</EuiBadge>}
/>
<EuiHeaderAlert
title="Kibana 7.0 is turning heads"
text="Simplified navigation, responsive dashboards, dark mode… pick your favorite."
action={
<EuiLink target="_blank" href="https://www.elastic.co/blog/kibana-7-0-0-released">
Read the blog <EuiIcon type="popout" size="s" />
</EuiLink>
}
date="10 April 2019"
badge={<EuiBadge color="hollow">7.0</EuiBadge>}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="newsfeed.components.flyoutList.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
<p>Version 7.0</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

.euiHeaderAlert {
min-width: 300px;
position: relative;
margin-bottom: $euiSizeL;
padding: 0 $euiSizeS $euiSizeL;
border-bottom: $euiBorderThin;
border-top: none;

.euiHeaderAlert__dismiss {
opacity: 0;
position: absolute;
right: $euiSize - 4px;
top: $euiSize - 4px;
transition: opacity $euiAnimSpeedNormal ease-in;
}

&:hover .euiHeaderAlert__dismiss,
.euiHeaderAlert__dismiss:focus {
opacity: 1;
}

.euiHeaderAlert__title {
@include euiTitle('xs');
margin-bottom: $euiSizeS;
padding-right: $euiSizeL; // Accounts for the dismiss button.
}

.euiHeaderAlert__text {
@include euiFontSizeS;
margin-bottom: $euiSize;
}

.euiHeaderAlert__action {
@include euiFontSizeS;
}

.euiHeaderAlert__date {
@include euiFontSizeXS;
color: $euiColorDarkShade;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

@import 'header_alert';
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import { EuiFlexGroup, EuiFlexItem, EuiBadgeProps, EuiI18n } from '@elastic/eui';

interface IEuiHeaderAlertProps {
action: JSX.Element;
className?: string;
date: string;
text: string;
title: string;
badge?: JSX.Element;
rest?: string[];
}

export const EuiHeaderAlert = ({
action,
className,
date,
text,
title,
badge,
...rest
}: IEuiHeaderAlertProps) => {
const classes = classNames('euiHeaderAlert', className);

let badgeContent: JSX.Element | null;
if (badge) {
badgeContent = badge;
} else {
badgeContent = null;
}

return (
<EuiI18n token="euiHeaderAlert.dismiss" default="Dismiss">
{(dismiss: any) => (
<div className={classes} {...rest}>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<div className="euiHeaderAlert__date">{date}</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>{badgeContent}</EuiFlexItem>
</EuiFlexGroup>

<div className="euiHeaderAlert__title">{title}</div>
<div className="euiHeaderAlert__text">{text}</div>
<div className="euiHeaderAlert__action euiLink">{action}</div>
</div>
)}
</EuiI18n>
);
};

EuiHeaderAlert.propTypes = {
action: PropTypes.node,
className: PropTypes.string,
date: PropTypes.node.isRequired,
text: PropTypes.node,
title: PropTypes.node.isRequired,
badge: PropTypes.node,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useState, Fragment } from 'react';
import { EuiHeaderSectionItemButton, EuiIcon, EuiNotificationBadge } from '@elastic/eui';
import { NewsfeedFlyout } from './flyout_list';

export const NewsfeedContext = React.createContext({} as any);

export const MailNavButton = () => {
const [showBadge, setShowBadge] = useState<boolean>(true);
const [flyoutVisible, setFlyoutVisible] = useState<boolean>(false);
function showFlyout() {
setShowBadge(false);
setFlyoutVisible(!flyoutVisible);
}
let flyout;
if (flyoutVisible) {
flyout = <NewsfeedFlyout />;
}
return (
<NewsfeedContext.Provider value={{ setFlyoutVisible }}>
<Fragment>
<EuiHeaderSectionItemButton
aria-controls="keyPadMenu"
aria-expanded={flyoutVisible}
aria-haspopup="true"
aria-label="Apps menu"
onClick={showFlyout}
>
<EuiIcon type="email" size="m" />

{showBadge ? (
<EuiNotificationBadge className="euiHeaderNotification">&#9642;</EuiNotificationBadge>
) : null}
</EuiHeaderSectionItemButton>
{flyout}
</Fragment>
</NewsfeedContext.Provider>
);
};
7 changes: 7 additions & 0 deletions x-pack/plugins/newsfeed/public/components/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

@import './header_alert/index';
14 changes: 14 additions & 0 deletions x-pack/plugins/newsfeed/public/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { PluginInitializerContext } from '../../../../src/core/public';
import { NewsfeedPublicPlugin } from './plugin';

export function plugin(initializerContext: PluginInitializerContext) {
return new NewsfeedPublicPlugin(initializerContext);
}

export { NewsfeedPublicPlugin as Plugin };
114 changes: 114 additions & 0 deletions x-pack/plugins/newsfeed/public/lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import * as Rx from 'rxjs';
import moment from 'moment';
import { filter, mergeMap, tap } from 'rxjs/operators';
import { HttpServiceBase } from '../../../../../src/core/public';

interface ApiItem {
hash: string;
expire_on: Date;
title: { [lang: string]: string };
description: { [lang: string]: string };
link_text: { [lang: string]: string };
link_url: { [lang: string]: string };

badge: null; // not used phase 1
image_url: null; // not used phase 1
languages: null; // not used phase 1
publish_on: null; // not used phase 1
}

interface NewsfeedItem {
title: string;
description: string;
linkText: string;
linkUrl: string;
}

interface FetchResult {
hasNew: boolean;
feedItems: NewsfeedItem[];
}

const DEFAULT_LANGUAGE = 'en'; // TODO: read from settings, default to en
const NEWSFEED_MAIN_INTERVAL = 120000; // A main interval to check for need to refresh (2min)
const NEWSFEED_FETCH_INTERVAL = moment.duration(1, 'day'); // how often to actually fetch the API
const NEWSFEED_LAST_FETCH_STORAGE_KEY = 'xpack.newsfeed.lastfetchtime';
const NEWSFEED_HASH_SET_STORAGE_KEY = 'xpack.newsfeed.hashes';
const NEWSFEED_SERVICE_URL = 'https://feeds.elastic.co/kibana/v7.0.0.json'; // FIXME: should be dynamic

function shouldFetch(): boolean {
const lastFetch: string | null = localStorage.getItem(NEWSFEED_LAST_FETCH_STORAGE_KEY);
if (lastFetch == null) {
return true;
}
const last = moment(lastFetch, 'x'); // parse as unix ms timestamp
const now = moment();
const duration = moment.duration(now.diff(last));

return duration > NEWSFEED_FETCH_INTERVAL;
}

function updateLastFetch() {
localStorage.setItem(NEWSFEED_LAST_FETCH_STORAGE_KEY, Date.now().toString());
}

function updateHashes(items: ApiItem[]): { previous: string[]; current: string[] } {
// combine localStorage hashes with new hashes
const hashSet: string | null = localStorage.getItem(NEWSFEED_HASH_SET_STORAGE_KEY);
let oldHashes: string[] = [];
if (hashSet != null) {
oldHashes = hashSet.split(',');
}
const newHashes = items.map(i => i.hash.slice(0, 10));
const updatedHashes = [...new Set(oldHashes.concat(newHashes))];
localStorage.setItem(NEWSFEED_HASH_SET_STORAGE_KEY, updatedHashes.join(','));

return { previous: oldHashes, current: updatedHashes };
}

/*
* Creates an Observable to newsfeed items, powered by the main interval
* Computes hasNew value from new item hashes saved in localStorage
*/
export function getApi(http: HttpServiceBase): Rx.Observable<void | FetchResult> {
return Rx.timer(0, NEWSFEED_MAIN_INTERVAL).pipe(
filter(() => shouldFetch()),
mergeMap(
(value: number): Rx.Observable<ApiItem[]> => {
return Rx.from(
http.fetch(NEWSFEED_SERVICE_URL, { method: 'GET' }).then(({ items }) => items)
);
}
),
filter(items => items.length > 0),
tap(() => updateLastFetch()),
mergeMap(
(items): Rx.Observable<FetchResult> => {
// calculate hasNew
const { previous, current } = updateHashes(items);
const hasNew = current.length > previous.length;

// model feed items
const feedItems: NewsfeedItem[] = items.map(it => {
return {
title: it.title[DEFAULT_LANGUAGE],
description: it.description[DEFAULT_LANGUAGE],
linkText: it.link_text[DEFAULT_LANGUAGE],
linkUrl: it.link_url[DEFAULT_LANGUAGE],
};
});

return Rx.of({
hasNew,
feedItems,
});
}
)
);
}
Loading