mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fully split apart the enabled extensions storage
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
31a3369dd0
commit
12e438892d
@ -22,6 +22,10 @@ import { shouldPersistentStorageDisableSyncInIpcListenerInjectionToken } from ".
|
|||||||
import { persistStateToConfigInjectionToken } from "./save-to-file";
|
import { persistStateToConfigInjectionToken } from "./save-to-file";
|
||||||
|
|
||||||
export interface PersistentStorage {
|
export interface PersistentStorage {
|
||||||
|
/**
|
||||||
|
* This method does the initial synchronous load from disk and then starts writing the state
|
||||||
|
* back to disk whenever it changes.
|
||||||
|
*/
|
||||||
loadAndStartSyncing: () => void;
|
loadAndStartSyncing: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { InjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import * as semver from "semver";
|
||||||
|
import type { MigrationDeclaration } from "./migrations.injectable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: not all stores can use this computed version, namely if any migration uses a range for
|
||||||
|
* the version selector.
|
||||||
|
*/
|
||||||
|
const storageMigrationVersionInjectable = getInjectable({
|
||||||
|
id: "storage-migration-version",
|
||||||
|
instantiate: (di, token) => {
|
||||||
|
const declarations = di.injectMany(token);
|
||||||
|
|
||||||
|
return declarations.reduce((version, decl) => {
|
||||||
|
if (semver.gte(decl.version, version)) {
|
||||||
|
return decl.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}, "1.0.0");
|
||||||
|
},
|
||||||
|
lifecycle: lifecycleEnum.keyedSingleton({
|
||||||
|
getInstanceKey: (di, token: InjectionToken<MigrationDeclaration, void>) => token.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default storageMigrationVersionInjectable;
|
||||||
@ -16,7 +16,7 @@ import type { RouteHandler, RouteParams } from "./registration";
|
|||||||
import { when } from "mobx";
|
import { when } from "mobx";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import type { Logger } from "../logger";
|
import type { Logger } from "../logger";
|
||||||
import type { EnabledExtensionsState } from "../../extensions/enabled-extensions-state.injectable";
|
import type { IsExtensionEnabled } from "../../features/extensions/enabled/common/is-enabled.injectable";
|
||||||
|
|
||||||
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
||||||
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
||||||
@ -65,8 +65,8 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
|
|||||||
|
|
||||||
export interface LensProtocolRouterDependencies {
|
export interface LensProtocolRouterDependencies {
|
||||||
readonly extensionLoader: ExtensionLoader;
|
readonly extensionLoader: ExtensionLoader;
|
||||||
readonly enabledExtensionsState: EnabledExtensionsState;
|
|
||||||
readonly logger: Logger;
|
readonly logger: Logger;
|
||||||
|
isExtensionEnabled: IsExtensionEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class LensProtocolRouter {
|
export abstract class LensProtocolRouter {
|
||||||
@ -209,7 +209,7 @@ export abstract class LensProtocolRouter {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.dependencies.enabledExtensionsState.isEnabled(extension)) {
|
if (!this.dependencies.isExtensionEnabled(extension)) {
|
||||||
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
@ -6,13 +6,13 @@
|
|||||||
import type { ExtensionLoader } from "../extension-loader";
|
import type { ExtensionLoader } from "../extension-loader";
|
||||||
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
|
|
||||||
import { delay } from "@k8slens/utilities";
|
import { delay } from "@k8slens/utilities";
|
||||||
import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
|
||||||
import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable";
|
import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable";
|
||||||
import type { IpcRenderer } from "electron";
|
import type { IpcRenderer } from "electron";
|
||||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
import currentlyInClusterFrameInjectable from "../../renderer/routes/currently-in-cluster-frame.injectable";
|
import currentlyInClusterFrameInjectable from "../../renderer/routes/currently-in-cluster-frame.injectable";
|
||||||
|
import updateExtensionsStateInjectable from "../../features/extensions/enabled/common/update-state.injectable";
|
||||||
|
|
||||||
const manifestPath = "manifest/path";
|
const manifestPath = "manifest/path";
|
||||||
const manifestPath2 = "manifest/path2";
|
const manifestPath2 = "manifest/path2";
|
||||||
|
|||||||
@ -13,10 +13,9 @@ import { issuesTrackerUrl } from "../../common/vars";
|
|||||||
import { buildVersionInjectionToken } from "../../common/vars/build-semantic-version.injectable";
|
import { buildVersionInjectionToken } from "../../common/vars/build-semantic-version.injectable";
|
||||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||||
import userStoreInjectable from "../../common/user-store/user-store.injectable";
|
import userStoreInjectable from "../../common/user-store/user-store.injectable";
|
||||||
import enabledExtensionsInjectable from "./get-enabled-extensions/get-enabled-extensions.injectable";
|
import enabledExtensionsInjectable from "../../features/extensions/enabled/common/enabled-extensions.injectable";
|
||||||
|
|
||||||
const userStore = asLegacyGlobalForExtensionApi(userStoreInjectable);
|
const userStore = asLegacyGlobalForExtensionApi(userStoreInjectable);
|
||||||
|
|
||||||
const enabledExtensions = asLegacyGlobalForExtensionApi(enabledExtensionsInjectable);
|
const enabledExtensions = asLegacyGlobalForExtensionApi(enabledExtensionsInjectable);
|
||||||
|
|
||||||
export const App = {
|
export const App = {
|
||||||
|
|||||||
@ -1,13 +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 enabledExtensionsStateInjectable from "../../enabled-extensions-state.injectable";
|
|
||||||
|
|
||||||
const enabledExtensionsInjectable = getInjectable({
|
|
||||||
id: "enabled-extensions",
|
|
||||||
instantiate: (di) => di.inject(enabledExtensionsStateInjectable).enabledExtensions,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default enabledExtensionsInjectable;
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
|
||||||
import { iter, object } from "@k8slens/utilities";
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import type { IComputedValue } from "mobx";
|
|
||||||
import { action, computed, observable } from "mobx";
|
|
||||||
import createPersistentStorageInjectable from "../common/persistent-storage/create.injectable";
|
|
||||||
import storeMigrationVersionInjectable from "../common/vars/store-migration-version.injectable";
|
|
||||||
|
|
||||||
export interface LensExtensionState {
|
|
||||||
enabled?: boolean;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IsEnabledExtensionDescriptor {
|
|
||||||
id: string;
|
|
||||||
isBundled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnabledExtensionsState {
|
|
||||||
readonly enabledExtensions: IComputedValue<string[]>;
|
|
||||||
isEnabled: (desc: IsEnabledExtensionDescriptor) => boolean;
|
|
||||||
mergeState: (newPartialState: Partial<Record<LensExtensionId, LensExtensionState>> | [LensExtensionId, LensExtensionState][]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enabledExtensionsStateInjectable = getInjectable({
|
|
||||||
id: "enabled-extensions-state",
|
|
||||||
instantiate: (di): EnabledExtensionsState => {
|
|
||||||
const storeMigrationVersion = di.inject(storeMigrationVersionInjectable);
|
|
||||||
const createPersistentStorage = di.inject(createPersistentStorageInjectable);
|
|
||||||
|
|
||||||
const state = observable.map<LensExtensionId, LensExtensionState>();
|
|
||||||
const storage = createPersistentStorage({
|
|
||||||
configName: "lens-extensions",
|
|
||||||
fromStore: action(({ extensions = {}}) => {
|
|
||||||
state.merge(extensions);
|
|
||||||
}),
|
|
||||||
toJSON: () => ({
|
|
||||||
extensions: Object.fromEntries(state),
|
|
||||||
}),
|
|
||||||
projectVersion: storeMigrationVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
// NOTE: this is done implicitly here currently
|
|
||||||
storage.loadAndStartSyncing();
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabledExtensions: computed(() => (
|
|
||||||
iter.chain(state.values())
|
|
||||||
.filter(({ enabled }) => enabled)
|
|
||||||
.map(({ name }) => name)
|
|
||||||
.toArray()
|
|
||||||
)),
|
|
||||||
isEnabled: ({ id, isBundled }) => isBundled || (state.get(id)?.enabled ?? false),
|
|
||||||
mergeState: action((newPartialState) => {
|
|
||||||
if (Array.isArray(newPartialState)) {
|
|
||||||
state.merge(newPartialState);
|
|
||||||
} else {
|
|
||||||
state.merge(object.entries(newPartialState));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default enabledExtensionsStateInjectable;
|
|
||||||
@ -6,7 +6,6 @@ import { getInjectable } from "@ogre-tools/injectable";
|
|||||||
import { ExtensionDiscovery } from "./extension-discovery";
|
import { ExtensionDiscovery } from "./extension-discovery";
|
||||||
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||||
import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable";
|
import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable";
|
||||||
import enabledExtensionsStateInjectable from "../enabled-extensions-state.injectable";
|
|
||||||
import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable";
|
import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
import installExtensionInjectable from "../install-extension/install-extension.injectable";
|
import installExtensionInjectable from "../install-extension/install-extension.injectable";
|
||||||
import extensionPackageRootDirectoryInjectable from "../install-extension/extension-package-root-directory.injectable";
|
import extensionPackageRootDirectoryInjectable from "../install-extension/extension-package-root-directory.injectable";
|
||||||
@ -28,13 +27,14 @@ import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
|||||||
import removePathInjectable from "../../common/fs/remove.injectable";
|
import removePathInjectable from "../../common/fs/remove.injectable";
|
||||||
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
|
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
|
||||||
import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable";
|
import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable";
|
||||||
|
import isExtensionEnabledInjectable from "../../features/extensions/enabled/common/is-enabled.injectable";
|
||||||
|
|
||||||
const extensionDiscoveryInjectable = getInjectable({
|
const extensionDiscoveryInjectable = getInjectable({
|
||||||
id: "extension-discovery",
|
id: "extension-discovery",
|
||||||
|
|
||||||
instantiate: (di) => new ExtensionDiscovery({
|
instantiate: (di) => new ExtensionDiscovery({
|
||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
enabledExtensionsState: di.inject(enabledExtensionsStateInjectable),
|
isExtensionEnabled: di.inject(isExtensionEnabledInjectable),
|
||||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
isCompatibleExtension: di.inject(isCompatibleExtensionInjectable),
|
isCompatibleExtension: di.inject(isCompatibleExtensionInjectable),
|
||||||
installExtension: di.inject(installExtensionInjectable),
|
installExtension: di.inject(installExtensionInjectable),
|
||||||
|
|||||||
@ -30,11 +30,10 @@ import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"
|
|||||||
import type { GetRelativePath } from "../../common/path/get-relative-path.injectable";
|
import type { GetRelativePath } from "../../common/path/get-relative-path.injectable";
|
||||||
import type { RemovePath } from "../../common/fs/remove.injectable";
|
import type { RemovePath } from "../../common/fs/remove.injectable";
|
||||||
import type TypedEventEmitter from "typed-emitter";
|
import type TypedEventEmitter from "typed-emitter";
|
||||||
import type { EnabledExtensionsState } from "../enabled-extensions-state.injectable";
|
import type { IsExtensionEnabled } from "../../features/extensions/enabled/common/is-enabled.injectable";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
readonly extensionLoader: ExtensionLoader;
|
readonly extensionLoader: ExtensionLoader;
|
||||||
readonly enabledExtensionsState: EnabledExtensionsState;
|
|
||||||
readonly extensionInstallationStateStore: ExtensionInstallationStateStore;
|
readonly extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||||
readonly extensionPackageRootDirectory: string;
|
readonly extensionPackageRootDirectory: string;
|
||||||
readonly resourcesDirectory: string;
|
readonly resourcesDirectory: string;
|
||||||
@ -42,6 +41,7 @@ interface Dependencies {
|
|||||||
readonly isProduction: boolean;
|
readonly isProduction: boolean;
|
||||||
readonly fileSystemSeparator: string;
|
readonly fileSystemSeparator: string;
|
||||||
readonly homeDirectoryPath: string;
|
readonly homeDirectoryPath: string;
|
||||||
|
isExtensionEnabled: IsExtensionEnabled;
|
||||||
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
|
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
|
||||||
installExtension: (name: string) => Promise<void>;
|
installExtension: (name: string) => Promise<void>;
|
||||||
readJsonFile: ReadJson;
|
readJsonFile: ReadJson;
|
||||||
@ -334,7 +334,7 @@ export class ExtensionDiscovery {
|
|||||||
try {
|
try {
|
||||||
const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest;
|
const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest;
|
||||||
const id = isBundled ? manifestPath : this.getInstalledManifestPath(manifest.name);
|
const id = isBundled ? manifestPath : this.getInstalledManifestPath(manifest.name);
|
||||||
const isEnabled = this.dependencies.enabledExtensionsState.isEnabled({ id, isBundled });
|
const isEnabled = this.dependencies.isExtensionEnabled({ id, isBundled });
|
||||||
const extensionDir = this.dependencies.getDirnameOfPath(manifestPath);
|
const extensionDir = this.dependencies.getDirnameOfPath(manifestPath);
|
||||||
const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||||
const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage)
|
const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage)
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { ExtensionLoader } from "./extension-loader";
|
import { ExtensionLoader } from "./extension-loader";
|
||||||
import updateExtensionsStateInjectable from "./update-extensions-state/update-extensions-state.injectable";
|
|
||||||
import { createExtensionInstanceInjectionToken } from "./create-extension-instance.token";
|
import { createExtensionInstanceInjectionToken } from "./create-extension-instance.token";
|
||||||
import extensionInstancesInjectable from "./extension-instances.injectable";
|
import extensionInstancesInjectable from "./extension-instances.injectable";
|
||||||
import type { LensExtension } from "../lens-extension";
|
import type { LensExtension } from "../lens-extension";
|
||||||
@ -14,6 +13,7 @@ import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
|||||||
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
|
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
|
||||||
import { bundledExtensionInjectionToken } from "@k8slens/legacy-extensions";
|
import { bundledExtensionInjectionToken } from "@k8slens/legacy-extensions";
|
||||||
import { extensionEntryPointNameInjectionToken } from "./entry-point-name";
|
import { extensionEntryPointNameInjectionToken } from "./entry-point-name";
|
||||||
|
import updateExtensionsStateInjectable from "../../features/extensions/enabled/common/update-state.injectable";
|
||||||
|
|
||||||
const extensionLoaderInjectable = getInjectable({
|
const extensionLoaderInjectable = getInjectable({
|
||||||
id: "extension-loader",
|
id: "extension-loader",
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import type { Logger } from "../../common/logger";
|
|||||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||||
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
||||||
import type { LensExtensionId, BundledExtension, InstalledExtension, LensExtensionConstructor } from "@k8slens/legacy-extensions";
|
import type { LensExtensionId, BundledExtension, InstalledExtension, LensExtensionConstructor } from "@k8slens/legacy-extensions";
|
||||||
import type { LensExtensionState } from "../enabled-extensions-state.injectable";
|
import type { UpdateExtensionsState } from "../../features/extensions/enabled/common/update-state.injectable";
|
||||||
|
|
||||||
const logModule = "[EXTENSIONS-LOADER]";
|
const logModule = "[EXTENSIONS-LOADER]";
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ interface Dependencies {
|
|||||||
readonly bundledExtensions: BundledExtension[];
|
readonly bundledExtensions: BundledExtension[];
|
||||||
readonly logger: Logger;
|
readonly logger: Logger;
|
||||||
readonly extensionEntryPointName: "main" | "renderer";
|
readonly extensionEntryPointName: "main" | "renderer";
|
||||||
updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void;
|
updateExtensionsState: UpdateExtensionsState;
|
||||||
createExtensionInstance: CreateExtensionInstance;
|
createExtensionInstance: CreateExtensionInstance;
|
||||||
getExtension: (instance: LensExtension) => Extension;
|
getExtension: (instance: LensExtension) => Extension;
|
||||||
joinPaths: JoinPaths;
|
joinPaths: JoinPaths;
|
||||||
@ -125,13 +125,11 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
// Transform userExtensions to a state object for storing into ExtensionsStore
|
// Transform userExtensions to a state object for storing into ExtensionsStore
|
||||||
@computed get storeState() {
|
@computed get storeState() {
|
||||||
return Object.fromEntries(
|
return Array.from(this.userExtensions)
|
||||||
Array.from(this.userExtensions)
|
.map(([extId, extension]) => [extId, {
|
||||||
.map(([extId, extension]) => [extId, {
|
enabled: extension.isEnabled,
|
||||||
enabled: extension.isEnabled,
|
name: extension.manifest.name,
|
||||||
name: extension.manifest.name,
|
}] as const);
|
||||||
}]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@ -1,13 +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 enabledExtensionsStateInjectable from "../../enabled-extensions-state.injectable";
|
|
||||||
|
|
||||||
const updateExtensionsStateInjectable = getInjectable({
|
|
||||||
id: "update-extensions-state",
|
|
||||||
instantiate: (di) => di.inject(enabledExtensionsStateInjectable).mergeState,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default updateExtensionsStateInjectable;
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { iter } from "@k8slens/utilities";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import enabledExtensionsStateInjectable from "./state.injectable";
|
||||||
|
|
||||||
|
const enabledExtensionsInjectable = getInjectable({
|
||||||
|
id: "enabled-extensions",
|
||||||
|
instantiate: (di) => {
|
||||||
|
const state = di.inject(enabledExtensionsStateInjectable);
|
||||||
|
|
||||||
|
return computed(() => (
|
||||||
|
iter.chain(state.values())
|
||||||
|
.filter(({ enabled }) => enabled)
|
||||||
|
.map(({ name }) => name)
|
||||||
|
.toArray()
|
||||||
|
));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enabledExtensionsInjectable;
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 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 enabledExtensionsStateInjectable from "./state.injectable";
|
||||||
|
|
||||||
|
export interface IsEnabledExtensionDescriptor {
|
||||||
|
readonly id: string;
|
||||||
|
readonly isBundled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IsExtensionEnabled = (desc: IsEnabledExtensionDescriptor) => boolean;
|
||||||
|
|
||||||
|
const isExtensionEnabledInjectable = getInjectable({
|
||||||
|
id: "is-extension-enabled",
|
||||||
|
instantiate: (di): IsExtensionEnabled => {
|
||||||
|
const state = di.inject(enabledExtensionsStateInjectable);
|
||||||
|
|
||||||
|
return ({ id, isBundled }) => isBundled || (state.get(id)?.enabled ?? false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default isExtensionEnabledInjectable;
|
||||||
@ -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 enabledExtensionsMigrationDeclarationInjectionToken = getInjectionToken<MigrationDeclaration>({
|
||||||
|
id: "enabled-extensions-migration-declaration",
|
||||||
|
});
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
|
||||||
|
export interface LensExtensionState {
|
||||||
|
enabled?: boolean;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledExtensionsStateInjectable = getInjectable({
|
||||||
|
id: "enabled-extensions-state",
|
||||||
|
instantiate: () => observable.map<LensExtensionId, LensExtensionState>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enabledExtensionsStateInjectable;
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { action, toJS } from "mobx";
|
||||||
|
import createPersistentStorageInjectable from "../../../../common/persistent-storage/create.injectable";
|
||||||
|
import persistentStorageMigrationsInjectable from "../../../../common/persistent-storage/migrations.injectable";
|
||||||
|
import storageMigrationVersionInjectable from "../../../../common/persistent-storage/storage-migration-version.injectable";
|
||||||
|
import { enabledExtensionsMigrationDeclarationInjectionToken } from "./migrations";
|
||||||
|
import type { LensExtensionState } from "./state.injectable";
|
||||||
|
import enabledExtensionsStateInjectable from "./state.injectable";
|
||||||
|
|
||||||
|
interface EnabledExtensionsStorageModal {
|
||||||
|
extensions: [LensExtensionId, LensExtensionState][];
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledExtensionsPersistentStorageInjectable = getInjectable({
|
||||||
|
id: "enabled-extensions-persistent-storage",
|
||||||
|
instantiate: (di) => {
|
||||||
|
const createPersistentStorage = di.inject(createPersistentStorageInjectable);
|
||||||
|
const state = di.inject(enabledExtensionsStateInjectable);
|
||||||
|
|
||||||
|
return createPersistentStorage<EnabledExtensionsStorageModal>({
|
||||||
|
configName: "lens-extensions",
|
||||||
|
fromStore: action(({ extensions = [] }) => {
|
||||||
|
state.replace(extensions);
|
||||||
|
}),
|
||||||
|
toJSON: () => ({
|
||||||
|
extensions: [...toJS(state)],
|
||||||
|
}),
|
||||||
|
projectVersion: di.inject(storageMigrationVersionInjectable, enabledExtensionsMigrationDeclarationInjectionToken),
|
||||||
|
migrations: di.inject(persistentStorageMigrationsInjectable, enabledExtensionsMigrationDeclarationInjectionToken),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enabledExtensionsPersistentStorageInjectable;
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { IObservableMapInitialValues } from "mobx";
|
||||||
|
import { action } from "mobx";
|
||||||
|
import type { LensExtensionState } from "./state.injectable";
|
||||||
|
import enabledExtensionsStateInjectable from "./state.injectable";
|
||||||
|
|
||||||
|
export type UpdateExtensionsState = (state: IObservableMapInitialValues<LensExtensionId, LensExtensionState>) => void;
|
||||||
|
|
||||||
|
const updateExtensionsStateInjectable = getInjectable({
|
||||||
|
id: "update-extensions-state",
|
||||||
|
instantiate: (di): UpdateExtensionsState => {
|
||||||
|
const state = di.inject(enabledExtensionsStateInjectable);
|
||||||
|
|
||||||
|
return action((newState) => state.merge(newState));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default updateExtensionsStateInjectable;
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { beforeApplicationIsLoadingInjectionToken } from "@k8slens/application";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import enabledExtensionsPersistentStorageInjectable from "../common/storage.injectable";
|
||||||
|
|
||||||
|
const loadEnabledExtensionsStorageInjectable = getInjectable({
|
||||||
|
id: "load-enabled-extensions-storage",
|
||||||
|
instantiate: (di) => ({
|
||||||
|
run: () => {
|
||||||
|
const storage = di.inject(enabledExtensionsPersistentStorageInjectable);
|
||||||
|
|
||||||
|
storage.loadAndStartSyncing();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
injectionToken: beforeApplicationIsLoadingInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default loadEnabledExtensionsStorageInjectable;
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { isObject } from "@k8slens/utilities";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { enabledExtensionsMigrationDeclarationInjectionToken } from "../common/migrations";
|
||||||
|
|
||||||
|
const enabledExtensionsMigrationV650Injectable = getInjectable({
|
||||||
|
id: "enabled-extensions-migration-v650",
|
||||||
|
instantiate: () => ({
|
||||||
|
version: "6.5.0",
|
||||||
|
run: (store) => {
|
||||||
|
const extensions = store.get("extensions");
|
||||||
|
|
||||||
|
if (!isObject(extensions)) {
|
||||||
|
store.set("extensions", undefined);
|
||||||
|
} else {
|
||||||
|
store.set("extensions", Object.entries(extensions));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
injectionToken: enabledExtensionsMigrationDeclarationInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enabledExtensionsMigrationV650Injectable;
|
||||||
@ -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 enabledExtensionsPersistentStorageInjectable from "../common/storage.injectable";
|
||||||
|
|
||||||
|
const loadEnabledExtensionsStorageInjectable = getInjectable({
|
||||||
|
id: "load-enabled-extensions-storage",
|
||||||
|
instantiate: (di) => ({
|
||||||
|
run: () => {
|
||||||
|
const storage = di.inject(enabledExtensionsPersistentStorageInjectable);
|
||||||
|
|
||||||
|
storage.loadAndStartSyncing();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
injectionToken: beforeFrameStartsSecondInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default loadEnabledExtensionsStorageInjectable;
|
||||||
@ -10,11 +10,9 @@ import { delay, noop } from "@k8slens/utilities";
|
|||||||
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
|
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
|
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||||
import enabledExtensionsStateInjectable from "../../../extensions/enabled-extensions-state.injectable";
|
|
||||||
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||||
import { LensExtension } from "../../../extensions/lens-extension";
|
import { LensExtension } from "../../../extensions/lens-extension";
|
||||||
import type { ObservableMap } from "mobx";
|
import type { ObservableMap } from "mobx";
|
||||||
import { computed } from "mobx";
|
|
||||||
import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable";
|
import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable";
|
||||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
|
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
|
||||||
@ -23,6 +21,8 @@ import pathExistsInjectable from "../../../common/fs/path-exists.injectable";
|
|||||||
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
|
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
|
||||||
import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable";
|
import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable";
|
||||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||||
|
import type { LensExtensionState } from "../../../features/extensions/enabled/common/state.injectable";
|
||||||
|
import enabledExtensionsStateInjectable from "../../../features/extensions/enabled/common/state.injectable";
|
||||||
|
|
||||||
function throwIfDefined(val: any): void {
|
function throwIfDefined(val: any): void {
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
@ -33,7 +33,7 @@ function throwIfDefined(val: any): void {
|
|||||||
describe("protocol router tests", () => {
|
describe("protocol router tests", () => {
|
||||||
let extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
|
let extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
|
||||||
let lpr: LensProtocolRouterMain;
|
let lpr: LensProtocolRouterMain;
|
||||||
let enabledExtensions: Set<string>;
|
let enabledExtensions: ObservableMap<LensExtensionId, LensExtensionState>;
|
||||||
let broadcastMessageMock: jest.Mock;
|
let broadcastMessageMock: jest.Mock;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -44,13 +44,7 @@ describe("protocol router tests", () => {
|
|||||||
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
|
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
|
||||||
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
|
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
|
||||||
|
|
||||||
enabledExtensions = new Set();
|
enabledExtensions = di.inject(enabledExtensionsStateInjectable);
|
||||||
|
|
||||||
di.override(enabledExtensionsStateInjectable, () => ({
|
|
||||||
isEnabled: ({ id, isBundled }) => isBundled || enabledExtensions.has(id),
|
|
||||||
enabledExtensions: computed(() => []),
|
|
||||||
mergeState: noop,
|
|
||||||
}));
|
|
||||||
|
|
||||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||||
|
|
||||||
@ -97,7 +91,7 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extensionInstances.set(extId, ext);
|
extensionInstances.set(extId, ext);
|
||||||
enabledExtensions.add(extId);
|
enabledExtensions.set(extId, { name: "@mirantis/minikube" });
|
||||||
|
|
||||||
lpr.addInternalHandler("/", noop);
|
lpr.addInternalHandler("/", noop);
|
||||||
|
|
||||||
@ -177,7 +171,7 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extensionInstances.set(extId, ext);
|
extensionInstances.set(extId, ext);
|
||||||
enabledExtensions.add(extId);
|
enabledExtensions.set(extId, { name: "@foobar/icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||||
@ -216,7 +210,7 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extensionInstances.set(extId, ext);
|
extensionInstances.set(extId, ext);
|
||||||
enabledExtensions.add(extId);
|
enabledExtensions.set(extId, { name: "@foobar/icecream" });
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -242,12 +236,9 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extensionInstances.set(extId, ext);
|
extensionInstances.set(extId, ext);
|
||||||
enabledExtensions.add(extId);
|
enabledExtensions.set(extId, { name: "icecream" });
|
||||||
}
|
}
|
||||||
|
|
||||||
enabledExtensions.add("@foobar/icecream");
|
|
||||||
enabledExtensions.add("icecream");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -5,22 +5,21 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
import { LensProtocolRouterMain } from "./lens-protocol-router-main";
|
import { LensProtocolRouterMain } from "./lens-protocol-router-main";
|
||||||
import enabledExtensionsStateInjectable from "../../../extensions/enabled-extensions-state.injectable";
|
|
||||||
import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable";
|
import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable";
|
||||||
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
|
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
|
import isExtensionEnabledInjectable from "../../../features/extensions/enabled/common/is-enabled.injectable";
|
||||||
|
|
||||||
const lensProtocolRouterMainInjectable = getInjectable({
|
const lensProtocolRouterMainInjectable = getInjectable({
|
||||||
id: "lens-protocol-router-main",
|
id: "lens-protocol-router-main",
|
||||||
|
|
||||||
instantiate: (di) =>
|
instantiate: (di) => new LensProtocolRouterMain({
|
||||||
new LensProtocolRouterMain({
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
isExtensionEnabled: di.inject(isExtensionEnabledInjectable),
|
||||||
extensionsStore: di.inject(enabledExtensionsStateInjectable),
|
showApplicationWindow: di.inject(showApplicationWindowInjectable),
|
||||||
showApplicationWindow: di.inject(showApplicationWindowInjectable),
|
broadcastMessage: di.inject(broadcastMessageInjectable),
|
||||||
broadcastMessage: di.inject(broadcastMessageInjectable),
|
logger: di.inject(loggerInjectable),
|
||||||
logger: di.inject(loggerInjectable),
|
}),
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default lensProtocolRouterMainInjectable;
|
export default lensProtocolRouterMainInjectable;
|
||||||
|
|||||||
@ -5,17 +5,17 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
import { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer";
|
import { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer";
|
||||||
import enabledExtensionsStateInjectable from "../../../extensions/enabled-extensions-state.injectable";
|
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
import showErrorNotificationInjectable from "../../components/notifications/show-error-notification.injectable";
|
import showErrorNotificationInjectable from "../../components/notifications/show-error-notification.injectable";
|
||||||
import showShortInfoNotificationInjectable from "../../components/notifications/show-short-info.injectable";
|
import showShortInfoNotificationInjectable from "../../components/notifications/show-short-info.injectable";
|
||||||
|
import isExtensionEnabledInjectable from "../../../features/extensions/enabled/common/is-enabled.injectable";
|
||||||
|
|
||||||
const lensProtocolRouterRendererInjectable = getInjectable({
|
const lensProtocolRouterRendererInjectable = getInjectable({
|
||||||
id: "lens-protocol-router-renderer",
|
id: "lens-protocol-router-renderer",
|
||||||
|
|
||||||
instantiate: (di) => new LensProtocolRouterRenderer({
|
instantiate: (di) => new LensProtocolRouterRenderer({
|
||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
extensionsStore: di.inject(enabledExtensionsStateInjectable),
|
isExtensionEnabled: di.inject(isExtensionEnabledInjectable),
|
||||||
logger: di.inject(loggerInjectable),
|
logger: di.inject(loggerInjectable),
|
||||||
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
||||||
showShortInfoNotification: di.inject(showShortInfoNotificationInjectable),
|
showShortInfoNotification: di.inject(showShortInfoNotificationInjectable),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user