From 9c1e2b7d9aadeee0c22671eff539bbf3cbffa805 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 29 Oct 2019 22:04:59 -0700 Subject: [PATCH 1/2] add NewsfeedApiDriver class --- src/plugins/newsfeed/public/lib/api.ts | 147 ++++++++++++------------- src/plugins/newsfeed/public/plugin.tsx | 14 +-- src/plugins/newsfeed/types.ts | 39 +++++++ 3 files changed, 116 insertions(+), 84 deletions(-) create mode 100644 src/plugins/newsfeed/types.ts diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts index c8e7ab428c9b2..de6fa6af7d6ee 100644 --- a/src/plugins/newsfeed/public/lib/api.ts +++ b/src/plugins/newsfeed/public/lib/api.ts @@ -21,27 +21,7 @@ import * as Rx from 'rxjs'; import moment from 'moment'; import { filter, mergeMap, tap } from 'rxjs/operators'; import { HttpServiceBase } from '../../../../../src/core/public'; - -interface ApiItem { - hash: string; - expire_on: Date; - title: { [lang: string]: string }; - description: { [lang: string]: string }; - link_text: { [lang: string]: string }; - link_url: { [lang: string]: string }; - - badge: null; // not used phase 1 - image_url: null; // not used phase 1 - languages: null; // not used phase 1 - publish_on: null; // not used phase 1 -} - -interface NewsfeedItem { - title: string; - description: string; - linkText: string; - linkUrl: string; -} +import { ApiItem, NewsfeedItem } from '../../types'; interface FetchResult { hasNew: boolean; @@ -53,75 +33,90 @@ const NEWSFEED_MAIN_INTERVAL = 120000; // A main interval to check for need to r const NEWSFEED_FETCH_INTERVAL = moment.duration(1, 'day'); // how often to actually fetch the API const NEWSFEED_LAST_FETCH_STORAGE_KEY = 'xpack.newsfeed.lastfetchtime'; const NEWSFEED_HASH_SET_STORAGE_KEY = 'xpack.newsfeed.hashes'; -const NEWSFEED_SERVICE_URL = 'https://feeds.elastic.co/kibana/v7.0.0.json'; // FIXME: should be dynamic +// const NEWSFEED_SERVICE_URL_TEMPLATE = 'https://feeds.elastic.co/kibana/v{VERSION}.json'; +const NEWSFEED_SERVICE_URL_TEMPLATE = 'https://feeds.elastic.co/kibana/v7.4.1.json'; // FIXME: needs to support untagged dev branches -function shouldFetch(): boolean { - const lastFetch: string | null = localStorage.getItem(NEWSFEED_LAST_FETCH_STORAGE_KEY); - if (lastFetch == null) { - return true; +class NewsfeedApiDriver { + constructor(private readonly kibanaVersion: string) {} + + shouldFetch(): boolean { + const lastFetch: string | null = sessionStorage.getItem(NEWSFEED_LAST_FETCH_STORAGE_KEY); + if (lastFetch == null) { + return true; + } + const last = moment(lastFetch, 'x'); // parse as unix ms timestamp + const now = moment(); + const duration = moment.duration(now.diff(last)); + + return duration > NEWSFEED_FETCH_INTERVAL; } - const last = moment(lastFetch, 'x'); // parse as unix ms timestamp - const now = moment(); - const duration = moment.duration(now.diff(last)); - return duration > NEWSFEED_FETCH_INTERVAL; -} + updateLastFetch() { + sessionStorage.setItem(NEWSFEED_LAST_FETCH_STORAGE_KEY, Date.now().toString()); + } -function updateLastFetch() { - localStorage.setItem(NEWSFEED_LAST_FETCH_STORAGE_KEY, Date.now().toString()); -} + updateHashes(items: ApiItem[]): { previous: string[]; current: string[] } { + // combine localStorage hashes with new hashes + const hashSet: string | null = localStorage.getItem(NEWSFEED_HASH_SET_STORAGE_KEY); + let oldHashes: string[] = []; + if (hashSet != null) { + oldHashes = hashSet.split(','); + } + const newHashes = items.map(i => i.hash.slice(0, 10)); + const updatedHashes = [...new Set(oldHashes.concat(newHashes))]; + localStorage.setItem(NEWSFEED_HASH_SET_STORAGE_KEY, updatedHashes.join(',')); + + return { previous: oldHashes, current: updatedHashes }; + } -function updateHashes(items: ApiItem[]): { previous: string[]; current: string[] } { - // combine localStorage hashes with new hashes - const hashSet: string | null = localStorage.getItem(NEWSFEED_HASH_SET_STORAGE_KEY); - let oldHashes: string[] = []; - if (hashSet != null) { - oldHashes = hashSet.split(','); + fetchNewsfeedItems(http: HttpServiceBase): Rx.Observable { + return Rx.from( + http + .fetch(NEWSFEED_SERVICE_URL_TEMPLATE.replace('VERSION', this.kibanaVersion), { + method: 'GET', + }) + .then(({ items }) => items) + ); } - const newHashes = items.map(i => i.hash.slice(0, 10)); - const updatedHashes = [...new Set(oldHashes.concat(newHashes))]; - localStorage.setItem(NEWSFEED_HASH_SET_STORAGE_KEY, updatedHashes.join(',')); - return { previous: oldHashes, current: updatedHashes }; + modelItems(items: ApiItem[]): Rx.Observable { + // calculate hasNew + const { previous, current } = this.updateHashes(items); + const hasNew = current.length > previous.length; + + // model feed items + const feedItems: NewsfeedItem[] = items.map(it => { + return { + title: it.title[DEFAULT_LANGUAGE], + description: it.description[DEFAULT_LANGUAGE], + linkText: it.link_text[DEFAULT_LANGUAGE], + linkUrl: it.link_url[DEFAULT_LANGUAGE], + badge: it.badge != null ? it.badge![DEFAULT_LANGUAGE] : it.badge, + languages: it.languages, + }; + }); + + return Rx.of({ + 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): Rx.Observable { +export function getApi( + http: HttpServiceBase, + kibanaVersion: string +): Rx.Observable { + const driver = new NewsfeedApiDriver(kibanaVersion); return Rx.timer(0, NEWSFEED_MAIN_INTERVAL).pipe( - filter(() => shouldFetch()), - mergeMap( - (value: number): Rx.Observable => { - return Rx.from( - http.fetch(NEWSFEED_SERVICE_URL, { method: 'GET' }).then(({ items }) => items) - ); - } - ), + filter(() => driver.shouldFetch()), + mergeMap(() => driver.fetchNewsfeedItems(http)), filter(items => items.length > 0), - tap(() => updateLastFetch()), - mergeMap( - (items): Rx.Observable => { - // calculate hasNew - const { previous, current } = updateHashes(items); - const hasNew = current.length > previous.length; - - // model feed items - const feedItems: NewsfeedItem[] = items.map(it => { - return { - title: it.title[DEFAULT_LANGUAGE], - description: it.description[DEFAULT_LANGUAGE], - linkText: it.link_text[DEFAULT_LANGUAGE], - linkUrl: it.link_url[DEFAULT_LANGUAGE], - }; - }); - - return Rx.of({ - hasNew, - feedItems, - }); - } - ) + tap(() => driver.updateLastFetch()), + mergeMap(items => driver.modelItems(items)) ); } diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx index df9850a310489..51ae86c1d1906 100644 --- a/src/plugins/newsfeed/public/plugin.tsx +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -21,12 +21,7 @@ import * as Rx from 'rxjs'; import { catchError, takeUntil } from 'rxjs/operators'; import ReactDOM from 'react-dom'; import React from 'react'; -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, -} from '../../../../src/core/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { getApi } from './lib/api'; import { MailNavButton } from './components/newsfeed_header_nav_button'; @@ -34,9 +29,12 @@ export type Setup = void; export type Start = void; export class NewsfeedPublicPlugin implements Plugin { + private readonly kibanaVersion: string; private readonly stop$ = new Rx.ReplaySubject(1); - constructor(initializerContext: PluginInitializerContext) {} + constructor(initializerContext: PluginInitializerContext) { + this.kibanaVersion = initializerContext.env.packageInfo.version; + } public setup(core: CoreSetup): Setup {} @@ -52,7 +50,7 @@ export class NewsfeedPublicPlugin implements Plugin { }); const { http } = core; - const api$ = getApi(http).pipe( + const api$ = getApi(http, this.kibanaVersion).pipe( takeUntil(this.stop$), // stop the interval when stop method is called catchError(() => { // show a message to try again later? diff --git a/src/plugins/newsfeed/types.ts b/src/plugins/newsfeed/types.ts new file mode 100644 index 0000000000000..88d02663431f2 --- /dev/null +++ b/src/plugins/newsfeed/types.ts @@ -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. + */ + +export interface ApiItem { + hash: string; + expire_on: Date; + title: { [lang: string]: string }; + description: { [lang: string]: string }; + link_text: { [lang: string]: string }; + link_url: { [lang: string]: string }; + + badge: null; // not used phase 1 + image_url: null; // not used phase 1 + languages: null; // not used phase 1 + publish_on: null; // not used phase 1 +} + +export interface NewsfeedItem { + title: string; + description: string; + linkText: string; + linkUrl: string; +} From f34d94c12cd2b2457ab5dec48437198ab25378d0 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 29 Oct 2019 22:16:43 -0700 Subject: [PATCH 2/2] fix xpack prefix --- src/plugins/newsfeed/public/lib/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts index de6fa6af7d6ee..8ecfcc845cde6 100644 --- a/src/plugins/newsfeed/public/lib/api.ts +++ b/src/plugins/newsfeed/public/lib/api.ts @@ -31,8 +31,8 @@ interface FetchResult { const DEFAULT_LANGUAGE = 'en'; // TODO: read from settings, default to en const NEWSFEED_MAIN_INTERVAL = 120000; // A main interval to check for need to refresh (2min) const NEWSFEED_FETCH_INTERVAL = moment.duration(1, 'day'); // how often to actually fetch the API -const NEWSFEED_LAST_FETCH_STORAGE_KEY = 'xpack.newsfeed.lastfetchtime'; -const NEWSFEED_HASH_SET_STORAGE_KEY = 'xpack.newsfeed.hashes'; +const NEWSFEED_LAST_FETCH_STORAGE_KEY = 'newsfeed.lastfetchtime'; +const NEWSFEED_HASH_SET_STORAGE_KEY = 'newsfeed.hashes'; // const NEWSFEED_SERVICE_URL_TEMPLATE = 'https://feeds.elastic.co/kibana/v{VERSION}.json'; const NEWSFEED_SERVICE_URL_TEMPLATE = 'https://feeds.elastic.co/kibana/v7.4.1.json'; // FIXME: needs to support untagged dev branches