Skip to content

Commit

Permalink
Add NewsfeedApiDriver class (#49710)
Browse files Browse the repository at this point in the history
* add NewsfeedApiDriver class

* fix xpack prefix
  • Loading branch information
tsullivan authored Oct 30, 2019
1 parent 55445b6 commit 8561c1e
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 86 deletions.
151 changes: 73 additions & 78 deletions src/plugins/newsfeed/public/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -51,77 +31,92 @@ 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_SERVICE_URL = 'https://feeds.elastic.co/kibana/v7.0.0.json'; // FIXME: should be dynamic
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

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<ApiItem[]> {
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<FetchResult> {
// 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<void | FetchResult> {
export function getApi(
http: HttpServiceBase,
kibanaVersion: string
): Rx.Observable<void | FetchResult> {
const driver = new NewsfeedApiDriver(kibanaVersion);
return Rx.timer(0, NEWSFEED_MAIN_INTERVAL).pipe(
filter(() => shouldFetch()),
mergeMap(
(value: number): Rx.Observable<ApiItem[]> => {
return Rx.from(
http.fetch(NEWSFEED_SERVICE_URL, { method: 'GET' }).then(({ items }) => items)
);
}
),
filter(() => driver.shouldFetch()),
mergeMap(() => driver.fetchNewsfeedItems(http)),
filter(items => items.length > 0),
tap(() => updateLastFetch()),
mergeMap(
(items): Rx.Observable<FetchResult> => {
// calculate hasNew
const { previous, current } = updateHashes(items);
const hasNew = current.length > previous.length;

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

return Rx.of({
hasNew,
feedItems,
});
}
)
tap(() => driver.updateLastFetch()),
mergeMap(items => driver.modelItems(items))
);
}
14 changes: 6 additions & 8 deletions src/plugins/newsfeed/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,20 @@ 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';

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) {}
constructor(initializerContext: PluginInitializerContext) {
this.kibanaVersion = initializerContext.env.packageInfo.version;
}

public setup(core: CoreSetup): Setup {}

Expand All @@ -52,7 +50,7 @@ export class NewsfeedPublicPlugin implements Plugin<Setup, Start> {
});

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?
Expand Down
39 changes: 39 additions & 0 deletions src/plugins/newsfeed/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 8561c1e

Please sign in to comment.