mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [Newsfeed] UI plugin for Kibana (#49579) * Added base folder structure for Newsfeed plugin * Added base folders for lib and component * Added newsfeed button to navigation controls on the right side * add getApi() to return api data observable (#49581) * Added flyout base body and provided EuiHeaderAlert component inside the newsfeed plugin * Moved newsfeed plugin to OSS and added for the styles purpose new folder for legacy plugin 'newsfeed' with the same id to support this * Added subscribe on fetch newsfeed change * Add NewsfeedApiDriver class (#49710) * add NewsfeedApiDriver class * fix xpack prefix * add corner case handling * Added data binding to the ui * added EuiHeaderAlert style overrides (#49739) * Fixed due to comments on PR * add missing fields to NewsfeedItem and FetchResult * fix templating of service url * gracefully handle temporary request failure * Mapped missing fields for data and badge * Fixed typos issues * integrate i18n.getLocale() * allow service url root to be changed in dev mode * replace a lot of consts with config * fix flyout height (#49809) * Add "error" field to FetchResult: Error | null * simplify fetch error handling * Do not store hash for items that are filtered out * add expireOn in case it is useful to UI * always use staging url for dev config * unit test for newsfeed api driver * simplify modelItems * Fixed eslint errors * Fixed label translations * Add unit test for concatenating the stored hashes with the new * add newsfeed to i18n.json * Fixed expression error * --wip-- [skip ci] * fix parse error * fix test * test(newsfeed): Added testing endpoint which simulates the Elastic Newsfeed for consumption in functional tests * add tests for getApi() * add tests for getApi * Added no news page * fix fetch not happening after page refresh with sessionStorage primed * test(newsfeed): Added testing endpoint which simulates the Elastic Newsfeed for consumption in functional tests * Added loading screen * Small fixes due to comments * Fixed issue with stop fetching news on error catch * test(newsfeed): Configure FTS to point newsfeed to the simulated newsfeed endpoit * Fixed browser error message: Invariant Violation: [React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry. * Fixed typo issue in label name * polish the code changes * Add simple jest/enzyme tests for the components * honor utc format * Filter pre-published items * Fall back to en * retry tests * comment clarfication * Setup newsfeed service fixture from test/common/config * Added base functional tests for newsfeed functionality * valid urlroot is for prod * add documentation for the supported enabled setting * more urlRoot * --wip-- [skip ci] * add the before for fn * add ui_capabilties test * update jest snapshot * Fixed failing test * finish newsfeed error functional test * include ui_capability config * error case testing in ci group 6 * refactor(newsfeed): moved newsfeed api call so that it is done before its use * code polish * enabled newsfeed_err test in CI * correct bad merge resolve * allow default export for ftr file * fix type check error
This commit is contained in:
parent
388b3ce3c8
commit
1f210a92d2
36 changed files with 2091 additions and 0 deletions
|
@ -21,6 +21,7 @@
|
|||
"visTypeMetric": "src/legacy/core_plugins/vis_type_metric",
|
||||
"visTypeVega": "src/legacy/core_plugins/vis_type_vega",
|
||||
"visTypeTable": "src/legacy/core_plugins/vis_type_table",
|
||||
"newsfeed": "src/plugins/newsfeed",
|
||||
"regionMap": "src/legacy/core_plugins/region_map",
|
||||
"statusPage": "src/legacy/core_plugins/status_page",
|
||||
"tileMap": "src/legacy/core_plugins/tile_map",
|
||||
|
|
|
@ -243,6 +243,10 @@ override this parameter to use their own Tile Map Service. For example:
|
|||
`ops.interval:`:: *Default: 5000* Set the interval in milliseconds to sample
|
||||
system and process performance metrics. The minimum value is 100.
|
||||
|
||||
`newsfeed.enabled:` :: *Default: `true`* Controls whether to enable the newsfeed
|
||||
system for the Kibana UI notification center. Set to `false` to disable the
|
||||
newsfeed system.
|
||||
|
||||
`path.data:`:: *Default: `data`* The path where Kibana stores persistent data
|
||||
not saved in Elasticsearch.
|
||||
|
||||
|
|
|
@ -23,4 +23,5 @@ require('@kbn/test').runTestsCli([
|
|||
require.resolve('../test/api_integration/config.js'),
|
||||
require.resolve('../test/plugin_functional/config.js'),
|
||||
require.resolve('../test/interpreter_functional/config.js'),
|
||||
require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'),
|
||||
]);
|
||||
|
|
23
src/legacy/core_plugins/newsfeed/constants.ts
Normal file
23
src/legacy/core_plugins/newsfeed/constants.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const PLUGIN_ID = 'newsfeed';
|
||||
export const DEFAULT_SERVICE_URLROOT = 'https://feeds.elastic.co';
|
||||
export const DEV_SERVICE_URLROOT = 'https://feeds-staging.elastic.co';
|
||||
export const DEFAULT_SERVICE_PATH = '/kibana/v{VERSION}.json';
|
71
src/legacy/core_plugins/newsfeed/index.ts
Normal file
71
src/legacy/core_plugins/newsfeed/index.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { LegacyPluginApi, LegacyPluginSpec, ArrayOrItem } from 'src/legacy/plugin_discovery/types';
|
||||
import { Legacy } from 'kibana';
|
||||
import { NewsfeedPluginInjectedConfig } from '../../../plugins/newsfeed/types';
|
||||
import {
|
||||
PLUGIN_ID,
|
||||
DEFAULT_SERVICE_URLROOT,
|
||||
DEV_SERVICE_URLROOT,
|
||||
DEFAULT_SERVICE_PATH,
|
||||
} from './constants';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(kibana: LegacyPluginApi): ArrayOrItem<LegacyPluginSpec> {
|
||||
const pluginSpec: Legacy.PluginSpecOptions = {
|
||||
id: PLUGIN_ID,
|
||||
config(Joi: any) {
|
||||
// NewsfeedPluginInjectedConfig in Joi form
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
service: Joi.object({
|
||||
pathTemplate: Joi.string().default(DEFAULT_SERVICE_PATH),
|
||||
urlRoot: Joi.when('$prod', {
|
||||
is: true,
|
||||
then: Joi.string().default(DEFAULT_SERVICE_URLROOT),
|
||||
otherwise: Joi.string().default(DEV_SERVICE_URLROOT),
|
||||
}),
|
||||
}).default(),
|
||||
defaultLanguage: Joi.string().default('en'),
|
||||
mainInterval: Joi.number().default(120 * 1000), // (2min) How often to retry failed fetches, and/or check if newsfeed items need to be refreshed from remote
|
||||
fetchInterval: Joi.number().default(86400 * 1000), // (1day) How often to fetch remote and reset the last fetched time
|
||||
}).default();
|
||||
},
|
||||
uiExports: {
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
injectDefaultVars(server): NewsfeedPluginInjectedConfig {
|
||||
const config = server.config();
|
||||
return {
|
||||
newsfeed: {
|
||||
service: {
|
||||
pathTemplate: config.get('newsfeed.service.pathTemplate') as string,
|
||||
urlRoot: config.get('newsfeed.service.urlRoot') as string,
|
||||
},
|
||||
defaultLanguage: config.get('newsfeed.defaultLanguage') as string,
|
||||
mainInterval: config.get('newsfeed.mainInterval') as number,
|
||||
fetchInterval: config.get('newsfeed.fetchInterval') as number,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
return new kibana.Plugin(pluginSpec);
|
||||
}
|
4
src/legacy/core_plugins/newsfeed/package.json
Normal file
4
src/legacy/core_plugins/newsfeed/package.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "newsfeed",
|
||||
"version": "kibana"
|
||||
}
|
3
src/legacy/core_plugins/newsfeed/public/index.scss
Normal file
3
src/legacy/core_plugins/newsfeed/public/index.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
@import 'src/legacy/ui/public/styles/styling_constants';
|
||||
|
||||
@import './np_ready/components/header_alert/_index';
|
|
@ -0,0 +1,27 @@
|
|||
@import '@elastic/eui/src/components/header/variables';
|
||||
|
||||
.kbnNews__flyout {
|
||||
top: $euiHeaderChildSize + 1px;
|
||||
height: calc(100% - #{$euiHeaderChildSize});
|
||||
}
|
||||
|
||||
.kbnNewsFeed__headerAlert.euiHeaderAlert {
|
||||
margin-bottom: $euiSizeL;
|
||||
padding: 0 $euiSizeS $euiSizeL;
|
||||
border-bottom: $euiBorderThin;
|
||||
border-top: none;
|
||||
|
||||
.euiHeaderAlert__title {
|
||||
@include euiTitle('xs');
|
||||
margin-bottom: $euiSizeS;
|
||||
}
|
||||
|
||||
.euiHeaderAlert__text {
|
||||
@include euiFontSizeS;
|
||||
margin-bottom: $euiSize;
|
||||
}
|
||||
|
||||
.euiHeaderAlert__action {
|
||||
@include euiFontSizeS;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, 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', 'kbnNewsFeed__headerAlert', className);
|
||||
|
||||
const badgeContent = badge || 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,
|
||||
};
|
22
src/plugins/newsfeed/constants.ts
Normal file
22
src/plugins/newsfeed/constants.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const NEWSFEED_FALLBACK_LANGUAGE = 'en';
|
||||
export const NEWSFEED_LAST_FETCH_STORAGE_KEY = 'newsfeed.lastfetchtime';
|
||||
export const NEWSFEED_HASH_SET_STORAGE_KEY = 'newsfeed.hashes';
|
6
src/plugins/newsfeed/kibana.json
Normal file
6
src/plugins/newsfeed/kibana.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"id": "newsfeed",
|
||||
"version": "kibana",
|
||||
"server": false,
|
||||
"ui": true
|
||||
}
|
27
src/plugins/newsfeed/public/components/__snapshots__/empty_news.test.tsx.snap
generated
Normal file
27
src/plugins/newsfeed/public/components/__snapshots__/empty_news.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`empty_news rendering renders the default Empty News 1`] = `
|
||||
<EuiEmptyPrompt
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="If your Kibana instance doesn’t have internet access, ask your administrator to disable this feature. Otherwise, we’ll keep trying to fetch the news."
|
||||
id="newsfeed.emptyPrompt.noNewsText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="emptyNewsfeed"
|
||||
iconType="documents"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
defaultMessage="No news?"
|
||||
id="newsfeed.emptyPrompt.noNewsTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="s"
|
||||
/>
|
||||
`;
|
20
src/plugins/newsfeed/public/components/__snapshots__/loading_news.test.tsx.snap
generated
Normal file
20
src/plugins/newsfeed/public/components/__snapshots__/loading_news.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`news_loading rendering renders the default News Loading 1`] = `
|
||||
<EuiEmptyPrompt
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Getting the latest news..."
|
||||
id="newsfeed.loadingPrompt.gettingNewsText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
title={
|
||||
<EuiLoadingKibana
|
||||
size="xl"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
`;
|
32
src/plugins/newsfeed/public/components/empty_news.test.tsx
Normal file
32
src/plugins/newsfeed/public/components/empty_news.test.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import { NewsEmptyPrompt } from './empty_news';
|
||||
|
||||
describe('empty_news', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders the default Empty News', () => {
|
||||
const wrapper = shallow(<NewsEmptyPrompt />);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
44
src/plugins/newsfeed/public/components/empty_news.tsx
Normal file
44
src/plugins/newsfeed/public/components/empty_news.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
export const NewsEmptyPrompt = () => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="documents"
|
||||
titleSize="s"
|
||||
data-test-subj="emptyNewsfeed"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage id="newsfeed.emptyPrompt.noNewsTitle" defaultMessage="No news?" />
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="newsfeed.emptyPrompt.noNewsText"
|
||||
defaultMessage="If your Kibana instance doesn’t have internet access, ask your administrator to disable this feature. Otherwise, we’ll keep trying to fetch the news."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
110
src/plugins/newsfeed/public/components/flyout_list.tsx
Normal file
110
src/plugins/newsfeed/public/components/flyout_list.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the 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 '../../../../legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert';
|
||||
import { NewsfeedContext } from './newsfeed_header_nav_button';
|
||||
import { NewsfeedItem } from '../../types';
|
||||
import { NewsEmptyPrompt } from './empty_news';
|
||||
import { NewsLoadingPrompt } from './loading_news';
|
||||
|
||||
export const NewsfeedFlyout = () => {
|
||||
const { newsFetchResult, setFlyoutVisible } = useContext(NewsfeedContext);
|
||||
const closeFlyout = useCallback(() => setFlyoutVisible(false), [setFlyoutVisible]);
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
onClose={closeFlyout}
|
||||
size="s"
|
||||
aria-labelledby="flyoutSmallTitle"
|
||||
className="kbnNews__flyout"
|
||||
data-test-subj="NewsfeedFlyout"
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="flyoutSmallTitle">
|
||||
<FormattedMessage id="newsfeed.flyoutList.whatsNewTitle" defaultMessage="What's new" />
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody className={'kbnNews__flyoutAlerts'}>
|
||||
{!newsFetchResult ? (
|
||||
<NewsLoadingPrompt />
|
||||
) : newsFetchResult.feedItems.length > 0 ? (
|
||||
newsFetchResult.feedItems.map((item: NewsfeedItem) => {
|
||||
return (
|
||||
<EuiHeaderAlert
|
||||
key={item.hash}
|
||||
title={item.title}
|
||||
text={item.description}
|
||||
data-test-subj="newsHeadAlert"
|
||||
action={
|
||||
<EuiLink target="_blank" href={item.linkUrl}>
|
||||
{item.linkText}
|
||||
<EuiIcon type="popout" size="s" />
|
||||
</EuiLink>
|
||||
}
|
||||
date={item.publishOn.format('DD MMMM YYYY')}
|
||||
badge={<EuiBadge color="hollow">{item.badge}</EuiBadge>}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<NewsEmptyPrompt />
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
|
||||
<FormattedMessage id="newsfeed.flyoutList.closeButtonLabel" defaultMessage="Close" />
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{newsFetchResult ? (
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="newsfeed.flyoutList.versionTextLabel"
|
||||
defaultMessage="{version}"
|
||||
values={{ version: `Version ${newsFetchResult.kibanaVersion}` }}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
32
src/plugins/newsfeed/public/components/loading_news.test.tsx
Normal file
32
src/plugins/newsfeed/public/components/loading_news.test.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import { NewsLoadingPrompt } from './loading_news';
|
||||
|
||||
describe('news_loading', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders the default News Loading', () => {
|
||||
const wrapper = shallow(<NewsLoadingPrompt />);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
39
src/plugins/newsfeed/public/components/loading_news.tsx
Normal file
39
src/plugins/newsfeed/public/components/loading_news.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { EuiLoadingKibana } from '@elastic/eui';
|
||||
|
||||
export const NewsLoadingPrompt = () => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<EuiLoadingKibana size="xl" />}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="newsfeed.loadingPrompt.gettingNewsText"
|
||||
defaultMessage="Getting the latest news..."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, Fragment, useEffect } from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
import { EuiHeaderSectionItemButton, EuiIcon, EuiNotificationBadge } from '@elastic/eui';
|
||||
import { NewsfeedFlyout } from './flyout_list';
|
||||
import { FetchResult } from '../../types';
|
||||
|
||||
export interface INewsfeedContext {
|
||||
setFlyoutVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
newsFetchResult: FetchResult | void | null;
|
||||
}
|
||||
export const NewsfeedContext = React.createContext({} as INewsfeedContext);
|
||||
|
||||
export type NewsfeedApiFetchResult = Rx.Observable<void | FetchResult | null>;
|
||||
|
||||
export interface Props {
|
||||
apiFetchResult: NewsfeedApiFetchResult;
|
||||
}
|
||||
|
||||
export const NewsfeedNavButton = ({ apiFetchResult }: Props) => {
|
||||
const [showBadge, setShowBadge] = useState<boolean>(false);
|
||||
const [flyoutVisible, setFlyoutVisible] = useState<boolean>(false);
|
||||
const [newsFetchResult, setNewsFetchResult] = useState<FetchResult | null | void>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleStatusChange(fetchResult: FetchResult | void | null) {
|
||||
if (fetchResult) {
|
||||
setShowBadge(fetchResult.hasNew);
|
||||
}
|
||||
setNewsFetchResult(fetchResult);
|
||||
}
|
||||
|
||||
const subscription = apiFetchResult.subscribe(res => handleStatusChange(res));
|
||||
return () => subscription.unsubscribe();
|
||||
}, [apiFetchResult]);
|
||||
|
||||
function showFlyout() {
|
||||
setShowBadge(false);
|
||||
setFlyoutVisible(!flyoutVisible);
|
||||
}
|
||||
|
||||
return (
|
||||
<NewsfeedContext.Provider value={{ setFlyoutVisible, newsFetchResult }}>
|
||||
<Fragment>
|
||||
<EuiHeaderSectionItemButton
|
||||
data-test-subj="newsfeed"
|
||||
aria-controls="keyPadMenu"
|
||||
aria-expanded={flyoutVisible}
|
||||
aria-haspopup="true"
|
||||
aria-label="Newsfeed menu"
|
||||
onClick={showFlyout}
|
||||
>
|
||||
<EuiIcon type="email" size="m" />
|
||||
{showBadge ? (
|
||||
<EuiNotificationBadge className="euiHeaderNotification" data-test-subj="showBadgeNews">
|
||||
▪
|
||||
</EuiNotificationBadge>
|
||||
) : null}
|
||||
</EuiHeaderSectionItemButton>
|
||||
{flyoutVisible ? <NewsfeedFlyout /> : null}
|
||||
</Fragment>
|
||||
</NewsfeedContext.Provider>
|
||||
);
|
||||
};
|
25
src/plugins/newsfeed/public/index.ts
Normal file
25
src/plugins/newsfeed/public/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from 'src/core/public';
|
||||
import { NewsfeedPublicPlugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new NewsfeedPublicPlugin(initializerContext);
|
||||
}
|
701
src/plugins/newsfeed/public/lib/api.test.ts
Normal file
701
src/plugins/newsfeed/public/lib/api.test.ts
Normal file
|
@ -0,0 +1,701 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { mapTo, take, tap, toArray } from 'rxjs/operators';
|
||||
import { interval, race } from 'rxjs';
|
||||
import sinon, { stub } from 'sinon';
|
||||
import moment from 'moment';
|
||||
import { HttpServiceBase } from 'src/core/public';
|
||||
import { NEWSFEED_HASH_SET_STORAGE_KEY, NEWSFEED_LAST_FETCH_STORAGE_KEY } from '../../constants';
|
||||
import { ApiItem, NewsfeedItem, NewsfeedPluginInjectedConfig } from '../../types';
|
||||
import { NewsfeedApiDriver, getApi } from './api';
|
||||
|
||||
const localStorageGet = sinon.stub();
|
||||
const sessionStoragetGet = sinon.stub();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: {
|
||||
getItem: localStorageGet,
|
||||
setItem: stub(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(window, 'sessionStorage', {
|
||||
value: {
|
||||
getItem: sessionStoragetGet,
|
||||
setItem: stub(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('NewsfeedApiDriver', () => {
|
||||
const kibanaVersion = 'test_version';
|
||||
const userLanguage = 'en';
|
||||
const fetchInterval = 2000;
|
||||
const getDriver = () => new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval);
|
||||
|
||||
afterEach(() => {
|
||||
sinon.reset();
|
||||
});
|
||||
|
||||
describe('shouldFetch', () => {
|
||||
it('defaults to true', () => {
|
||||
const driver = getDriver();
|
||||
expect(driver.shouldFetch()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true if last fetch time precedes page load time', () => {
|
||||
sessionStoragetGet.throws('Wrong key passed!');
|
||||
sessionStoragetGet.withArgs(NEWSFEED_LAST_FETCH_STORAGE_KEY).returns(322642800000); // 1980-03-23
|
||||
const driver = getDriver();
|
||||
expect(driver.shouldFetch()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if last fetch time is recent enough', () => {
|
||||
sessionStoragetGet.throws('Wrong key passed!');
|
||||
sessionStoragetGet.withArgs(NEWSFEED_LAST_FETCH_STORAGE_KEY).returns(3005017200000); // 2065-03-23
|
||||
const driver = getDriver();
|
||||
expect(driver.shouldFetch()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateHashes', () => {
|
||||
it('returns previous and current storage', () => {
|
||||
const driver = getDriver();
|
||||
const items: NewsfeedItem[] = [
|
||||
{
|
||||
title: 'Good news, everyone!',
|
||||
description: 'good item description',
|
||||
linkText: 'click here',
|
||||
linkUrl: 'about:blank',
|
||||
badge: 'test',
|
||||
publishOn: moment(1572489035150),
|
||||
expireOn: moment(1572489047858),
|
||||
hash: 'hash1oneoneoneone',
|
||||
},
|
||||
];
|
||||
expect(driver.updateHashes(items)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"current": Array [
|
||||
"hash1oneoneoneone",
|
||||
],
|
||||
"previous": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('concatenates the previous hashes with the current', () => {
|
||||
localStorageGet.throws('Wrong key passed!');
|
||||
localStorageGet.withArgs(NEWSFEED_HASH_SET_STORAGE_KEY).returns('happyness');
|
||||
const driver = getDriver();
|
||||
const items: NewsfeedItem[] = [
|
||||
{
|
||||
title: 'Better news, everyone!',
|
||||
description: 'better item description',
|
||||
linkText: 'click there',
|
||||
linkUrl: 'about:blank',
|
||||
badge: 'concatentated',
|
||||
publishOn: moment(1572489035150),
|
||||
expireOn: moment(1572489047858),
|
||||
hash: 'three33hash',
|
||||
},
|
||||
];
|
||||
expect(driver.updateHashes(items)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"current": Array [
|
||||
"happyness",
|
||||
"three33hash",
|
||||
],
|
||||
"previous": Array [
|
||||
"happyness",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('Validates items for required fields', () => {
|
||||
const driver = getDriver();
|
||||
expect(driver.validateItem({})).toBe(false);
|
||||
expect(
|
||||
driver.validateItem({
|
||||
title: 'Gadzooks!',
|
||||
description: 'gadzooks item description',
|
||||
linkText: 'click here',
|
||||
linkUrl: 'about:blank',
|
||||
badge: 'test',
|
||||
publishOn: moment(1572489035150),
|
||||
expireOn: moment(1572489047858),
|
||||
hash: 'hash2twotwotwotwotwo',
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
driver.validateItem({
|
||||
title: 'Gadzooks!',
|
||||
description: 'gadzooks item description',
|
||||
linkText: 'click here',
|
||||
linkUrl: 'about:blank',
|
||||
publishOn: moment(1572489035150),
|
||||
hash: 'hash2twotwotwotwotwo',
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
driver.validateItem({
|
||||
title: 'Gadzooks!',
|
||||
description: 'gadzooks item description',
|
||||
linkText: 'click here',
|
||||
linkUrl: 'about:blank',
|
||||
publishOn: moment(1572489035150),
|
||||
// hash: 'hash2twotwotwotwotwo', // should fail because this is missing
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
describe('modelItems', () => {
|
||||
it('Models empty set with defaults', () => {
|
||||
const driver = getDriver();
|
||||
const apiItems: ApiItem[] = [];
|
||||
expect(driver.modelItems(apiItems)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"feedItems": Array [],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "test_version",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('Selects default language', () => {
|
||||
const driver = getDriver();
|
||||
const apiItems: ApiItem[] = [
|
||||
{
|
||||
title: {
|
||||
en: 'speaking English',
|
||||
es: 'habla Espanol',
|
||||
},
|
||||
description: {
|
||||
en: 'language test',
|
||||
es: 'idiomas',
|
||||
},
|
||||
languages: ['en', 'es'],
|
||||
link_text: {
|
||||
en: 'click here',
|
||||
es: 'aqui',
|
||||
},
|
||||
link_url: {
|
||||
en: 'xyzxyzxyz',
|
||||
es: 'abcabc',
|
||||
},
|
||||
badge: {
|
||||
en: 'firefighter',
|
||||
es: 'bombero',
|
||||
},
|
||||
publish_on: new Date('2014-10-31T04:23:47Z'),
|
||||
expire_on: new Date('2049-10-31T04:23:47Z'),
|
||||
hash: 'abcabc1231123123hash',
|
||||
},
|
||||
];
|
||||
expect(driver.modelItems(apiItems)).toMatchObject({
|
||||
error: null,
|
||||
feedItems: [
|
||||
{
|
||||
badge: 'firefighter',
|
||||
description: 'language test',
|
||||
hash: 'abcabc1231',
|
||||
linkText: 'click here',
|
||||
linkUrl: 'xyzxyzxyz',
|
||||
title: 'speaking English',
|
||||
},
|
||||
],
|
||||
hasNew: true,
|
||||
kibanaVersion: 'test_version',
|
||||
});
|
||||
});
|
||||
|
||||
it("Falls back to English when user language isn't present", () => {
|
||||
// Set Language to French
|
||||
const driver = new NewsfeedApiDriver(kibanaVersion, 'fr', fetchInterval);
|
||||
const apiItems: ApiItem[] = [
|
||||
{
|
||||
title: {
|
||||
en: 'speaking English',
|
||||
fr: 'Le Title',
|
||||
},
|
||||
description: {
|
||||
en: 'not French',
|
||||
fr: 'Le Description',
|
||||
},
|
||||
languages: ['en', 'fr'],
|
||||
link_text: {
|
||||
en: 'click here',
|
||||
fr: 'Le Link Text',
|
||||
},
|
||||
link_url: {
|
||||
en: 'xyzxyzxyz',
|
||||
fr: 'le_url',
|
||||
},
|
||||
badge: {
|
||||
en: 'firefighter',
|
||||
fr: 'le_badge',
|
||||
},
|
||||
publish_on: new Date('2014-10-31T04:23:47Z'),
|
||||
expire_on: new Date('2049-10-31T04:23:47Z'),
|
||||
hash: 'frfrfrfr1231123123hash',
|
||||
}, // fallback: no
|
||||
{
|
||||
title: {
|
||||
en: 'speaking English',
|
||||
es: 'habla Espanol',
|
||||
},
|
||||
description: {
|
||||
en: 'not French',
|
||||
es: 'no Espanol',
|
||||
},
|
||||
languages: ['en', 'es'],
|
||||
link_text: {
|
||||
en: 'click here',
|
||||
es: 'aqui',
|
||||
},
|
||||
link_url: {
|
||||
en: 'xyzxyzxyz',
|
||||
es: 'abcabc',
|
||||
},
|
||||
badge: {
|
||||
en: 'firefighter',
|
||||
es: 'bombero',
|
||||
},
|
||||
publish_on: new Date('2014-10-31T04:23:47Z'),
|
||||
expire_on: new Date('2049-10-31T04:23:47Z'),
|
||||
hash: 'enenenen1231123123hash',
|
||||
}, // fallback: yes
|
||||
];
|
||||
expect(driver.modelItems(apiItems)).toMatchObject({
|
||||
error: null,
|
||||
feedItems: [
|
||||
{
|
||||
badge: 'le_badge',
|
||||
description: 'Le Description',
|
||||
hash: 'frfrfrfr12',
|
||||
linkText: 'Le Link Text',
|
||||
linkUrl: 'le_url',
|
||||
title: 'Le Title',
|
||||
},
|
||||
{
|
||||
badge: 'firefighter',
|
||||
description: 'not French',
|
||||
hash: 'enenenen12',
|
||||
linkText: 'click here',
|
||||
linkUrl: 'xyzxyzxyz',
|
||||
title: 'speaking English',
|
||||
},
|
||||
],
|
||||
hasNew: true,
|
||||
kibanaVersion: 'test_version',
|
||||
});
|
||||
});
|
||||
|
||||
it('Models multiple items into an API FetchResult', () => {
|
||||
const driver = getDriver();
|
||||
const apiItems: ApiItem[] = [
|
||||
{
|
||||
title: {
|
||||
en: 'guess what',
|
||||
},
|
||||
description: {
|
||||
en: 'this tests the modelItems function',
|
||||
},
|
||||
link_text: {
|
||||
en: 'click here',
|
||||
},
|
||||
link_url: {
|
||||
en: 'about:blank',
|
||||
},
|
||||
publish_on: new Date('2014-10-31T04:23:47Z'),
|
||||
expire_on: new Date('2049-10-31T04:23:47Z'),
|
||||
hash: 'abcabc1231123123hash',
|
||||
},
|
||||
{
|
||||
title: {
|
||||
en: 'guess when',
|
||||
},
|
||||
description: {
|
||||
en: 'this also tests the modelItems function',
|
||||
},
|
||||
link_text: {
|
||||
en: 'click here',
|
||||
},
|
||||
link_url: {
|
||||
en: 'about:blank',
|
||||
},
|
||||
badge: {
|
||||
en: 'hero',
|
||||
},
|
||||
publish_on: new Date('2014-10-31T04:23:47Z'),
|
||||
expire_on: new Date('2049-10-31T04:23:47Z'),
|
||||
hash: 'defdefdef456456456',
|
||||
},
|
||||
];
|
||||
expect(driver.modelItems(apiItems)).toMatchObject({
|
||||
error: null,
|
||||
feedItems: [
|
||||
{
|
||||
badge: null,
|
||||
description: 'this tests the modelItems function',
|
||||
hash: 'abcabc1231',
|
||||
linkText: 'click here',
|
||||
linkUrl: 'about:blank',
|
||||
title: 'guess what',
|
||||
},
|
||||
{
|
||||
badge: 'hero',
|
||||
description: 'this also tests the modelItems function',
|
||||
hash: 'defdefdef4',
|
||||
linkText: 'click here',
|
||||
linkUrl: 'about:blank',
|
||||
title: 'guess when',
|
||||
},
|
||||
],
|
||||
hasNew: true,
|
||||
kibanaVersion: 'test_version',
|
||||
});
|
||||
});
|
||||
|
||||
it('Filters expired', () => {
|
||||
const driver = getDriver();
|
||||
const apiItems: ApiItem[] = [
|
||||
{
|
||||
title: {
|
||||
en: 'guess what',
|
||||
},
|
||||
description: {
|
||||
en: 'this tests the modelItems function',
|
||||
},
|
||||
link_text: {
|
||||
en: 'click here',
|
||||
},
|
||||
link_url: {
|
||||
en: 'about:blank',
|
||||
},
|
||||
publish_on: new Date('2013-10-31T04:23:47Z'),
|
||||
expire_on: new Date('2014-10-31T04:23:47Z'), // too old
|
||||
hash: 'abcabc1231123123hash',
|
||||
},
|
||||
];
|
||||
expect(driver.modelItems(apiItems)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"feedItems": Array [],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "test_version",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('Filters pre-published', () => {
|
||||
const driver = getDriver();
|
||||
const apiItems: ApiItem[] = [
|
||||
{
|
||||
title: {
|
||||
en: 'guess what',
|
||||
},
|
||||
description: {
|
||||
en: 'this tests the modelItems function',
|
||||
},
|
||||
link_text: {
|
||||
en: 'click here',
|
||||
},
|
||||
link_url: {
|
||||
en: 'about:blank',
|
||||
},
|
||||
publish_on: new Date('2055-10-31T04:23:47Z'), // too new
|
||||
expire_on: new Date('2056-10-31T04:23:47Z'),
|
||||
hash: 'abcabc1231123123hash',
|
||||
},
|
||||
];
|
||||
expect(driver.modelItems(apiItems)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"feedItems": Array [],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "test_version",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApi', () => {
|
||||
const mockHttpGet = jest.fn();
|
||||
let httpMock = ({
|
||||
fetch: mockHttpGet,
|
||||
} as unknown) as HttpServiceBase;
|
||||
const getHttpMockWithItems = (mockApiItems: ApiItem[]) => (
|
||||
arg1: string,
|
||||
arg2: { method: string }
|
||||
) => {
|
||||
if (
|
||||
arg1 === 'http://fakenews.co/kibana-test/v6.8.2.json' &&
|
||||
arg2.method &&
|
||||
arg2.method === 'GET'
|
||||
) {
|
||||
return Promise.resolve({ items: mockApiItems });
|
||||
}
|
||||
return Promise.reject('wrong args!');
|
||||
};
|
||||
let configMock: NewsfeedPluginInjectedConfig;
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
configMock = {
|
||||
newsfeed: {
|
||||
service: {
|
||||
urlRoot: 'http://fakenews.co',
|
||||
pathTemplate: '/kibana-test/v{VERSION}.json',
|
||||
},
|
||||
defaultLanguage: 'en',
|
||||
mainInterval: 86400000,
|
||||
fetchInterval: 86400000,
|
||||
},
|
||||
};
|
||||
httpMock = ({
|
||||
fetch: mockHttpGet,
|
||||
} as unknown) as HttpServiceBase;
|
||||
});
|
||||
|
||||
it('creates a result', done => {
|
||||
mockHttpGet.mockImplementationOnce(() => Promise.resolve({ items: [] }));
|
||||
getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"feedItems": Array [],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "6.8.2",
|
||||
}
|
||||
`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('hasNew is true when the service returns hashes not in the cache', done => {
|
||||
const mockApiItems: ApiItem[] = [
|
||||
{
|
||||
title: {
|
||||
en: 'speaking English',
|
||||
es: 'habla Espanol',
|
||||
},
|
||||
description: {
|
||||
en: 'language test',
|
||||
es: 'idiomas',
|
||||
},
|
||||
languages: ['en', 'es'],
|
||||
link_text: {
|
||||
en: 'click here',
|
||||
es: 'aqui',
|
||||
},
|
||||
link_url: {
|
||||
en: 'xyzxyzxyz',
|
||||
es: 'abcabc',
|
||||
},
|
||||
badge: {
|
||||
en: 'firefighter',
|
||||
es: 'bombero',
|
||||
},
|
||||
publish_on: new Date('2014-10-31T04:23:47Z'),
|
||||
expire_on: new Date('2049-10-31T04:23:47Z'),
|
||||
hash: 'abcabc1231123123hash',
|
||||
},
|
||||
];
|
||||
|
||||
mockHttpGet.mockImplementationOnce(getHttpMockWithItems(mockApiItems));
|
||||
|
||||
getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"feedItems": Array [
|
||||
Object {
|
||||
"badge": "firefighter",
|
||||
"description": "language test",
|
||||
"expireOn": "2049-10-31T04:23:47.000Z",
|
||||
"hash": "abcabc1231",
|
||||
"linkText": "click here",
|
||||
"linkUrl": "xyzxyzxyz",
|
||||
"publishOn": "2014-10-31T04:23:47.000Z",
|
||||
"title": "speaking English",
|
||||
},
|
||||
],
|
||||
"hasNew": true,
|
||||
"kibanaVersion": "6.8.2",
|
||||
}
|
||||
`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('hasNew is false when service returns hashes that are all stored', done => {
|
||||
localStorageGet.throws('Wrong key passed!');
|
||||
localStorageGet.withArgs(NEWSFEED_HASH_SET_STORAGE_KEY).returns('happyness');
|
||||
const mockApiItems: ApiItem[] = [
|
||||
{
|
||||
title: { en: 'hasNew test' },
|
||||
description: { en: 'test' },
|
||||
link_text: { en: 'click here' },
|
||||
link_url: { en: 'xyzxyzxyz' },
|
||||
badge: { en: 'firefighter' },
|
||||
publish_on: new Date('2014-10-31T04:23:47Z'),
|
||||
expire_on: new Date('2049-10-31T04:23:47Z'),
|
||||
hash: 'happyness',
|
||||
},
|
||||
];
|
||||
mockHttpGet.mockImplementationOnce(getHttpMockWithItems(mockApiItems));
|
||||
getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"feedItems": Array [
|
||||
Object {
|
||||
"badge": "firefighter",
|
||||
"description": "test",
|
||||
"expireOn": "2049-10-31T04:23:47.000Z",
|
||||
"hash": "happyness",
|
||||
"linkText": "click here",
|
||||
"linkUrl": "xyzxyzxyz",
|
||||
"publishOn": "2014-10-31T04:23:47.000Z",
|
||||
"title": "hasNew test",
|
||||
},
|
||||
],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "6.8.2",
|
||||
}
|
||||
`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards an error', done => {
|
||||
mockHttpGet.mockImplementationOnce((arg1, arg2) => Promise.reject('sorry, try again later!'));
|
||||
|
||||
getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "sorry, try again later!",
|
||||
"feedItems": Array [],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "6.8.2",
|
||||
}
|
||||
`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Retry fetching', () => {
|
||||
const successItems: ApiItem[] = [
|
||||
{
|
||||
title: { en: 'hasNew test' },
|
||||
description: { en: 'test' },
|
||||
link_text: { en: 'click here' },
|
||||
link_url: { en: 'xyzxyzxyz' },
|
||||
badge: { en: 'firefighter' },
|
||||
publish_on: new Date('2014-10-31T04:23:47Z'),
|
||||
expire_on: new Date('2049-10-31T04:23:47Z'),
|
||||
hash: 'happyness',
|
||||
},
|
||||
];
|
||||
|
||||
it("retries until fetch doesn't error", done => {
|
||||
configMock.newsfeed.mainInterval = 10; // fast retry for testing
|
||||
mockHttpGet
|
||||
.mockImplementationOnce(() => Promise.reject('Sorry, try again later!'))
|
||||
.mockImplementationOnce(() => Promise.reject('Sorry, internal server error!'))
|
||||
.mockImplementationOnce(() => Promise.reject("Sorry, it's too cold to go outside!"))
|
||||
.mockImplementationOnce(getHttpMockWithItems(successItems));
|
||||
|
||||
getApi(httpMock, configMock.newsfeed, '6.8.2')
|
||||
.pipe(
|
||||
take(4),
|
||||
toArray()
|
||||
)
|
||||
.subscribe(result => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"error": "Sorry, try again later!",
|
||||
"feedItems": Array [],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "6.8.2",
|
||||
},
|
||||
Object {
|
||||
"error": "Sorry, internal server error!",
|
||||
"feedItems": Array [],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "6.8.2",
|
||||
},
|
||||
Object {
|
||||
"error": "Sorry, it's too cold to go outside!",
|
||||
"feedItems": Array [],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "6.8.2",
|
||||
},
|
||||
Object {
|
||||
"error": null,
|
||||
"feedItems": Array [
|
||||
Object {
|
||||
"badge": "firefighter",
|
||||
"description": "test",
|
||||
"expireOn": "2049-10-31T04:23:47.000Z",
|
||||
"hash": "happyness",
|
||||
"linkText": "click here",
|
||||
"linkUrl": "xyzxyzxyz",
|
||||
"publishOn": "2014-10-31T04:23:47.000Z",
|
||||
"title": "hasNew test",
|
||||
},
|
||||
],
|
||||
"hasNew": false,
|
||||
"kibanaVersion": "6.8.2",
|
||||
},
|
||||
]
|
||||
`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't retry if fetch succeeds", done => {
|
||||
configMock.newsfeed.mainInterval = 10; // fast retry for testing
|
||||
mockHttpGet.mockImplementation(getHttpMockWithItems(successItems));
|
||||
|
||||
const timeout$ = interval(1000).pipe(mapTo(undefined)); // lets us capture some results after a short time
|
||||
let timesFetched = 0;
|
||||
|
||||
const get$ = getApi(httpMock, configMock.newsfeed, '6.8.2').pipe(
|
||||
tap(() => {
|
||||
timesFetched++;
|
||||
})
|
||||
);
|
||||
|
||||
race(get$, timeout$).subscribe(() => {
|
||||
expect(timesFetched).toBe(1); // first fetch was successful, so there was no retry
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
194
src/plugins/newsfeed/public/lib/api.ts
Normal file
194
src/plugins/newsfeed/public/lib/api.ts
Normal file
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { catchError, filter, mergeMap, tap } from 'rxjs/operators';
|
||||
import { HttpServiceBase } from 'src/core/public';
|
||||
import {
|
||||
NEWSFEED_FALLBACK_LANGUAGE,
|
||||
NEWSFEED_LAST_FETCH_STORAGE_KEY,
|
||||
NEWSFEED_HASH_SET_STORAGE_KEY,
|
||||
} from '../../constants';
|
||||
import { NewsfeedPluginInjectedConfig, ApiItem, NewsfeedItem, FetchResult } from '../../types';
|
||||
|
||||
type ApiConfig = NewsfeedPluginInjectedConfig['newsfeed']['service'];
|
||||
|
||||
export class NewsfeedApiDriver {
|
||||
private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service
|
||||
|
||||
constructor(
|
||||
private readonly kibanaVersion: string,
|
||||
private readonly userLanguage: string,
|
||||
private readonly fetchInterval: number
|
||||
) {}
|
||||
|
||||
shouldFetch(): boolean {
|
||||
const lastFetchUtc: string | null = sessionStorage.getItem(NEWSFEED_LAST_FETCH_STORAGE_KEY);
|
||||
if (lastFetchUtc == null) {
|
||||
return true;
|
||||
}
|
||||
const last = moment(lastFetchUtc, 'x'); // parse as unix ms timestamp (already is UTC)
|
||||
|
||||
// does the last fetch time precede the time that the page was loaded?
|
||||
if (this.loadedTime.diff(last) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = moment.utc(); // always use UTC to compare timestamps that came from the service
|
||||
const duration = moment.duration(now.diff(last));
|
||||
|
||||
return duration.asMilliseconds() > this.fetchInterval;
|
||||
}
|
||||
|
||||
updateLastFetch() {
|
||||
sessionStorage.setItem(NEWSFEED_LAST_FETCH_STORAGE_KEY, Date.now().toString());
|
||||
}
|
||||
|
||||
updateHashes(items: NewsfeedItem[]): { previous: string[]; current: string[] } {
|
||||
// replace localStorage hashes with new hashes
|
||||
const stored: string | null = localStorage.getItem(NEWSFEED_HASH_SET_STORAGE_KEY);
|
||||
let old: string[] = [];
|
||||
if (stored != null) {
|
||||
old = stored.split(',');
|
||||
}
|
||||
|
||||
const newHashes = items.map(i => i.hash);
|
||||
const updatedHashes = [...new Set(old.concat(newHashes))];
|
||||
localStorage.setItem(NEWSFEED_HASH_SET_STORAGE_KEY, updatedHashes.join(','));
|
||||
|
||||
return { previous: old, current: updatedHashes };
|
||||
}
|
||||
|
||||
fetchNewsfeedItems(http: HttpServiceBase, config: ApiConfig): Rx.Observable<FetchResult> {
|
||||
const urlPath = config.pathTemplate.replace('{VERSION}', this.kibanaVersion);
|
||||
const fullUrl = config.urlRoot + urlPath;
|
||||
|
||||
return Rx.from(
|
||||
http
|
||||
.fetch(fullUrl, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(({ items }) => this.modelItems(items))
|
||||
);
|
||||
}
|
||||
|
||||
validateItem(item: Partial<NewsfeedItem>) {
|
||||
const hasMissing = [
|
||||
item.title,
|
||||
item.description,
|
||||
item.linkText,
|
||||
item.linkUrl,
|
||||
item.publishOn,
|
||||
item.hash,
|
||||
].includes(undefined);
|
||||
|
||||
return !hasMissing;
|
||||
}
|
||||
|
||||
modelItems(items: ApiItem[]): FetchResult {
|
||||
const feedItems: NewsfeedItem[] = items.reduce((accum: NewsfeedItem[], it: ApiItem) => {
|
||||
let chosenLanguage = this.userLanguage;
|
||||
const {
|
||||
expire_on: expireOnUtc,
|
||||
publish_on: publishOnUtc,
|
||||
languages,
|
||||
title,
|
||||
description,
|
||||
link_text: linkText,
|
||||
link_url: linkUrl,
|
||||
badge,
|
||||
hash,
|
||||
} = it;
|
||||
|
||||
if (moment(expireOnUtc).isBefore(Date.now())) {
|
||||
return accum; // ignore item if expired
|
||||
}
|
||||
|
||||
if (moment(publishOnUtc).isAfter(Date.now())) {
|
||||
return accum; // ignore item if publish date hasn't occurred yet (pre-published)
|
||||
}
|
||||
|
||||
if (languages && !languages.includes(chosenLanguage)) {
|
||||
chosenLanguage = NEWSFEED_FALLBACK_LANGUAGE; // don't remove the item: fallback on a language
|
||||
}
|
||||
|
||||
const tempItem: NewsfeedItem = {
|
||||
title: title[chosenLanguage],
|
||||
description: description[chosenLanguage],
|
||||
linkText: linkText[chosenLanguage],
|
||||
linkUrl: linkUrl[chosenLanguage],
|
||||
badge: badge != null ? badge![chosenLanguage] : null,
|
||||
publishOn: moment(publishOnUtc),
|
||||
expireOn: moment(expireOnUtc),
|
||||
hash: hash.slice(0, 10), // optimize for storage and faster parsing
|
||||
};
|
||||
|
||||
if (!this.validateItem(tempItem)) {
|
||||
return accum; // ignore if title, description, etc is missing
|
||||
}
|
||||
|
||||
return [...accum, tempItem];
|
||||
}, []);
|
||||
|
||||
// calculate hasNew
|
||||
const { previous, current } = this.updateHashes(feedItems);
|
||||
const hasNew = current.length > previous.length;
|
||||
|
||||
return {
|
||||
error: null,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
hasNew,
|
||||
feedItems,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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,
|
||||
config: NewsfeedPluginInjectedConfig['newsfeed'],
|
||||
kibanaVersion: string
|
||||
): Rx.Observable<void | FetchResult> {
|
||||
const userLanguage = i18n.getLocale() || config.defaultLanguage;
|
||||
const fetchInterval = config.fetchInterval;
|
||||
const driver = new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval);
|
||||
|
||||
return Rx.timer(0, config.mainInterval).pipe(
|
||||
filter(() => driver.shouldFetch()),
|
||||
mergeMap(() =>
|
||||
driver.fetchNewsfeedItems(http, config.service).pipe(
|
||||
catchError(err => {
|
||||
window.console.error(err);
|
||||
return Rx.of({
|
||||
error: err,
|
||||
kibanaVersion,
|
||||
hasNew: false,
|
||||
feedItems: [],
|
||||
});
|
||||
})
|
||||
)
|
||||
),
|
||||
tap(() => driver.updateLastFetch())
|
||||
);
|
||||
}
|
76
src/plugins/newsfeed/public/plugin.tsx
Normal file
76
src/plugins/newsfeed/public/plugin.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
||||
import { NewsfeedPluginInjectedConfig } from '../types';
|
||||
import { NewsfeedNavButton, NewsfeedApiFetchResult } from './components/newsfeed_header_nav_button';
|
||||
import { getApi } from './lib/api';
|
||||
|
||||
export type Setup = void;
|
||||
export type Start = void;
|
||||
|
||||
export class NewsfeedPublicPlugin implements Plugin<Setup, Start> {
|
||||
private readonly kibanaVersion: string;
|
||||
private readonly stop$ = new Rx.ReplaySubject(1);
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.kibanaVersion = initializerContext.env.packageInfo.version;
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup): Setup {}
|
||||
|
||||
public start(core: CoreStart): Start {
|
||||
const api$ = this.fetchNewsfeed(core);
|
||||
core.chrome.navControls.registerRight({
|
||||
order: 1000,
|
||||
mount: target => this.mount(api$, target),
|
||||
});
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.stop$.next();
|
||||
}
|
||||
|
||||
private fetchNewsfeed(core: CoreStart) {
|
||||
const { http, injectedMetadata } = core;
|
||||
const config = injectedMetadata.getInjectedVar(
|
||||
'newsfeed'
|
||||
) as NewsfeedPluginInjectedConfig['newsfeed'];
|
||||
|
||||
return getApi(http, config, this.kibanaVersion).pipe(
|
||||
takeUntil(this.stop$), // stop the interval when stop method is called
|
||||
catchError(() => Rx.of(null)) // do not throw error
|
||||
);
|
||||
}
|
||||
|
||||
private mount(api$: NewsfeedApiFetchResult, targetDomElement: HTMLElement) {
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<NewsfeedNavButton apiFetchResult={api$} />
|
||||
</I18nProvider>,
|
||||
targetDomElement
|
||||
);
|
||||
return () => ReactDOM.unmountComponentAtNode(targetDomElement);
|
||||
}
|
||||
}
|
63
src/plugins/newsfeed/types.ts
Normal file
63
src/plugins/newsfeed/types.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Moment } from 'moment';
|
||||
|
||||
export interface NewsfeedPluginInjectedConfig {
|
||||
newsfeed: {
|
||||
service: {
|
||||
urlRoot: string;
|
||||
pathTemplate: string;
|
||||
};
|
||||
defaultLanguage: string;
|
||||
mainInterval: number; // how often to check last updated time
|
||||
fetchInterval: number; // how often to fetch remote service and set last updated
|
||||
};
|
||||
}
|
||||
|
||||
export interface ApiItem {
|
||||
hash: string;
|
||||
expire_on: Date;
|
||||
publish_on: Date;
|
||||
title: { [lang: string]: string };
|
||||
description: { [lang: string]: string };
|
||||
link_text: { [lang: string]: string };
|
||||
link_url: { [lang: string]: string };
|
||||
badge?: { [lang: string]: string } | null;
|
||||
languages?: string[] | null;
|
||||
image_url?: null; // not used phase 1
|
||||
}
|
||||
|
||||
export interface NewsfeedItem {
|
||||
title: string;
|
||||
description: string;
|
||||
linkText: string;
|
||||
linkUrl: string;
|
||||
badge: string | null;
|
||||
publishOn: Moment;
|
||||
expireOn: Moment;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export interface FetchResult {
|
||||
kibanaVersion: string;
|
||||
hasNew: boolean;
|
||||
feedItems: NewsfeedItem[];
|
||||
error: Error | null;
|
||||
}
|
|
@ -41,6 +41,7 @@ export function getFunctionalTestGroupRunConfigs({ kibanaInstallDir } = {}) {
|
|||
'scripts/functional_tests',
|
||||
'--include-tag', tag,
|
||||
'--config', 'test/functional/config.js',
|
||||
'--config', 'test/ui_capabilities/newsfeed_err/config.ts',
|
||||
// '--config', 'test/functional/config.firefox.js',
|
||||
'--bail',
|
||||
'--debug',
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { format as formatUrl } from 'url';
|
||||
import { OPTIMIZE_BUNDLE_DIR, esTestConfig, kbnTestConfig } from '@kbn/test';
|
||||
import { services } from './services';
|
||||
|
@ -57,6 +58,10 @@ export default function () {
|
|||
`--kibana.disableWelcomeScreen=true`,
|
||||
'--telemetry.banner=false',
|
||||
`--server.maxPayloadBytes=1679958`,
|
||||
// newsfeed mock service
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'newsfeed')}`,
|
||||
`--newsfeed.service.urlRoot=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`,
|
||||
`--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/v{VERSION}.json`,
|
||||
],
|
||||
},
|
||||
services
|
||||
|
|
33
test/common/fixtures/plugins/newsfeed/index.ts
Normal file
33
test/common/fixtures/plugins/newsfeed/index.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import { initPlugin as initNewsfeed } from './newsfeed_simulation';
|
||||
|
||||
const NAME = 'newsfeed-FTS-external-service-simulators';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
name: NAME,
|
||||
init: (server: Hapi.Server) => {
|
||||
initNewsfeed(server, `/api/_${NAME}`);
|
||||
},
|
||||
});
|
||||
}
|
114
test/common/fixtures/plugins/newsfeed/newsfeed_simulation.ts
Normal file
114
test/common/fixtures/plugins/newsfeed/newsfeed_simulation.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
|
||||
interface WebhookRequest extends Hapi.Request {
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export async function initPlugin(server: Hapi.Server, path: string) {
|
||||
server.route({
|
||||
method: ['GET'],
|
||||
path: `${path}/kibana/v{version}.json`,
|
||||
options: {
|
||||
cors: {
|
||||
origin: ['*'],
|
||||
additionalHeaders: [
|
||||
'Sec-Fetch-Mode',
|
||||
'Access-Control-Request-Method',
|
||||
'Access-Control-Request-Headers',
|
||||
'cache-control',
|
||||
'x-requested-with',
|
||||
'Origin',
|
||||
'User-Agent',
|
||||
'DNT',
|
||||
'content-type',
|
||||
'kbn-version',
|
||||
],
|
||||
},
|
||||
},
|
||||
handler: newsfeedHandler,
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: ['GET'],
|
||||
path: `${path}/kibana/crash.json`,
|
||||
options: {
|
||||
cors: {
|
||||
origin: ['*'],
|
||||
additionalHeaders: [
|
||||
'Sec-Fetch-Mode',
|
||||
'Access-Control-Request-Method',
|
||||
'Access-Control-Request-Headers',
|
||||
'cache-control',
|
||||
'x-requested-with',
|
||||
'Origin',
|
||||
'User-Agent',
|
||||
'DNT',
|
||||
'content-type',
|
||||
'kbn-version',
|
||||
],
|
||||
},
|
||||
},
|
||||
handler() {
|
||||
throw new Error('Internal server error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function newsfeedHandler(request: WebhookRequest, h: any) {
|
||||
return htmlResponse(h, 200, JSON.stringify(mockNewsfeed(request.params.version)));
|
||||
}
|
||||
|
||||
const mockNewsfeed = (version: string) => ({
|
||||
items: [
|
||||
{
|
||||
title: { en: `You are functionally testing the newsfeed widget with fixtures!` },
|
||||
description: { en: 'See test/common/fixtures/plugins/newsfeed/newsfeed_simulation' },
|
||||
link_text: { en: 'Generic feed-viewer could go here' },
|
||||
link_url: { en: 'https://feeds.elastic.co' },
|
||||
languages: null,
|
||||
badge: null,
|
||||
image_url: null,
|
||||
publish_on: '2019-06-21T00:00:00',
|
||||
expire_on: '2019-12-31T00:00:00',
|
||||
hash: '39ca7d409c7eb25f4c69a5a6a11309b2f5ced7ca3f9b3a0109517126e0fd91ca',
|
||||
},
|
||||
{
|
||||
title: { en: 'Staging too!' },
|
||||
description: { en: 'Hello world' },
|
||||
link_text: { en: 'Generic feed-viewer could go here' },
|
||||
link_url: { en: 'https://feeds-staging.elastic.co' },
|
||||
languages: null,
|
||||
badge: null,
|
||||
image_url: null,
|
||||
publish_on: '2019-06-21T00:00:00',
|
||||
expire_on: '2019-12-31T00:00:00',
|
||||
hash: 'db445c9443eb50ea2eb15f20edf89cf0f7dac2b058b11cafc2c8c288b6e4ce2a',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function htmlResponse(h: any, code: number, text: string) {
|
||||
return h
|
||||
.response(text)
|
||||
.type('application/json')
|
||||
.code(code);
|
||||
}
|
7
test/common/fixtures/plugins/newsfeed/package.json
Normal file
7
test/common/fixtures/plugins/newsfeed/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "newsfeed-fixtures",
|
||||
"version": "0.0.0",
|
||||
"kibana": {
|
||||
"version": "kibana"
|
||||
}
|
||||
}
|
63
test/functional/apps/home/_newsfeed.ts
Normal file
63
test/functional/apps/home/_newsfeed.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const globalNav = getService('globalNav');
|
||||
const PageObjects = getPageObjects(['common', 'newsfeed']);
|
||||
|
||||
describe('Newsfeed', () => {
|
||||
before(async () => {
|
||||
await PageObjects.newsfeed.resetPage();
|
||||
});
|
||||
|
||||
it('has red icon which is a sign of not checked news', async () => {
|
||||
const hasCheckedNews = await PageObjects.newsfeed.getRedButtonSign();
|
||||
expect(hasCheckedNews).to.be(true);
|
||||
});
|
||||
|
||||
it('clicking on newsfeed icon should open you newsfeed', async () => {
|
||||
await globalNav.clickNewsfeed();
|
||||
const isOpen = await PageObjects.newsfeed.openNewsfeedPanel();
|
||||
expect(isOpen).to.be(true);
|
||||
});
|
||||
|
||||
it('no red icon, because all news is checked', async () => {
|
||||
const hasCheckedNews = await PageObjects.newsfeed.getRedButtonSign();
|
||||
expect(hasCheckedNews).to.be(false);
|
||||
});
|
||||
|
||||
it('shows all news from newsfeed', async () => {
|
||||
const objects = await PageObjects.newsfeed.getNewsfeedList();
|
||||
expect(objects).to.eql([
|
||||
'21 June 2019\nYou are functionally testing the newsfeed widget with fixtures!\nSee test/common/fixtures/plugins/newsfeed/newsfeed_simulation\nGeneric feed-viewer could go here',
|
||||
'21 June 2019\nStaging too!\nHello world\nGeneric feed-viewer could go here',
|
||||
]);
|
||||
});
|
||||
|
||||
it('clicking on newsfeed icon should close opened newsfeed', async () => {
|
||||
await globalNav.clickNewsfeed();
|
||||
const isOpen = await PageObjects.newsfeed.openNewsfeedPanel();
|
||||
expect(isOpen).to.be(false);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -29,6 +29,7 @@ export default function ({ getService, loadTestFile }) {
|
|||
|
||||
loadTestFile(require.resolve('./_navigation'));
|
||||
loadTestFile(require.resolve('./_home'));
|
||||
loadTestFile(require.resolve('./_newsfeed'));
|
||||
loadTestFile(require.resolve('./_add_data'));
|
||||
loadTestFile(require.resolve('./_sample_data'));
|
||||
});
|
||||
|
|
|
@ -35,6 +35,7 @@ import { HeaderPageProvider } from './header_page';
|
|||
import { HomePageProvider } from './home_page';
|
||||
// @ts-ignore not TS yet
|
||||
import { MonitoringPageProvider } from './monitoring_page';
|
||||
import { NewsfeedPageProvider } from './newsfeed_page';
|
||||
// @ts-ignore not TS yet
|
||||
import { PointSeriesPageProvider } from './point_series_page';
|
||||
// @ts-ignore not TS yet
|
||||
|
@ -61,6 +62,7 @@ export const pageObjects = {
|
|||
header: HeaderPageProvider,
|
||||
home: HomePageProvider,
|
||||
monitoring: MonitoringPageProvider,
|
||||
newsfeed: NewsfeedPageProvider,
|
||||
pointSeries: PointSeriesPageProvider,
|
||||
settings: SettingsPageProvider,
|
||||
share: SharePageProvider,
|
||||
|
|
73
test/functional/page_objects/newsfeed_page.ts
Normal file
73
test/functional/page_objects/newsfeed_page.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export function NewsfeedPageProvider({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
const flyout = getService('flyout');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
|
||||
class NewsfeedPage {
|
||||
async resetPage() {
|
||||
await PageObjects.common.navigateToUrl('home');
|
||||
}
|
||||
|
||||
async closeNewsfeedPanel() {
|
||||
await flyout.ensureClosed('NewsfeedFlyout');
|
||||
log.debug('clickNewsfeed icon');
|
||||
await retry.waitFor('newsfeed flyout', async () => {
|
||||
if (await testSubjects.exists('NewsfeedFlyout')) {
|
||||
await testSubjects.click('NewsfeedFlyout > euiFlyoutCloseButton');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async openNewsfeedPanel() {
|
||||
log.debug('clickNewsfeed icon');
|
||||
return await testSubjects.exists('NewsfeedFlyout');
|
||||
}
|
||||
|
||||
async getRedButtonSign() {
|
||||
return await testSubjects.exists('showBadgeNews');
|
||||
}
|
||||
|
||||
async getNewsfeedList() {
|
||||
const list = await testSubjects.find('NewsfeedFlyout');
|
||||
const cells = await list.findAllByCssSelector('[data-test-subj="newsHeadAlert"]');
|
||||
|
||||
const objects = [];
|
||||
for (const cell of cells) {
|
||||
objects.push(await cell.getVisibleText());
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
async openNewsfeedEmptyPanel() {
|
||||
return await testSubjects.exists('emptyNewsfeed');
|
||||
}
|
||||
}
|
||||
|
||||
return new NewsfeedPage();
|
||||
}
|
|
@ -32,6 +32,10 @@ export function GlobalNavProvider({ getService }: FtrProviderContext) {
|
|||
return await testSubjects.click('headerGlobalNav > logo');
|
||||
}
|
||||
|
||||
public async clickNewsfeed(): Promise<void> {
|
||||
return await testSubjects.click('headerGlobalNav > newsfeed');
|
||||
}
|
||||
|
||||
public async exists(): Promise<boolean> {
|
||||
return await testSubjects.exists('headerGlobalNav');
|
||||
}
|
||||
|
|
45
test/ui_capabilities/newsfeed_err/config.ts
Normal file
45
test/ui_capabilities/newsfeed_err/config.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
|
||||
// @ts-ignore untyped module
|
||||
import getFunctionalConfig from '../../functional/config';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default async ({ readConfigFile }: FtrConfigProviderContext) => {
|
||||
const functionalConfig = await getFunctionalConfig({ readConfigFile });
|
||||
|
||||
return {
|
||||
...functionalConfig,
|
||||
|
||||
testFiles: [require.resolve('./test')],
|
||||
|
||||
kbnTestServer: {
|
||||
...functionalConfig.kbnTestServer,
|
||||
serverArgs: [
|
||||
...functionalConfig.kbnTestServer.serverArgs,
|
||||
`--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/crash.json`,
|
||||
],
|
||||
},
|
||||
|
||||
junit: {
|
||||
reportName: 'Newsfeed Error Handling',
|
||||
},
|
||||
};
|
||||
};
|
60
test/ui_capabilities/newsfeed_err/test.ts
Normal file
60
test/ui_capabilities/newsfeed_err/test.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../functional/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function uiCapabilitiesTests({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const globalNav = getService('globalNav');
|
||||
const PageObjects = getPageObjects(['common', 'newsfeed']);
|
||||
|
||||
describe('Newsfeed icon button handle errors', function() {
|
||||
this.tags('ciGroup6');
|
||||
|
||||
before(async () => {
|
||||
await PageObjects.newsfeed.resetPage();
|
||||
});
|
||||
|
||||
it('clicking on newsfeed icon should open you empty newsfeed', async () => {
|
||||
await globalNav.clickNewsfeed();
|
||||
const isOpen = await PageObjects.newsfeed.openNewsfeedPanel();
|
||||
expect(isOpen).to.be(true);
|
||||
|
||||
const hasNewsfeedEmptyPanel = await PageObjects.newsfeed.openNewsfeedEmptyPanel();
|
||||
expect(hasNewsfeedEmptyPanel).to.be(true);
|
||||
});
|
||||
|
||||
it('no red icon', async () => {
|
||||
const hasCheckedNews = await PageObjects.newsfeed.getRedButtonSign();
|
||||
expect(hasCheckedNews).to.be(false);
|
||||
});
|
||||
|
||||
it('shows empty panel due to error response', async () => {
|
||||
const objects = await PageObjects.newsfeed.getNewsfeedList();
|
||||
expect(objects).to.eql([]);
|
||||
});
|
||||
|
||||
it('clicking on newsfeed icon should close opened newsfeed', async () => {
|
||||
await globalNav.clickNewsfeed();
|
||||
const isOpen = await PageObjects.newsfeed.openNewsfeedPanel();
|
||||
expect(isOpen).to.be(false);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue