1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fully split apart the weblinks storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-03-17 11:47:47 -04:00
parent ae32375beb
commit 037c62d426
30 changed files with 448 additions and 253 deletions

27
package-lock.json generated
View File

@ -6416,6 +6416,7 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"dev": true,
"engines": {
"node": ">=10"
},
@ -6673,6 +6674,7 @@
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dev": true,
"dependencies": {
"defer-to-connect": "^2.0.0"
},
@ -6941,6 +6943,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dev": true,
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
@ -7199,7 +7202,8 @@
"node_modules/@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==",
"dev": true
},
"node_modules/@types/http-proxy": {
"version": "1.17.10",
@ -7325,6 +7329,7 @@
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@ -7620,6 +7625,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@ -10137,6 +10143,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"dev": true,
"engines": {
"node": ">=10.6.0"
}
@ -10145,6 +10152,7 @@
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
"dev": true,
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
@ -10162,6 +10170,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"dependencies": {
"pump": "^3.0.0"
},
@ -10176,6 +10185,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@ -12008,6 +12018,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"dev": true,
"engines": {
"node": ">=10"
}
@ -16026,6 +16037,7 @@
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"dev": true,
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
@ -16648,6 +16660,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"dev": true,
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
@ -20779,7 +20792,8 @@
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
},
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
@ -21079,6 +21093,7 @@
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
"integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
"dev": true,
"dependencies": {
"json-buffer": "3.0.1"
}
@ -23742,6 +23757,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"dev": true,
"engines": {
"node": ">=10"
},
@ -27098,6 +27114,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -28701,6 +28718,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
"engines": {
"node": ">=10"
},
@ -29816,7 +29834,8 @@
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true
},
"node_modules/resolve-cwd": {
"version": "3.0.0",
@ -29860,6 +29879,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dev": true,
"dependencies": {
"lowercase-keys": "^2.0.0"
},
@ -34173,7 +34193,6 @@
"filehound": "^1.17.6",
"fs-extra": "^9.0.1",
"glob-to-regexp": "^0.4.1",
"got": "^11.8.6",
"grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.7",
"history": "^4.10.1",

View File

@ -143,7 +143,6 @@
"filehound": "^1.17.6",
"fs-extra": "^9.0.1",
"glob-to-regexp": "^0.4.1",
"got": "^11.8.6",
"grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.7",
"history": "^4.10.1",

View File

@ -4,10 +4,10 @@
*/
import { getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import removeWeblinkInjectable from "../../features/weblinks/common/remove.injectable";
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
import productNameInjectable from "../vars/product-name.injectable";
import weblinkStoreInjectable from "../weblinks-store/weblink-store.injectable";
export type WebLinkStatusPhase = "available" | "unavailable";
@ -34,13 +34,13 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
// NOTE: this is safe because `onContextMenuOpen` is only supposed to be called in the renderer
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi("renderer");
const productName = di.inject(productNameInjectable);
const weblinkStore = di.inject(weblinkStoreInjectable);
const removeWeblink = di.inject(removeWeblinkInjectable);
if (this.metadata.source === "local") {
context.menuItems.push({
title: "Delete",
icon: "delete",
onClick: async () => weblinkStore.removeById(this.getId()),
onClick: () => removeWeblink(this.getId()),
confirm: {
message: `Remove Web Link "${this.getName()}" from ${productName}?`,
},

View File

@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import createPersistentStorageInjectable from "../persistent-storage/create.injectable";
import persistentStorageMigrationsInjectable from "../persistent-storage/migrations.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import { weblinkStoreMigrationInjectionToken } from "./migration-token";
import { WeblinkStore } from "./weblink-store";
const weblinkStoreInjectable = getInjectable({
id: "weblink-store",
instantiate: (di) => new WeblinkStore({
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(persistentStorageMigrationsInjectable, weblinkStoreMigrationInjectionToken),
createPersistentStorage: di.inject(createPersistentStorageInjectable),
}),
});
export default weblinkStoreInjectable;

View File

@ -1,79 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, comparer, observable, runInAction } from "mobx";
import * as uuid from "uuid";
import type { CreatePersistentStorage, PersistentStorage } from "../persistent-storage/create.injectable";
import type { Migrations } from "conf/dist/source/types";
export interface WeblinkData {
id: string;
name: string;
url: string;
}
export interface WeblinkCreateOptions {
id?: string;
name: string;
url: string;
}
export interface WeblinkStoreModel {
weblinks: WeblinkData[];
}
interface Dependencies {
readonly storeMigrationVersion: string;
readonly migrations: Migrations<Record<string, unknown>>;
createPersistentStorage: CreatePersistentStorage;
}
export class WeblinkStore {
private readonly store: PersistentStorage;
readonly weblinks = observable.array<WeblinkData>();
constructor(private readonly dependencies: Dependencies) {
this.store = this.dependencies.createPersistentStorage<WeblinkStoreModel>({
configName: "lens-weblink-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
projectVersion: this.dependencies.storeMigrationVersion,
migrations: this.dependencies.migrations,
fromStore: action(({ weblinks = [] }) => {
this.weblinks.replace(weblinks);
}),
toJSON: () => ({
weblinks: this.weblinks.toJSON(),
}),
});
this.store.loadAndStartSyncing();
}
add(data: WeblinkCreateOptions) {
return runInAction(() => {
const {
id = uuid.v4(),
name,
url,
} = data;
const weblink: WeblinkData = { id, name, url };
this.weblinks.push(weblink);
return weblink;
});
}
removeById(id: string) {
runInAction(() => {
this.weblinks.replace(this.weblinks.filter((w) => w.id !== id));
});
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { action } from "mobx";
import weblinksStateInjectable from "./state.injectable";
import type { WeblinkData } from "./storage.injectable";
import * as uuid from "uuid";
import { getOrInsert } from "@k8slens/utilities";
export interface WeblinkCreateOptions {
id?: string;
name: string;
url: string;
}
export type AddWeblink = (data: WeblinkCreateOptions) => WeblinkData;
const addWeblinkInjectable = getInjectable({
id: "add-weblink",
instantiate: (di): AddWeblink => {
const state = di.inject(weblinksStateInjectable);
return action((data) => {
const {
id = uuid.v4(),
name,
url,
} = data;
if (state.has(id)) {
throw new Error(`There already exists a weblink with id=${id}`);
}
return getOrInsert(state, id, { id, name, url });
});
},
});
export default addWeblinkInjectable;

View File

@ -4,7 +4,7 @@
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MigrationDeclaration } from "../persistent-storage/migrations.injectable";
import type { MigrationDeclaration } from "../../../common/persistent-storage/migrations.injectable";
export const weblinkStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "weblink-store-migration-token",

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { action } from "mobx";
import weblinksStateInjectable from "./state.injectable";
export type RemoveWeblink = (id: string) => void;
const removeWeblinkInjectable = getInjectable({
id: "remove-weblink",
instantiate: (di): RemoveWeblink => {
const state = di.inject(weblinksStateInjectable);
return action((id) => state.delete(id));
},
});
export default removeWeblinkInjectable;

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
import type { WeblinkData } from "./storage.injectable";
const weblinksStateInjectable = getInjectable({
id: "weblinks-state",
instantiate: () => observable.map<string, WeblinkData>(),
});
export default weblinksStateInjectable;

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { action, comparer, toJS } from "mobx";
import createPersistentStorageInjectable from "../../../common/persistent-storage/create.injectable";
import persistentStorageMigrationsInjectable from "../../../common/persistent-storage/migrations.injectable";
import storeMigrationVersionInjectable from "../../../common/vars/store-migration-version.injectable";
import { weblinkStoreMigrationInjectionToken } from "./migration-token";
import weblinksStateInjectable from "./state.injectable";
export interface WeblinkData {
id: string;
name: string;
url: string;
}
export interface WeblinkStoreModel {
weblinks: WeblinkData[];
}
const weblinksPersistentStorageInjectable = getInjectable({
id: "weblinks-persistent-storage",
instantiate: (di) => {
const state = di.inject(weblinksStateInjectable);
const createPersistentStorage = di.inject(createPersistentStorageInjectable);
return createPersistentStorage<WeblinkStoreModel>({
configName: "lens-weblink-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
projectVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(persistentStorageMigrationsInjectable, weblinkStoreMigrationInjectionToken),
fromStore: action(({ weblinks = [] }) => {
state.replace(weblinks.map(weblink => [weblink.id, weblink]));
}),
toJSON: () => ({
weblinks: [...toJS(state).values()],
}),
});
},
});
export default weblinksPersistentStorageInjectable;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import weblinksStateInjectable from "./state.injectable";
const weblinksInjectable = getInjectable({
id: "weblinks",
instantiate: (di) => {
const state = di.inject(weblinksStateInjectable);
return computed(() => [...state.values()]);
},
});
export default weblinksInjectable;

View File

@ -4,10 +4,10 @@
*/
import { docsUrl, forumsUrl } from "../../../common/vars";
import type { WeblinkData } from "../../../common/weblinks-store/weblink-store";
import { getInjectable } from "@ogre-tools/injectable";
import { weblinkStoreMigrationInjectionToken } from "../../../common/weblinks-store/migration-token";
import * as links from "../links";
import { weblinkStoreMigrationInjectionToken } from "../common/migration-token";
import * as links from "./links";
import type { WeblinkData } from "../common/storage.injectable";
const v514WeblinkStoreMigrationInjectable = getInjectable({
id: "v5.1.4-weblink-store-migration",

View File

@ -3,10 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { WeblinkData } from "../../../common/weblinks-store/weblink-store";
import * as links from "../links";
import * as links from "../../../features/weblinks/main/links";
import { getInjectable } from "@ogre-tools/injectable";
import { weblinkStoreMigrationInjectionToken } from "../../../common/weblinks-store/migration-token";
import { weblinkStoreMigrationInjectionToken } from "../../../features/weblinks/common/migration-token";
import type { WeblinkData } from "../common/storage.injectable";
const v545Beta1WeblinkStoreMigrationInjectable = getInjectable({
id: "v5.4.5-beta.1-weblink-store-migration",

View File

@ -4,11 +4,11 @@
*/
import { docsUrl, forumsUrl } from "../../../common/vars";
import type { WeblinkData } from "../../../common/weblinks-store/weblink-store";
import { getInjectable } from "@ogre-tools/injectable";
import { weblinkStoreMigrationInjectionToken } from "../../../common/weblinks-store/migration-token";
import { lensDocumentationWeblinkId, lensForumsWeblinkId } from "../links";
import { weblinkStoreMigrationInjectionToken } from "../../../features/weblinks/common/migration-token";
import { lensDocumentationWeblinkId, lensForumsWeblinkId } from "../../../features/weblinks/main/links";
import { applicationInformationToken } from "@k8slens/application";
import type { WeblinkData } from "../common/storage.injectable";
const currentVersionWeblinkStoreMigrationInjectable = getInjectable({
id: "current-version-weblink-store-migration",

View File

@ -2,22 +2,20 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
import syncWeblinksInjectable from "../../catalog-sources/sync-weblinks.injectable";
const setupSyncingOfWeblinksInjectable = getInjectable({
id: "setup-syncing-of-weblinks",
import { getInjectable } from "@ogre-tools/injectable";
import weblinksPersistentStorageInjectable from "../common/storage.injectable";
const loadWeblinkStorageInjectable = getInjectable({
id: "load-weblink-storage",
instantiate: (di) => ({
run: () => {
const syncWeblinks = di.inject(syncWeblinksInjectable);
const storage = di.inject(weblinksPersistentStorageInjectable);
syncWeblinks();
storage.loadAndStartSyncing();
},
}),
injectionToken: onLoadOfApplicationInjectionToken,
});
export default setupSyncingOfWeblinksInjectable;
export default loadWeblinkStorageInjectable;

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
import weblinkVerificationStartableStoppableInjectable from "./weblink-verification.injectable";
import catalogEntityRegistryInjectable from "../../../main/catalog/entity-registry.injectable";
import weblinkVerificationsInjectable from "./weblink-verifications.injectable";
import { computed } from "mobx";
import { iter } from "@k8slens/utilities";
const setupSyncingOfWeblinksInjectable = getInjectable({
id: "setup-syncing-of-weblinks",
instantiate: (di) => ({
run: () => {
const weblinkVerificationStartableStoppable = di.inject(weblinkVerificationStartableStoppableInjectable);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const weblinkVerifications = di.inject(weblinkVerificationsInjectable);
weblinkVerificationStartableStoppable.start();
catalogEntityRegistry.addComputedSource("weblinks", computed(() => (
iter.chain(weblinkVerifications.values())
.map(([weblink]) => weblink)
.toArray()
)));
},
}),
injectionToken: onLoadOfApplicationInjectionToken,
});
export default setupSyncingOfWeblinksInjectable;

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../../main/start-main-application/runnable-tokens/phases";
import weblinkVerificationStartableStoppableInjectable from "./weblink-verification.injectable";
const stopValidatingWeblinksInjectable = getInjectable({
id: "stop-validating-weblinks",
instantiate: (di) => ({
run: () => {
const weblinkVerificationStartableStoppable = di.inject(weblinkVerificationStartableStoppableInjectable);
weblinkVerificationStartableStoppable.stop();
return undefined;
},
}),
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopValidatingWeblinksInjectable;

View File

@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import validateWeblinkInjectable from "./validate-weblink.injectable";
export default getGlobalOverride(validateWeblinkInjectable, () => async () => "available");

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { chainSignal } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import fetchInjectable from "../../../common/fetch/fetch.injectable";
import { withTimeout } from "../../../common/fetch/timeout-controller";
export type ValidateWeblink = (url: string, signal: AbortSignal) => Promise<"available" | "unavailable">;
const validateWeblinkInjectable = getInjectable({
id: "validate-weblink",
instantiate: (di): ValidateWeblink => {
const fetch = di.inject(fetchInjectable);
return async (url, signal) => {
const timeout = withTimeout(20_000);
chainSignal(timeout, signal);
try {
const res = await fetch(url, {
signal: timeout.signal,
});
if (res.status >= 200 && res.status < 500) {
return "available";
}
} catch {
// ignore
} finally {
timeout.abort();
}
return "unavailable";
};
},
causesSideEffects: true,
});
export default validateWeblinkInjectable;

View File

@ -0,0 +1,105 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Disposer } from "@k8slens/utilities";
import { delay, disposer } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import { random } from "lodash";
import { reaction, runInAction } from "mobx";
import { WebLink } from "../../../common/catalog-entities";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
import weblinksInjectable from "../common/weblinks.injectable";
import validateWeblinkInjectable from "./validate-weblink.injectable";
import weblinkVerificationsInjectable from "./weblink-verifications.injectable";
const sixtyMinutes = 60 * 60 * 1000;
const weblinkVerificationStartableStoppableInjectable = getInjectable({
id: "weblink-verification-startable-stoppable",
instantiate: (di) => {
const weblinkVerifications = di.inject(weblinkVerificationsInjectable);
const validateWeblink = di.inject(validateWeblinkInjectable);
const weblinks = di.inject(weblinksInjectable);
const startPeriodicallyCheckingWebLink = (link: WebLink): Disposer => {
const controller = new AbortController();
const dispose = disposer(() => controller.abort());
void (async () => {
for (;;) {
const newStatus = await validateWeblink(link.spec.url, controller.signal);
runInAction(() => {
link.status.phase = newStatus;
});
const nextCheckAfter = sixtyMinutes + random(0, 5 * 60 * 1000, false);
await delay(nextCheckAfter, controller.signal);
if (controller.signal.aborted) {
return;
}
}
})();
return dispose;
};
return getStartableStoppable("weblink-verification", () => disposer(
reaction(
() => weblinks.get(),
(links) => {
const seenWeblinks = new Set<string>();
for (const weblinkData of links) {
seenWeblinks.add(weblinkData.id);
if (!weblinkVerifications.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,
},
});
weblinkVerifications.set(weblinkData.id, [
link,
startPeriodicallyCheckingWebLink(link),
]);
}
}
// Stop checking and remove weblinks that are no longer in the store
for (const [weblinkId, [, disposer]] of weblinkVerifications) {
if (!seenWeblinks.has(weblinkId)) {
disposer();
weblinkVerifications.delete(weblinkId);
}
}
},
{
fireImmediately: true,
},
),
() => {
// Stop the validations
for (const [, [, disposer]] of weblinkVerifications) {
disposer();
}
},
));
},
});
export default weblinkVerificationStartableStoppableInjectable;

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Disposer } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
import type { WebLink } from "../../../common/catalog-entities";
const weblinkVerificationsInjectable = getInjectable({
id: "weblink-verifications",
instantiate: () => observable.map<string, [WebLink, Disposer]>(),
});
export default weblinkVerificationsInjectable;

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { beforeFrameStartsSecondInjectionToken } from "../../../renderer/before-frame-starts/tokens";
import weblinksPersistentStorageInjectable from "../common/storage.injectable";
const loadWeblinkStorageInjectable = getInjectable({
id: "load-weblink-storage",
instantiate: (di) => ({
run: () => {
const storage = di.inject(weblinksPersistentStorageInjectable);
storage.loadAndStartSyncing();
},
}),
injectionToken: beforeFrameStartsSecondInjectionToken,
});
export default loadWeblinkStorageInjectable;

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export { syncWeblinks } from "./weblinks";

View File

@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { syncWeblinks } from "./weblinks";
import weblinkStoreInjectable from "../../common/weblinks-store/weblink-store.injectable";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
const syncWeblinksInjectable = getInjectable({
id: "sync-weblinks",
instantiate: (di) => syncWeblinks({
weblinkStore: di.inject(weblinkStoreInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
}),
});
export default syncWeblinksInjectable;

View File

@ -1,93 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { computed, observable, reaction } from "mobx";
import type { WeblinkStore } from "../../common/weblinks-store/weblink-store";
import { WebLink } from "../../common/catalog-entities";
import type { CatalogEntityRegistry } from "../catalog";
import got from "got";
import type { Disposer } from "@k8slens/utilities";
import { random } from "lodash";
async function validateLink(link: WebLink) {
try {
const response = await got.get(link.spec.url, {
throwHttpErrors: false,
timeout: 20_000,
});
if (response.statusCode >= 200 && response.statusCode < 500) {
link.status.phase = "available";
} else {
link.status.phase = "unavailable";
}
} catch {
link.status.phase = "unavailable";
}
}
interface Dependencies {
weblinkStore: WeblinkStore;
catalogEntityRegistry: CatalogEntityRegistry;
}
export const syncWeblinks = ({ weblinkStore, catalogEntityRegistry }: Dependencies) => () => {
const webLinkEntities = observable.map<string, [WebLink, Disposer]>();
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) => {
const seenWeblinks = new Set<string>();
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.addComputedSource("weblinks", computed(() => Array.from(webLinkEntities.values(), ([link]) => link)));
};

View File

@ -10,7 +10,7 @@ import spawnInjectable from "./child-process/spawn.injectable";
import initializeExtensionsInjectable from "./start-main-application/runnables/initialize-extensions.injectable";
import setupIpcMainHandlersInjectable from "./electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable";
import setupLensProxyInjectable from "./start-main-application/runnables/setup-lens-proxy.injectable";
import setupSyncingOfWeblinksInjectable from "./start-main-application/runnables/setup-syncing-of-weblinks.injectable";
import setupSyncingOfWeblinksInjectable from "../features/weblinks/main/setup-syncing-of-weblinks.injectable";
import setupDeepLinkingInjectable from "./electron-app/runnables/setup-deep-linking.injectable";
import setupMainWindowVisibilityAfterActivationInjectable from "./electron-app/runnables/setup-main-window-visibility-after-activation.injectable";
import setupDeviceShutdownInjectable from "./electron-app/runnables/setup-device-shutdown.injectable";

View File

@ -7,15 +7,15 @@ import React from "react";
import { observer } from "mobx-react";
import { Input } from "../input";
import { isUrl } from "../input/input_validators";
import type { WeblinkStore } from "../../../common/weblinks-store/weblink-store";
import { computed, makeObservable, observable } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import weblinkStoreInjectable from "../../../common/weblinks-store/weblink-store.injectable";
import type { AddWeblink } from "../../../features/weblinks/common/add.injectable";
import addWeblinkInjectable from "../../../features/weblinks/common/add.injectable";
interface Dependencies {
closeCommandOverlay: () => void;
weblinkStore: WeblinkStore;
addWeblink: AddWeblink;
}
@ -42,7 +42,7 @@ class NonInjectedWeblinkAddCommand extends React.Component<Dependencies> {
}
onSubmit(name: string) {
this.props.weblinkStore.add({
this.props.addWeblink({
name: name || this.url,
url: this.url,
});
@ -97,6 +97,6 @@ export const WeblinkAddCommand = withInjectables<Dependencies>(NonInjectedWeblin
getProps: (di, props) => ({
...props,
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
weblinkStore: di.inject(weblinkStoreInjectable),
addWeblink: di.inject(addWeblinkInjectable),
}),
});

View File

@ -22,3 +22,11 @@ export function setTimeoutFor(controller: AbortController, timeout: number): voi
controller.signal.addEventListener("abort", () => clearTimeout(handle));
}
export function chainSignal(target: AbortController, signal: AbortSignal) {
if (signal.aborted) {
target.abort();
} else {
signal.addEventListener("abort", (event) => target.abort(event));
}
}

View File

@ -10,11 +10,11 @@
* @param timeout The number of milliseconds before resolving
* @param failFast An abort controller instance to cause the delay to short-circuit
*/
export function delay(timeout = 1000, failFast?: AbortController): Promise<void> {
export function delay(timeout = 1000, failFast?: AbortSignal): Promise<void> {
return new Promise(resolve => {
const timeoutId = setTimeout(resolve, timeout);
failFast?.signal.addEventListener("abort", () => {
failFast?.addEventListener("abort", () => {
clearTimeout(timeoutId);
resolve();
});