diff --git a/package.json b/package.json index d1f8a44576..4a9eddec0a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "5.1.3", + "version": "5.1.4", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", diff --git a/src/common/catalog-entities/web-link.ts b/src/common/catalog-entities/web-link.ts index e18b3ac931..d54aa8533a 100644 --- a/src/common/catalog-entities/web-link.ts +++ b/src/common/catalog-entities/web-link.ts @@ -24,8 +24,10 @@ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { productName } from "../vars"; import { WeblinkStore } from "../weblink-store"; +export type WebLinkStatusPhase = "available" | "unavailable"; + export interface WebLinkStatus extends CatalogEntityStatus { - phase: "available" | "unavailable"; + phase: WebLinkStatusPhase; } export type WebLinkSpec = { diff --git a/src/common/weblink-store.ts b/src/common/weblink-store.ts index a91f83afa2..234e17537a 100644 --- a/src/common/weblink-store.ts +++ b/src/common/weblink-store.ts @@ -21,7 +21,7 @@ import { action, comparer, observable, makeObservable } from "mobx"; import { BaseStore } from "./base-store"; -import migrations from "../migrations/hotbar-store"; +import migrations from "../migrations/weblinks-store"; import * as uuid from "uuid"; import { toJS } from "./utils"; diff --git a/src/main/catalog-sources/weblinks.ts b/src/main/catalog-sources/weblinks.ts index 2593fedc7c..c8e983f3be 100644 --- a/src/main/catalog-sources/weblinks.ts +++ b/src/main/catalog-sources/weblinks.ts @@ -19,43 +19,19 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { observable, reaction } from "mobx"; +import { computed, observable, reaction } from "mobx"; import { WeblinkStore } from "../../common/weblink-store"; import { WebLink } from "../../common/catalog-entities"; import { catalogEntityRegistry } from "../catalog"; import got from "got"; -import logger from "../logger"; -import { docsUrl, slackUrl } from "../../common/vars"; - -const defaultLinks = [ - { title: "Lens Website", url: "https://k8slens.dev" }, - { title: "Lens Documentation", url: docsUrl }, - { title: "Lens Community Slack", url: slackUrl }, - { title: "Kubernetes Documentation", url: "https://kubernetes.io/docs/home/" }, - { title: "Lens on Twitter", url: "https://twitter.com/k8slens" }, - { title: "Lens Official Blog", url: "https://medium.com/k8slens" } -].map((link) => ( - new WebLink({ - metadata: { - uid: link.url, - name: link.title, - source: "app", - labels: {} - }, - spec: { - url: link.url - }, - status: { - phase: "available", - active: true - } - }) -)); +import type { Disposer } from "../../common/utils"; +import { random } from "lodash"; async function validateLink(link: WebLink) { try { const response = await got.get(link.spec.url, { - throwHttpErrors: false + throwHttpErrors: false, + timeout: 20_000, }); if (response.statusCode >= 200 && response.statusCode < 500) { @@ -63,7 +39,7 @@ async function validateLink(link: WebLink) { } else { link.status.phase = "unavailable"; } - } catch(error) { + } catch { link.status.phase = "unavailable"; } } @@ -71,32 +47,60 @@ async function validateLink(link: WebLink) { export function syncWeblinks() { const weblinkStore = WeblinkStore.getInstance(); - const weblinks = observable.array(defaultLinks); + const webLinkEntities = observable.map(); + + function periodicallyCheckLink(link: WebLink): Disposer { + validateLink(link); + + let interval: NodeJS.Timeout; + const timeout = setTimeout(() => { + interval = setInterval(() => validateLink(link), 60 * 60 * 1000); // every 60 minutes + }, random(0, 5 * 60 * 1000, false)); // spread out over 5 minutes + + return () => { + clearTimeout(timeout); + clearInterval(interval); + }; + } reaction(() => weblinkStore.weblinks, (links) => { - weblinks.replace(links.map((data) => new WebLink({ - metadata: { - uid: data.id, - name: data.name, - source: "local", - labels: {} - }, - spec: { - url: data.url - }, - status: { - phase: "available", - active: true - } - }))); - weblinks.push(...defaultLinks); + const seenWeblinks = new Set(); - for (const link of weblinks) { - validateLink(link).catch((error) => { - logger.error(`failed to validate link ${link.spec.url}: %s`, error); - }); + for (const weblinkData of links) { + seenWeblinks.add(weblinkData.id); + + if (!webLinkEntities.has(weblinkData.id)) { + const link = new WebLink({ + metadata: { + uid: weblinkData.id, + name: weblinkData.name, + source: "local", + labels: {} + }, + spec: { + url: weblinkData.url + }, + status: { + phase: "available", + active: true + } + }); + + webLinkEntities.set(weblinkData.id, [ + link, + periodicallyCheckLink(link), + ]); + } + } + + // Stop checking and remove weblinks that are no longer in the store + for (const [weblinkId, [, disposer]] of webLinkEntities) { + if (!seenWeblinks.has(weblinkId)) { + disposer(); + webLinkEntities.delete(weblinkId); + } } }, {fireImmediately: true}); - catalogEntityRegistry.addObservableSource("weblinks", weblinks); + catalogEntityRegistry.addComputedSource("weblinks", computed(() => Array.from(webLinkEntities.values(), ([link]) => link))); } diff --git a/src/migrations/weblinks-store/5.1.4.ts b/src/migrations/weblinks-store/5.1.4.ts new file mode 100644 index 0000000000..84aaad5e6d --- /dev/null +++ b/src/migrations/weblinks-store/5.1.4.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { docsUrl, slackUrl } from "../../common/vars"; +import type { WeblinkData } from "../../common/weblink-store"; +import type { MigrationDeclaration } from "../helpers"; + +export default { + version: "5.1.4", + run(store) { + const weblinksRaw: any = store.get("weblinks"); + const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[]; + + weblinks.push( + { id: "https://k8slens.dev", name: "Lens Website", url: "https://k8slens.dev" }, + { id: docsUrl, name: "Lens Documentation", url: docsUrl }, + { id: slackUrl, name: "Lens Community Slack", url: slackUrl }, + { id: "https://kubernetes.io/docs/home/", name: "Kubernetes Documentation", url: "https://kubernetes.io/docs/home/" }, + { id: "https://twitter.com/k8slens", name: "Lens on Twitter", url: "https://twitter.com/k8slens" }, + { id: "https://medium.com/k8slens", name: "Lens Official Blog", url: "https://medium.com/k8slens" } + ); + + store.set("weblinks", weblinks); + } +} as MigrationDeclaration; diff --git a/src/migrations/weblinks-store/index.ts b/src/migrations/weblinks-store/index.ts new file mode 100644 index 0000000000..1646b07810 --- /dev/null +++ b/src/migrations/weblinks-store/index.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { joinMigrations } from "../helpers"; + +import version514 from "./5.1.4"; + +export default joinMigrations( + version514, +);