Remove BaseStore<T> and replace with composition of dependencies (#7002)

* Rename ExtensionStore -> BaseExtensionStore
- The name was too close to ExtensionsStore

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move ExtensionsStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move ClusterStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move UserStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup types to remove multiple cast locations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move HotbarStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move WeblinkStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move FileSystemProvisionerStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Clean up impl and rename to better describe intent

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix remaining type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the enabled extensions storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the clusters storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the hotbar storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the weblinks storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the user preferences storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix crashing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests and snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix integration test failures

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve typing to prevent errors in the future.

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup @k8slens/messaging and friends

- To fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix lint issue

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix global override not being complete enough causing tests to fail

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Bump memory for unit tests on CI

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Attempt to fix memory issue on CI again

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fixup test because of new injectables

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Upgrade Jest

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix unit tests falling over

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Back out jest config change

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove console log

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshot

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests by matching equality instead of snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests by forcing specific snapshot style

- Ubuntu CI seems to format arrays in snapshots differently than macOS locally

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2023-03-28 17:54:25 +03:00
1 parent ba4a283af9
commit 49f0a1af9c
365 files changed
+6733 -3972

No files matched your search

@@ -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;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MigrationDeclaration } from "../../../common/persistent-storage/migrations.injectable";
export const weblinkStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "weblink-store-migration-token",
});
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -0,0 +1,35 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { docsUrl, forumsUrl } from "../../../common/vars";
import { getInjectable } from "@ogre-tools/injectable";
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",
instantiate: () => ({
version: "5.1.4",
run(store) {
const weblinksRaw = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
weblinks.push(
{ id: "https://k8slens.dev", name: links.lensWebsiteLinkName, url: "https://k8slens.dev" },
{ id: docsUrl, name: links.lensDocumentationWeblinkName, url: docsUrl },
{ id: forumsUrl, name: links.lensForumsWeblinkName, url: forumsUrl },
{ id: "https://twitter.com/k8slens", name: links.lensTwitterWeblinkName, url: "https://twitter.com/k8slens" },
{ id: "https://medium.com/k8slens", name: links.lensBlogWeblinkName, url: "https://medium.com/k8slens" },
{ id: "https://kubernetes.io/docs/home/", name: links.kubernetesDocumentationWeblinkName, url: "https://kubernetes.io/docs/home/" },
);
store.set("weblinks", weblinks);
},
}),
injectionToken: weblinkStoreMigrationInjectionToken,
});
export default v514WeblinkStoreMigrationInjectable;
@@ -0,0 +1,61 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as links from "../../../features/weblinks/main/links";
import { getInjectable } from "@ogre-tools/injectable";
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",
instantiate: () => ({
version: "5.4.5-beta.1 || >=5.5.0-alpha.0",
run(store) {
const weblinksRaw = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
const lensWebsite = weblinks.find(weblink => weblink.name === links.lensWebsiteLinkName);
if (lensWebsite) {
lensWebsite.id = links.lensWebsiteWeblinkId;
}
const lensDocumentationWeblink = weblinks.find(weblink => weblink.name === links.lensDocumentationWeblinkName);
if (lensDocumentationWeblink) {
lensDocumentationWeblink.id = links.lensDocumentationWeblinkId;
}
const lensForumsWeblink = weblinks.find(weblink => weblink.name === links.lensForumsWeblinkName);
if (lensForumsWeblink) {
lensForumsWeblink.id = links.lensForumsWeblinkId;
}
const lensTwitterWeblink = weblinks.find(weblink => weblink.name === links.lensTwitterWeblinkName);
if (lensTwitterWeblink) {
lensTwitterWeblink.id = links.lensTwitterWeblinkId;
}
const lensBlogWeblink = weblinks.find(weblink => weblink.name === links.lensBlogWeblinkName);
if (lensBlogWeblink) {
lensBlogWeblink.id = links.lensBlogWeblinkId;
}
const kubernetesDocumentationWeblink = weblinks.find(weblink => weblink.name === links.kubernetesDocumentationWeblinkName);
if (kubernetesDocumentationWeblink) {
kubernetesDocumentationWeblink.id = links.kubernetesDocumentationWeblinkId;
}
store.set("weblinks", weblinks);
},
}),
injectionToken: weblinkStoreMigrationInjectionToken,
});
export default v545Beta1WeblinkStoreMigrationInjectable;
@@ -0,0 +1,44 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { docsUrl, forumsUrl } from "../../../common/vars";
import { getInjectable } from "@ogre-tools/injectable";
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",
instantiate: (di) => {
const { version } = di.inject(applicationInformationToken);
return {
version, // Run always after upgrade
run(store) {
const weblinksRaw = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
const forumsWeblink = weblinks.find(weblink => weblink.id === lensForumsWeblinkId);
if (forumsWeblink) {
forumsWeblink.url = forumsUrl;
}
const docsWeblink = weblinks.find(weblink => weblink.id === lensDocumentationWeblinkId);
if (docsWeblink) {
docsWeblink.url = docsUrl;
}
const removedSlackLink = weblinks.filter(weblink => weblink.id !== "lens-slack-link");
store.set("weblinks", removedSlackLink);
},
};
},
injectionToken: weblinkStoreMigrationInjectionToken,
});
export default currentVersionWeblinkStoreMigrationInjectable;
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const lensWebsiteWeblinkId = "lens-website-link";
export const lensWebsiteLinkName = "Lens Website";
export const lensDocumentationWeblinkId = "lens-documentation-link";
export const lensDocumentationWeblinkName = "Lens Documentation";
export const lensForumsWeblinkId = "lens-forums-link";
export const lensForumsWeblinkName = "Lens Forums";
export const lensTwitterWeblinkId = "lens-twitter-link";
export const lensTwitterWeblinkName = "Lens on Twitter";
export const lensBlogWeblinkId = "lens-blog-link";
export const lensBlogWeblinkName = "Lens Official Blog";
export const kubernetesDocumentationWeblinkId = "kubernetes-documentation-link";
export const kubernetesDocumentationWeblinkName = "Kubernetes Documentation";
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
import { getInjectable } from "@ogre-tools/injectable";
import weblinksPersistentStorageInjectable from "../common/storage.injectable";
const loadWeblinkStorageInjectable = getInjectable({
id: "load-weblink-storage",
instantiate: (di) => ({
run: () => {
const storage = di.inject(weblinksPersistentStorageInjectable);
storage.loadAndStartSyncing();
},
}),
injectionToken: onLoadOfApplicationInjectionToken,
});
export default loadWeblinkStorageInjectable;
@@ -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;
@@ -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;
@@ -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");
@@ -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;
@@ -0,0 +1,105 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getStartableStoppable } from "@k8slens/startable-stoppable";
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 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;
@@ -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;
@@ -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;