Skip to content

Commit

Permalink
add getApi() to return api data observable (#49581)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsullivan authored Oct 29, 2019
1 parent c1b5e12 commit 7b78a9f
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 0 deletions.
109 changes: 109 additions & 0 deletions x-pack/plugins/newsfeed/public/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,112 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

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

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

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

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

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

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

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

return duration > NEWSFEED_FETCH_INTERVAL;
}

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

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

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

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

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

return Rx.of({
hasNew,
feedItems,
});
}
)
);
}
22 changes: 22 additions & 0 deletions x-pack/plugins/newsfeed/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import * as Rx from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
import ReactDOM from 'react-dom';
import React from 'react';
import {
Expand All @@ -12,12 +14,15 @@ import {
CoreStart,
Plugin,
} from '../../../../src/core/public';
import { getApi } from './lib/api';
import { MailNavButton } from './components/spaces_header_nav_button';

export type Setup = void;
export type Start = void;

export class NewsfeedPublicPlugin implements Plugin<Setup, Start> {
private readonly stop$ = new Rx.ReplaySubject(1);

constructor(initializerContext: PluginInitializerContext) {}

public setup(core: CoreSetup): Setup {}
Expand All @@ -32,5 +37,22 @@ export class NewsfeedPublicPlugin implements Plugin<Setup, Start> {
order: 1000,
mount,
});

const { http } = core;
const api$ = getApi(http).pipe(
takeUntil(this.stop$), // stop the interval when stop method is called
catchError(() => {
// show a message to try again later?
// do not throw error
return Rx.of(null);
})
);

// TODO: pass to component?
api$.subscribe();
}

public stop() {
this.stop$.next();
}
}

0 comments on commit 7b78a9f

Please sign in to comment.