diff --git a/package-lock.json b/package-lock.json index d00efc567c..d635ec9d7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/core/package.json b/packages/core/package.json index 42c39d6181..2af545184c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/common/catalog-entities/web-link.ts b/packages/core/src/common/catalog-entities/web-link.ts index 833f05d65b..4b18ac7f73 100644 --- a/packages/core/src/common/catalog-entities/web-link.ts +++ b/packages/core/src/common/catalog-entities/web-link.ts @@ -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 weblinkStore.removeById(this.getId()), + onClick: () => removeWeblink(this.getId()), confirm: { message: `Remove Web Link "${this.getName()}" from ${productName}?`, }, diff --git a/packages/core/src/common/weblinks-store/weblink-store.injectable.ts b/packages/core/src/common/weblinks-store/weblink-store.injectable.ts deleted file mode 100644 index c31531d655..0000000000 --- a/packages/core/src/common/weblinks-store/weblink-store.injectable.ts +++ /dev/null @@ -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; diff --git a/packages/core/src/common/weblinks-store/weblink-store.ts b/packages/core/src/common/weblinks-store/weblink-store.ts deleted file mode 100644 index 0bc0095b1b..0000000000 --- a/packages/core/src/common/weblinks-store/weblink-store.ts +++ /dev/null @@ -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>; - createPersistentStorage: CreatePersistentStorage; -} - -export class WeblinkStore { - private readonly store: PersistentStorage; - - readonly weblinks = observable.array(); - - constructor(private readonly dependencies: Dependencies) { - this.store = this.dependencies.createPersistentStorage({ - 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)); - }); - } -} diff --git a/packages/core/src/features/weblinks/common/add.injectable.ts b/packages/core/src/features/weblinks/common/add.injectable.ts new file mode 100644 index 0000000000..75450d237d --- /dev/null +++ b/packages/core/src/features/weblinks/common/add.injectable.ts @@ -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; diff --git a/packages/core/src/common/weblinks-store/migration-token.ts b/packages/core/src/features/weblinks/common/migration-token.ts similarity index 77% rename from packages/core/src/common/weblinks-store/migration-token.ts rename to packages/core/src/features/weblinks/common/migration-token.ts index 6f63d697d6..a0d8ce58fc 100644 --- a/packages/core/src/common/weblinks-store/migration-token.ts +++ b/packages/core/src/features/weblinks/common/migration-token.ts @@ -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({ id: "weblink-store-migration-token", diff --git a/packages/core/src/features/weblinks/common/remove.injectable.ts b/packages/core/src/features/weblinks/common/remove.injectable.ts new file mode 100644 index 0000000000..0928903561 --- /dev/null +++ b/packages/core/src/features/weblinks/common/remove.injectable.ts @@ -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; diff --git a/packages/core/src/features/weblinks/common/state.injectable.ts b/packages/core/src/features/weblinks/common/state.injectable.ts new file mode 100644 index 0000000000..ab909050aa --- /dev/null +++ b/packages/core/src/features/weblinks/common/state.injectable.ts @@ -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(), +}); + +export default weblinksStateInjectable; diff --git a/packages/core/src/features/weblinks/common/storage.injectable.ts b/packages/core/src/features/weblinks/common/storage.injectable.ts new file mode 100644 index 0000000000..6b6c668b83 --- /dev/null +++ b/packages/core/src/features/weblinks/common/storage.injectable.ts @@ -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({ + 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; diff --git a/packages/core/src/features/weblinks/common/weblinks.injectable.ts b/packages/core/src/features/weblinks/common/weblinks.injectable.ts new file mode 100644 index 0000000000..a4dca05caf --- /dev/null +++ b/packages/core/src/features/weblinks/common/weblinks.injectable.ts @@ -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; diff --git a/packages/core/src/main/weblinks-store/migrations/5.1.4.injectable.ts b/packages/core/src/features/weblinks/main/5.1.4.injectable.ts similarity index 86% rename from packages/core/src/main/weblinks-store/migrations/5.1.4.injectable.ts rename to packages/core/src/features/weblinks/main/5.1.4.injectable.ts index 000937abba..db8eb1510b 100644 --- a/packages/core/src/main/weblinks-store/migrations/5.1.4.injectable.ts +++ b/packages/core/src/features/weblinks/main/5.1.4.injectable.ts @@ -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", diff --git a/packages/core/src/main/weblinks-store/migrations/5.4.5-beta.1.injectable.ts b/packages/core/src/features/weblinks/main/5.4.5-beta.1.injectable.ts similarity index 89% rename from packages/core/src/main/weblinks-store/migrations/5.4.5-beta.1.injectable.ts rename to packages/core/src/features/weblinks/main/5.4.5-beta.1.injectable.ts index fc67e4a2f5..01f252deb1 100644 --- a/packages/core/src/main/weblinks-store/migrations/5.4.5-beta.1.injectable.ts +++ b/packages/core/src/features/weblinks/main/5.4.5-beta.1.injectable.ts @@ -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", diff --git a/packages/core/src/main/weblinks-store/migrations/currentVersion.injectable.ts b/packages/core/src/features/weblinks/main/currentVersion.injectable.ts similarity index 87% rename from packages/core/src/main/weblinks-store/migrations/currentVersion.injectable.ts rename to packages/core/src/features/weblinks/main/currentVersion.injectable.ts index 4b873d78c4..dfb2946164 100644 --- a/packages/core/src/main/weblinks-store/migrations/currentVersion.injectable.ts +++ b/packages/core/src/features/weblinks/main/currentVersion.injectable.ts @@ -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", diff --git a/packages/core/src/main/weblinks-store/links.ts b/packages/core/src/features/weblinks/main/links.ts similarity index 100% rename from packages/core/src/main/weblinks-store/links.ts rename to packages/core/src/features/weblinks/main/links.ts diff --git a/packages/core/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts b/packages/core/src/features/weblinks/main/load-storage.injectable.ts similarity index 55% rename from packages/core/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts rename to packages/core/src/features/weblinks/main/load-storage.injectable.ts index 09a630db37..e309381bbd 100644 --- a/packages/core/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts +++ b/packages/core/src/features/weblinks/main/load-storage.injectable.ts @@ -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; diff --git a/packages/core/src/features/weblinks/main/setup-syncing-of-weblinks.injectable.ts b/packages/core/src/features/weblinks/main/setup-syncing-of-weblinks.injectable.ts new file mode 100644 index 0000000000..1f6b440904 --- /dev/null +++ b/packages/core/src/features/weblinks/main/setup-syncing-of-weblinks.injectable.ts @@ -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; diff --git a/packages/core/src/features/weblinks/main/stop-validating-weblinks.injectable.ts b/packages/core/src/features/weblinks/main/stop-validating-weblinks.injectable.ts new file mode 100644 index 0000000000..221a3ee0dd --- /dev/null +++ b/packages/core/src/features/weblinks/main/stop-validating-weblinks.injectable.ts @@ -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; diff --git a/packages/core/src/features/weblinks/main/validate-weblink.global-override-for-injectable.ts b/packages/core/src/features/weblinks/main/validate-weblink.global-override-for-injectable.ts new file mode 100644 index 0000000000..8ec03bebaf --- /dev/null +++ b/packages/core/src/features/weblinks/main/validate-weblink.global-override-for-injectable.ts @@ -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"); diff --git a/packages/core/src/features/weblinks/main/validate-weblink.injectable.ts b/packages/core/src/features/weblinks/main/validate-weblink.injectable.ts new file mode 100644 index 0000000000..c38b483a95 --- /dev/null +++ b/packages/core/src/features/weblinks/main/validate-weblink.injectable.ts @@ -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; diff --git a/packages/core/src/features/weblinks/main/weblink-verification.injectable.ts b/packages/core/src/features/weblinks/main/weblink-verification.injectable.ts new file mode 100644 index 0000000000..ca13270b11 --- /dev/null +++ b/packages/core/src/features/weblinks/main/weblink-verification.injectable.ts @@ -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(); + + 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; diff --git a/packages/core/src/features/weblinks/main/weblink-verifications.injectable.ts b/packages/core/src/features/weblinks/main/weblink-verifications.injectable.ts new file mode 100644 index 0000000000..6e3f7d8f0f --- /dev/null +++ b/packages/core/src/features/weblinks/main/weblink-verifications.injectable.ts @@ -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(), +}); + +export default weblinkVerificationsInjectable; diff --git a/packages/core/src/features/weblinks/renderer/load-storage.injectable.ts b/packages/core/src/features/weblinks/renderer/load-storage.injectable.ts new file mode 100644 index 0000000000..3cfacc3fa3 --- /dev/null +++ b/packages/core/src/features/weblinks/renderer/load-storage.injectable.ts @@ -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; diff --git a/packages/core/src/main/catalog-sources/index.ts b/packages/core/src/main/catalog-sources/index.ts deleted file mode 100644 index 98c3f08536..0000000000 --- a/packages/core/src/main/catalog-sources/index.ts +++ /dev/null @@ -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"; diff --git a/packages/core/src/main/catalog-sources/sync-weblinks.injectable.ts b/packages/core/src/main/catalog-sources/sync-weblinks.injectable.ts deleted file mode 100644 index f75de34a9d..0000000000 --- a/packages/core/src/main/catalog-sources/sync-weblinks.injectable.ts +++ /dev/null @@ -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; diff --git a/packages/core/src/main/catalog-sources/weblinks.ts b/packages/core/src/main/catalog-sources/weblinks.ts deleted file mode 100644 index 2ff2b645a1..0000000000 --- a/packages/core/src/main/catalog-sources/weblinks.ts +++ /dev/null @@ -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(); - - 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(); - - 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))); -}; diff --git a/packages/core/src/main/getDiForUnitTesting.ts b/packages/core/src/main/getDiForUnitTesting.ts index b374eec1ca..83b3aa6d82 100644 --- a/packages/core/src/main/getDiForUnitTesting.ts +++ b/packages/core/src/main/getDiForUnitTesting.ts @@ -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"; diff --git a/packages/core/src/renderer/components/catalog-entities/weblink-add-command.tsx b/packages/core/src/renderer/components/catalog-entities/weblink-add-command.tsx index 0209ff93fa..221af86f38 100644 --- a/packages/core/src/renderer/components/catalog-entities/weblink-add-command.tsx +++ b/packages/core/src/renderer/components/catalog-entities/weblink-add-command.tsx @@ -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 { } 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(NonInjectedWeblin getProps: (di, props) => ({ ...props, closeCommandOverlay: di.inject(commandOverlayInjectable).close, - weblinkStore: di.inject(weblinkStoreInjectable), + addWeblink: di.inject(addWeblinkInjectable), }), }); diff --git a/packages/utility-features/utilities/src/abort-controller.ts b/packages/utility-features/utilities/src/abort-controller.ts index 784b495a9e..d434a24c6f 100644 --- a/packages/utility-features/utilities/src/abort-controller.ts +++ b/packages/utility-features/utilities/src/abort-controller.ts @@ -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)); + } +} diff --git a/packages/utility-features/utilities/src/delay.ts b/packages/utility-features/utilities/src/delay.ts index d86395026b..83be44be71 100644 --- a/packages/utility-features/utilities/src/delay.ts +++ b/packages/utility-features/utilities/src/delay.ts @@ -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 { +export function delay(timeout = 1000, failFast?: AbortSignal): Promise { return new Promise(resolve => { const timeoutId = setTimeout(resolve, timeout); - failFast?.signal.addEventListener("abort", () => { + failFast?.addEventListener("abort", () => { clearTimeout(timeoutId); resolve(); });