diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 638e86ef375fe..406b75c12b2e0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -162,6 +162,7 @@ # Pulse /packages/kbn-analytics/ @elastic/pulse /src/legacy/core_plugins/ui_metric/ @elastic/pulse +/src/plugins/newsfeed/ @elastic/pulse /src/plugins/telemetry/ @elastic/pulse /src/plugins/telemetry_collection_manager/ @elastic/pulse /src/plugins/telemetry_management_section/ @elastic/pulse diff --git a/src/legacy/core_plugins/newsfeed/index.ts b/src/legacy/core_plugins/newsfeed/index.ts deleted file mode 100644 index cf8852be09a1e..0000000000000 --- a/src/legacy/core_plugins/newsfeed/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 { - 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); -} diff --git a/src/legacy/core_plugins/newsfeed/package.json b/src/legacy/core_plugins/newsfeed/package.json deleted file mode 100644 index d4d753f32b0f9..0000000000000 --- a/src/legacy/core_plugins/newsfeed/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "newsfeed", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/newsfeed/public/index.scss b/src/legacy/core_plugins/newsfeed/public/index.scss deleted file mode 100644 index a77132379041c..0000000000000 --- a/src/legacy/core_plugins/newsfeed/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import './np_ready/components/header_alert/_index'; diff --git a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/_index.scss b/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/_index.scss deleted file mode 100644 index e25dbd25daaf5..0000000000000 --- a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/_index.scss +++ /dev/null @@ -1,27 +0,0 @@ -@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; - } -} diff --git a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert.tsx b/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert.tsx deleted file mode 100644 index c3c3e4144fca8..0000000000000 --- a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 ( - - {(dismiss: any) => ( -
- - -
{date}
-
- {badgeContent} -
- -
{title}
-
{text}
-
{action}
-
- )} -
- ); -}; - -EuiHeaderAlert.propTypes = { - action: PropTypes.node, - className: PropTypes.string, - date: PropTypes.node.isRequired, - text: PropTypes.node, - title: PropTypes.node.isRequired, - badge: PropTypes.node, -}; diff --git a/src/legacy/core_plugins/newsfeed/constants.ts b/src/plugins/newsfeed/common/constants.ts similarity index 65% rename from src/legacy/core_plugins/newsfeed/constants.ts rename to src/plugins/newsfeed/common/constants.ts index 55a0c51c2ac65..6bc95873a342d 100644 --- a/src/legacy/core_plugins/newsfeed/constants.ts +++ b/src/plugins/newsfeed/common/constants.ts @@ -17,7 +17,10 @@ * 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'; +export const NEWSFEED_FALLBACK_LANGUAGE = 'en'; +export const NEWSFEED_LAST_FETCH_STORAGE_KEY = 'newsfeed.lastfetchtime'; +export const NEWSFEED_HASH_SET_STORAGE_KEY = 'newsfeed.hashes'; + +export const NEWSFEED_DEFAULT_SERVICE_BASE_URL = 'https://feeds.elastic.co'; +export const NEWSFEED_DEV_SERVICE_BASE_URL = 'https://feeds-staging.elastic.co'; +export const NEWSFEED_DEFAULT_SERVICE_PATH = '/kibana/v{VERSION}.json'; diff --git a/src/plugins/newsfeed/kibana.json b/src/plugins/newsfeed/kibana.json index 9d49b42424a06..b9f37b67f6921 100644 --- a/src/plugins/newsfeed/kibana.json +++ b/src/plugins/newsfeed/kibana.json @@ -1,6 +1,6 @@ { "id": "newsfeed", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/newsfeed/public/components/flyout_list.tsx b/src/plugins/newsfeed/public/components/flyout_list.tsx index bd554d7d98b7d..6fb5ee673678f 100644 --- a/src/plugins/newsfeed/public/components/flyout_list.tsx +++ b/src/plugins/newsfeed/public/components/flyout_list.tsx @@ -29,9 +29,9 @@ import { EuiButtonEmpty, EuiText, EuiBadge, + EuiHeaderAlert, } 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'; diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts index 9d64dd26da047..b77e63512c8c0 100644 --- a/src/plugins/newsfeed/public/lib/api.test.ts +++ b/src/plugins/newsfeed/public/lib/api.test.ts @@ -22,8 +22,11 @@ import { interval, race } from 'rxjs'; import sinon, { stub } from 'sinon'; import moment from 'moment'; import { HttpSetup } from 'src/core/public'; -import { NEWSFEED_HASH_SET_STORAGE_KEY, NEWSFEED_LAST_FETCH_STORAGE_KEY } from '../../constants'; -import { ApiItem, NewsfeedItem, NewsfeedPluginInjectedConfig } from '../../types'; +import { + NEWSFEED_HASH_SET_STORAGE_KEY, + NEWSFEED_LAST_FETCH_STORAGE_KEY, +} from '../../common/constants'; +import { ApiItem, NewsfeedItem, NewsfeedPluginBrowserConfig } from '../../types'; import { NewsfeedApiDriver, getApi } from './api'; const localStorageGet = sinon.stub(); @@ -458,7 +461,7 @@ describe('getApi', () => { } return Promise.reject('wrong args!'); }; - let configMock: NewsfeedPluginInjectedConfig; + let configMock: NewsfeedPluginBrowserConfig; afterEach(() => { jest.resetAllMocks(); @@ -466,15 +469,13 @@ describe('getApi', () => { beforeEach(() => { configMock = { - newsfeed: { - service: { - urlRoot: 'http://fakenews.co', - pathTemplate: '/kibana-test/v{VERSION}.json', - }, - defaultLanguage: 'en', - mainInterval: 86400000, - fetchInterval: 86400000, + service: { + urlRoot: 'http://fakenews.co', + pathTemplate: '/kibana-test/v{VERSION}.json', }, + defaultLanguage: 'en', + mainInterval: 86400000, + fetchInterval: 86400000, }; httpMock = ({ fetch: mockHttpGet, @@ -483,7 +484,7 @@ describe('getApi', () => { it('creates a result', done => { mockHttpGet.mockImplementationOnce(() => Promise.resolve({ items: [] })); - getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + getApi(httpMock, configMock, '6.8.2').subscribe(result => { expect(result).toMatchInlineSnapshot(` Object { "error": null, @@ -528,7 +529,7 @@ describe('getApi', () => { mockHttpGet.mockImplementationOnce(getHttpMockWithItems(mockApiItems)); - getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + getApi(httpMock, configMock, '6.8.2').subscribe(result => { expect(result).toMatchInlineSnapshot(` Object { "error": null, @@ -568,7 +569,7 @@ describe('getApi', () => { }, ]; mockHttpGet.mockImplementationOnce(getHttpMockWithItems(mockApiItems)); - getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + getApi(httpMock, configMock, '6.8.2').subscribe(result => { expect(result).toMatchInlineSnapshot(` Object { "error": null, @@ -595,7 +596,7 @@ describe('getApi', () => { it('forwards an error', done => { mockHttpGet.mockImplementationOnce((arg1, arg2) => Promise.reject('sorry, try again later!')); - getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + getApi(httpMock, configMock, '6.8.2').subscribe(result => { expect(result).toMatchInlineSnapshot(` Object { "error": "sorry, try again later!", @@ -623,14 +624,14 @@ describe('getApi', () => { ]; it("retries until fetch doesn't error", done => { - configMock.newsfeed.mainInterval = 10; // fast retry for testing + configMock.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') + getApi(httpMock, configMock, '6.8.2') .pipe(take(4), toArray()) .subscribe(result => { expect(result).toMatchInlineSnapshot(` @@ -677,13 +678,13 @@ describe('getApi', () => { }); it("doesn't retry if fetch succeeds", done => { - configMock.newsfeed.mainInterval = 10; // fast retry for testing + configMock.mainInterval = 10; // fast retry for testing mockHttpGet.mockImplementation(getHttpMockWithItems(successItems)); const timeout$ = interval(1000); // lets us capture some results after a short time let timesFetched = 0; - const get$ = getApi(httpMock, configMock.newsfeed, '6.8.2').pipe( + const get$ = getApi(httpMock, configMock, '6.8.2').pipe( tap(() => { timesFetched++; }) diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts index bfeff4aa3e37b..5088ddaec81ea 100644 --- a/src/plugins/newsfeed/public/lib/api.ts +++ b/src/plugins/newsfeed/public/lib/api.ts @@ -26,10 +26,10 @@ import { NEWSFEED_FALLBACK_LANGUAGE, NEWSFEED_LAST_FETCH_STORAGE_KEY, NEWSFEED_HASH_SET_STORAGE_KEY, -} from '../../constants'; -import { NewsfeedPluginInjectedConfig, ApiItem, NewsfeedItem, FetchResult } from '../../types'; +} from '../../common/constants'; +import { ApiItem, NewsfeedItem, FetchResult, NewsfeedPluginBrowserConfig } from '../../types'; -type ApiConfig = NewsfeedPluginInjectedConfig['newsfeed']['service']; +type ApiConfig = NewsfeedPluginBrowserConfig['service']; export class NewsfeedApiDriver { private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service @@ -167,7 +167,7 @@ export class NewsfeedApiDriver { */ export function getApi( http: HttpSetup, - config: NewsfeedPluginInjectedConfig['newsfeed'], + config: NewsfeedPluginBrowserConfig, kibanaVersion: string ): Rx.Observable { const userLanguage = i18n.getLocale() || config.defaultLanguage; diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx index d21cf75a1a65e..a1b2b265853e0 100644 --- a/src/plugins/newsfeed/public/plugin.tsx +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -23,7 +23,7 @@ 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 { FetchResult, NewsfeedPluginInjectedConfig } from '../types'; +import { NewsfeedPluginBrowserConfig } from '../types'; import { NewsfeedNavButton, NewsfeedApiFetchResult } from './components/newsfeed_header_nav_button'; import { getApi } from './lib/api'; @@ -32,9 +32,11 @@ export type Start = object; export class NewsfeedPublicPlugin implements Plugin { private readonly kibanaVersion: string; + private readonly config: NewsfeedPluginBrowserConfig; private readonly stop$ = new Rx.ReplaySubject(1); - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.get(); this.kibanaVersion = initializerContext.env.packageInfo.version; } @@ -57,16 +59,8 @@ export class NewsfeedPublicPlugin implements Plugin { } private fetchNewsfeed(core: CoreStart) { - const { http, injectedMetadata } = core; - const config = injectedMetadata.getInjectedVar('newsfeed') as - | NewsfeedPluginInjectedConfig['newsfeed'] - | undefined; - - if (!config) { - // running in new platform, injected metadata not available - return new Rx.Observable(); - } - return getApi(http, config, this.kibanaVersion).pipe( + const { http } = core; + return getApi(http, this.config, this.kibanaVersion).pipe( takeUntil(this.stop$), // stop the interval when stop method is called catchError(() => Rx.of(null)) // do not throw error ); diff --git a/src/plugins/newsfeed/server/config.ts b/src/plugins/newsfeed/server/config.ts new file mode 100644 index 0000000000000..234bc2d83c467 --- /dev/null +++ b/src/plugins/newsfeed/server/config.ts @@ -0,0 +1,43 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { + NEWSFEED_DEFAULT_SERVICE_PATH, + NEWSFEED_DEFAULT_SERVICE_BASE_URL, + NEWSFEED_DEV_SERVICE_BASE_URL, +} from '../common/constants'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + service: schema.object({ + pathTemplate: schema.string({ defaultValue: NEWSFEED_DEFAULT_SERVICE_PATH }), + urlRoot: schema.conditional( + schema.contextRef('prod'), + schema.literal(true), // Point to staging if it's not a production release + schema.string({ defaultValue: NEWSFEED_DEFAULT_SERVICE_BASE_URL }), + schema.string({ defaultValue: NEWSFEED_DEV_SERVICE_BASE_URL }) + ), + }), + defaultLanguage: schema.string({ defaultValue: 'en' }), + mainInterval: schema.number({ defaultValue: 120 * 1000 }), // (2min) How often to retry failed fetches, and/or check if newsfeed items need to be refreshed from remote + fetchInterval: schema.number({ defaultValue: 86400 * 1000 }), // (1day) How often to fetch remote and reset the last fetched time +}); + +export type NewsfeedConfigType = TypeOf; diff --git a/src/plugins/newsfeed/server/index.ts b/src/plugins/newsfeed/server/index.ts new file mode 100644 index 0000000000000..71852f38d41d3 --- /dev/null +++ b/src/plugins/newsfeed/server/index.ts @@ -0,0 +1,36 @@ +/* + * 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 { PluginConfigDescriptor } from 'kibana/server'; +import { NewsfeedPlugin } from './plugin'; +import { configSchema, NewsfeedConfigType } from './config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + service: true, + defaultLanguage: true, + mainInterval: true, + fetchInterval: true, + }, +}; + +export function plugin() { + return new NewsfeedPlugin(); +} diff --git a/src/plugins/newsfeed/constants.ts b/src/plugins/newsfeed/server/plugin.ts similarity index 81% rename from src/plugins/newsfeed/constants.ts rename to src/plugins/newsfeed/server/plugin.ts index ddcbbb6cb1dbe..60b69b3977bf5 100644 --- a/src/plugins/newsfeed/constants.ts +++ b/src/plugins/newsfeed/server/plugin.ts @@ -17,6 +17,12 @@ * 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'; +import { Plugin } from 'kibana/server'; + +export class NewsfeedPlugin implements Plugin { + public setup() {} + + public start() {} + + public stop() {} +} diff --git a/src/plugins/newsfeed/types.ts b/src/plugins/newsfeed/types.ts index 78485c6ee4f59..8eb5af1bf8f4b 100644 --- a/src/plugins/newsfeed/types.ts +++ b/src/plugins/newsfeed/types.ts @@ -19,16 +19,15 @@ 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 +// Ideally, we may want to obtain the type from the configSchema and exposeToBrowser keys... +export interface NewsfeedPluginBrowserConfig { + 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 {